Merge pull request #747 from MyEtherWallet/develop

Release 0.0.7
This commit is contained in:
Daniel Ternyak 2018-01-08 01:20:16 -06:00 committed by GitHub
commit eeb315c97d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
291 changed files with 7714 additions and 2612 deletions

View File

@ -4,6 +4,5 @@
"useTabs": false,
"semi": true,
"tabWidth": 2,
"trailingComma":
"none"
"trailingComma": "none"
}

View File

@ -20,6 +20,8 @@ install:
jobs:
include:
- stage: test
script: npm run prettier:diff
- stage: test
script: npm run test
- stage: test

168
README.md
View File

@ -39,22 +39,25 @@ npm run dev:https
```
#### Address Derivation Checker:
EthereumJS-Util previously contained a bug that would incorrectly derive addresses from private keys with a 1/128 probability of occurring. A summary of this issue can be found [here](https://www.reddit.com/r/ethereum/comments/48rt6n/using_myetherwalletcom_just_burned_me_for/d0m4c6l/).
As a reactionary measure, the address derivation checker was created.
As a reactionary measure, the address derivation checker was created.
To test for correct address derivation, the address derivation checker uses multiple sources of address derivation (EthereumJS and PyEthereum) to ensure that multiple official implementations derive the same address for any given private key.
##### The derivation checker utility assumes that you have:
1. Docker installed/available
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, [Docker for Mac](https://docs.docker.com/docker-for-mac/) is suggested)
2. `docker pull dternyak/eth-priv-to-addr`
##### Run Derivation Checker
The derivation checker utility runs as part of the integration test suite.
```bash
@ -84,7 +87,6 @@ npm run test:int
The following are guides for developers to follow for writing compliant code.
### Redux and Actions
Each reducer has one file in `reducers/[namespace].ts` that contains the reducer
@ -116,7 +118,7 @@ export function [namespace](
return {
...state,
// Alterations to state
};
};
default:
return state;
}
@ -124,11 +126,12 @@ export function [namespace](
```
#### 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`
* 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`
```
├── common
@ -139,27 +142,30 @@ export function [namespace](
├── 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 interface NameOfActionAction {
type: TypeKeys.NAMESPACE_NAME_OF_ACTION,
/* Rest of the action object shape */
};
type: TypeKeys.NAMESPACE_NAME_OF_ACTION;
/* Rest of the action object shape */
}
/*** Action Union ***/
export type NamespaceAction =
| ActionOneAction
| ActionTwoAction
| ActionThreeAction;
export type NamespaceAction = ActionOneAction | ActionTwoAction | ActionThreeAction;
```
##### actionCreators.ts
```ts
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';
@ -172,7 +178,9 @@ export function nameOfAction(): interfaces.NameOfActionAction {
};
};
```
##### index.ts
```ts
export * from './actionCreators';
export * from './actionTypes';
@ -215,60 +223,34 @@ 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.
Props made available through higher order components can be tricky to type. You can inherit the injected props, and in the case of react router, specialize the generic in `withRouter` so it can omit all of its injected props from the component.
```ts
interface MyComponentProps {
name: string;
countryCode?: string;
routerLocation: { pathname: string };
}
import { RouteComponentProps } from 'react-router-dom';
...
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 {
interface MyComponentProps extends RouteComponentProps<{}> {
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;
const { name, countryCode, location } = this.props; // location being the one of the injected props from the withRouter HOC
...
}
}
export default withRouter<Props>(MyComponent);
```
## 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) {
@ -279,6 +261,7 @@ public onValueChange = (e: React.FormEvent<HTMLInputElement>) => {
}
};
```
Where you type the event as a `React.FormEvent` of type `HTML<TYPE>Element`.
## Class names
@ -292,18 +275,18 @@ 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:
```ts
import React from "react";
import React from 'react';
import "./MyComponent.scss";
import './MyComponent.scss';
export default class MyComponent extends React.component<{}, {}> {
render() {
return (
<div className="MyComponent">
<div className="MyComponent-child">Hello!</div>
</div>
);
}
render() {
return (
<div className="MyComponent">
<div className="MyComponent-child">Hello!</div>
</div>
);
}
}
```
@ -311,15 +294,15 @@ These style modules adhere to [SuitCSS naming convention](https://github.com/sui
```scss
.MyComponent {
/* Styles */
/* Styles */
&-child {
/* Styles */
&-child {
/* Styles */
&.is-hidden {
display: none;
}
}
&.is-hidden {
display: none;
}
}
}
```
@ -329,10 +312,10 @@ create a new namespace (Potentially breaking that out into its own component.)
Variables and mixins can be imported from the files in `common/styles`:
```scss
@import "sass/colors";
@import 'sass/colors';
code {
color: $code-color;
color: $code-color;
}
```
@ -350,35 +333,36 @@ When working on a module that has styling in Less, try to do the following:
* Ensure that there has been little to no deviation from screenshot
#### Adding Icon-fonts
1. Download chosen icon-font
1. Declare css font-family:
```
@font-face {
font-family: 'social-media';
src: url('../assets/fonts/social-media.eot');
src: url('../assets/fonts/social-media.eot') format('embedded-opentype'),
url('../assets/fonts/social-media.woff2') format('woff2'),
url('../assets/fonts/social-media.woff') format('woff'),
url('../assets/fonts/social-media.ttf') format('truetype'),
url('../assets/fonts/social-media.svg') format('svg');
font-weight: normal;
font-style: normal;
}
```
1. Declare css font-family:
```
@font-face {
font-family: 'social-media';
src: url('../assets/fonts/social-media.eot');
src: url('../assets/fonts/social-media.eot') format('embedded-opentype'),
url('../assets/fonts/social-media.woff2') format('woff2'),
url('../assets/fonts/social-media.woff') format('woff'),
url('../assets/fonts/social-media.ttf') format('truetype'),
url('../assets/fonts/social-media.svg') format('svg');
font-weight: normal;
font-style: normal;
}
```
1. Create classes for each icon using their unicode character
```
.sm-logo-facebook:before {
content: '\ea02';
}
```
* [How to get unicode icon values?](https://stackoverflow.com/questions/27247145/get-the-unicode-icon-value-from-a-custom-font)
```
.sm-logo-facebook:before {
content: '\ea02';
}
```
* [How to get unicode icon values?](https://stackoverflow.com/questions/27247145/get-the-unicode-icon-value-from-a-custom-font)
1. Write some markup:
```
<a href="/">
<i className={`sm-icon sm-logo-${text} sm-24px`} />
Hello World
</a>
```
```
<a href="/">
<i className={`sm-icon sm-logo-${text} sm-24px`} />
Hello World
</a>
```
## Thanks & Support

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { withRouter, Switch, Redirect, Router, Route } from 'react-router-dom';
import { withRouter, Switch, Redirect, HashRouter, Route, BrowserRouter } from 'react-router-dom';
// Components
import Contracts from 'containers/Tabs/Contracts';
import ENS from 'containers/Tabs/ENS';
@ -11,11 +11,14 @@ import Swap from 'containers/Tabs/Swap';
import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage';
import BroadcastTx from 'containers/Tabs/BroadcastTx';
import ErrorScreen from 'components/ErrorScreen';
import PageNotFound from 'components/PageNotFound';
import LogOutPrompt from 'components/LogOutPrompt';
import { Aux } from 'components/ui';
import { Store } from 'redux';
import { AppState } from 'reducers';
// TODO: fix this
interface Props {
store: any;
history: any;
store: Store<AppState>;
}
interface State {
@ -27,12 +30,12 @@ export default class Root extends Component<Props, State> {
error: null
};
public componentDidCatch(error) {
public componentDidCatch(error: Error) {
this.setState({ error });
}
public render() {
const { store, history } = this.props;
const { store } = this.props;
const { error } = this.state;
if (error) {
@ -40,24 +43,38 @@ export default class Root extends Component<Props, State> {
}
// key={Math.random()} = hack for HMR from https://github.com/webpack/webpack-dev-server/issues/395
const routes = (
<Switch>
<Route exact={true} path="/" component={GenerateWallet} />
<Route path="/generate" component={GenerateWallet}>
<Route path="keystore" component={GenerateWallet} />
<Route path="mnemonic" component={GenerateWallet} />
</Route>
<Route path="/help" component={Help} />
<Route path="/swap" component={Swap} />
<Route path="/account" component={SendTransaction}>
<Route path="send" component={SendTransaction} />
<Route path="info" component={SendTransaction} />
</Route>
<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} />
<Route component={PageNotFound} />
</Switch>
);
const Router = process.env.BUILD_DOWNLOADABLE ? HashRouter : BrowserRouter;
return (
<Provider store={store} key={Math.random()}>
<Router history={history} key={Math.random()}>
<div>
<Route exact={true} path="/" component={GenerateWallet} />
<Route path="/help" component={Help} />
<Route path="/swap" component={Swap} />
<Route path="/account" component={SendTransaction}>
<Route path="send" component={SendTransaction} />
<Route path="info" component={SendTransaction} />
</Route>
<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} />
<Router key={Math.random()}>
<Aux>
{routes}
<LegacyRoutes />
</div>
<LogOutPrompt />
</Aux>
</Router>
</Provider>
);

View File

@ -1,22 +0,0 @@
import { generate } from 'ethereumjs-wallet';
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';
export type TGenerateNewWallet = typeof generateNewWallet;
export function generateNewWallet(password: string): interfaces.GenerateNewWalletAction {
return {
type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET,
wallet: generate(),
password
};
}
export type TContinueToPaper = typeof continueToPaper;
export function continueToPaper(): interfaces.ContinueToPaperAction {
return { type: TypeKeys.GENERATE_WALLET_CONTINUE_TO_PAPER };
}
export type TResetGenerateWallet = typeof resetGenerateWallet;
export function resetGenerateWallet(): interfaces.ResetGenerateWalletAction {
return { type: TypeKeys.GENERATE_WALLET_RESET };
}

View File

@ -1,25 +0,0 @@
import { IFullWallet } from 'ethereumjs-wallet';
import { TypeKeys } from './constants';
/*** Generate Wallet File ***/
export interface GenerateNewWalletAction {
type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET;
wallet: IFullWallet;
password: string;
}
/*** Reset Generate Wallet ***/
export interface ResetGenerateWalletAction {
type: TypeKeys.GENERATE_WALLET_RESET;
}
/*** Confirm Continue To Paper ***/
export interface ContinueToPaperAction {
type: TypeKeys.GENERATE_WALLET_CONTINUE_TO_PAPER;
}
/*** Action Union ***/
export type GenerateWalletAction =
| GenerateNewWalletAction
| ContinueToPaperAction
| ResetGenerateWalletAction;

View File

@ -1,5 +0,0 @@
export enum TypeKeys {
GENERATE_WALLET_GENERATE_WALLET = 'GENERATE_WALLET_GENERATE_WALLET',
GENERATE_WALLET_CONTINUE_TO_PAPER = 'GENERATE_WALLET_CONTINUE_TO_PAPER',
GENERATE_WALLET_RESET = 'GENERATE_WALLET_RESET'
}

View File

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

View File

@ -1,31 +1,39 @@
import { handleJSONResponse } from 'api/utils';
export const rateSymbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP', 'ETH'];
export const rateSymbols: Symbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP', 'ETH'];
export type Symbols = (keyof ISymbol)[];
// TODO - internationalize
const ERROR_MESSAGE = 'Could not fetch rate data.';
const CCApi = 'https://min-api.cryptocompare.com';
const CCRates = (symbols: string[]) => {
const tsyms = rateSymbols.concat(symbols).join(',');
const tsyms = rateSymbols.concat(symbols as any).join(',');
return `${CCApi}/data/price?fsym=ETH&tsyms=${tsyms}`;
};
export interface CCResponse {
[symbol: string]: {
USD: number;
EUR: number;
GBP: number;
BTC: number;
CHF: number;
REP: number;
ETH: number;
};
[symbol: string]: ISymbol;
}
interface ISymbol {
USD: number;
EUR: number;
GBP: number;
BTC: number;
CHF: number;
REP: number;
ETH: number;
}
interface IRates extends ISymbol {
Response?: 'Error';
}
export const fetchRates = (symbols: string[] = []): Promise<CCResponse> =>
fetch(CCRates(symbols))
.then(response => handleJSONResponse(response, ERROR_MESSAGE))
.then(rates => {
.then((rates: IRates) => {
// API errors come as 200s, so check the json for error
if (rates.Response && rates.Response === 'Error') {
throw new Error('Failed to fetch rates');
@ -35,12 +43,15 @@ export const fetchRates = (symbols: string[] = []): Promise<CCResponse> =>
// do it all in one request
// to their respective rates via ETH.
return symbols.reduce(
(eqRates, sym) => {
(eqRates, sym: keyof ISymbol) => {
if (rates[sym]) {
eqRates[sym] = rateSymbols.reduce((symRates, rateSym) => {
symRates[rateSym] = 1 / rates[sym] * rates[rateSym];
return symRates;
}, {});
eqRates[sym] = rateSymbols.reduce(
(symRates, rateSym) => {
symRates[rateSym] = 1 / rates[sym] * rates[rateSym];
return symRates;
},
{} as ISymbol
);
}
return eqRates;
},
@ -54,6 +65,6 @@ export const fetchRates = (symbols: string[] = []): Promise<CCResponse> =>
REP: rates.REP,
ETH: 1
}
}
} as CCResponse
);
});

View File

@ -27,6 +27,16 @@ export function loadBityRatesSucceededSwap(
};
}
export type TLoadShapeshiftSucceededSwap = typeof loadShapeshiftRatesSucceededSwap;
export function loadShapeshiftRatesSucceededSwap(
payload
): interfaces.LoadShapshiftRatesSucceededSwapAction {
return {
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED,
payload
};
}
export type TDestinationAddressSwap = typeof destinationAddressSwap;
export function destinationAddressSwap(payload?: string): interfaces.DestinationAddressSwapAction {
return {
@ -49,6 +59,13 @@ export function loadBityRatesRequestedSwap(): interfaces.LoadBityRatesRequestedS
};
}
export type TLoadShapeshiftRequestedSwap = typeof loadShapeshiftRatesRequestedSwap;
export function loadShapeshiftRatesRequestedSwap(): interfaces.LoadShapeshiftRequestedSwapAction {
return {
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED
};
}
export type TStopLoadBityRatesSwap = typeof stopLoadBityRatesSwap;
export function stopLoadBityRatesSwap(): interfaces.StopLoadBityRatesSwapAction {
return {
@ -56,6 +73,13 @@ export function stopLoadBityRatesSwap(): interfaces.StopLoadBityRatesSwapAction
};
}
export type TStopLoadShapeshiftRatesSwap = typeof stopLoadShapeshiftRatesSwap;
export function stopLoadShapeshiftRatesSwap(): interfaces.StopLoadShapeshiftRatesSwapAction {
return {
type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES
};
}
export type TOrderTimeSwap = typeof orderTimeSwap;
export function orderTimeSwap(payload: number): interfaces.OrderSwapTimeSwapAction {
return {
@ -74,6 +98,16 @@ export function bityOrderCreateSucceededSwap(
};
}
export type TShapeshiftOrderCreateSucceededSwap = typeof shapeshiftOrderCreateSucceededSwap;
export function shapeshiftOrderCreateSucceededSwap(
payload: interfaces.ShapeshiftOrderResponse
): interfaces.ShapeshiftOrderCreateSucceededSwapAction {
return {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED,
payload
};
}
export type TBityOrderCreateRequestedSwap = typeof bityOrderCreateRequestedSwap;
export function bityOrderCreateRequestedSwap(
amount: number,
@ -82,7 +116,7 @@ export function bityOrderCreateRequestedSwap(
mode: number = 0
): interfaces.BityOrderCreateRequestedSwapAction {
return {
type: TypeKeys.SWAP_ORDER_CREATE_REQUESTED,
type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED,
payload: {
amount,
destinationAddress,
@ -92,29 +126,70 @@ export function bityOrderCreateRequestedSwap(
};
}
export function bityOrderCreateFailedSwap(): interfaces.BityOrderCreateFailedSwapAction {
export type TShapeshiftOrderCreateRequestedSwap = typeof shapeshiftOrderCreateRequestedSwap;
export function shapeshiftOrderCreateRequestedSwap(
withdrawal: string,
originKind: string,
destinationKind: string,
destinationAmount: number
): interfaces.ShapeshiftOrderCreateRequestedSwapAction {
return {
type: TypeKeys.SWAP_ORDER_CREATE_FAILED
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED,
payload: {
withdrawal,
originKind,
destinationKind,
destinationAmount
}
};
}
export type TOrderStatusSucceededSwap = typeof orderStatusSucceededSwap;
export function orderStatusSucceededSwap(
export function bityOrderCreateFailedSwap(): interfaces.BityOrderCreateFailedSwapAction {
return {
type: TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED
};
}
export function shapeshiftOrderCreateFailedSwap(): interfaces.ShapeshiftOrderCreateFailedSwapAction {
return {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED
};
}
export type TBityOrderStatusSucceededSwap = typeof bityOrderStatusSucceededSwap;
export function bityOrderStatusSucceededSwap(
payload: interfaces.BityOrderResponse
): interfaces.OrderStatusSucceededSwapAction {
): interfaces.BityOrderStatusSucceededSwapAction {
return {
type: TypeKeys.SWAP_BITY_ORDER_STATUS_SUCCEEDED,
payload
};
}
export type TOrderStatusRequestedSwap = typeof orderStatusRequestedSwap;
export function orderStatusRequestedSwap(): interfaces.OrderStatusRequestedSwapAction {
export type TShapeshiftOrderStatusSucceededSwap = typeof shapeshiftOrderStatusSucceededSwap;
export function shapeshiftOrderStatusSucceededSwap(
payload: interfaces.ShapeshiftStatusResponse
): interfaces.ShapeshiftOrderStatusSucceededSwapAction {
return {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED,
payload
};
}
export type TBityOrderStatusRequestedSwap = typeof bityOrderStatusRequested;
export function bityOrderStatusRequested(): interfaces.BityOrderStatusRequestedSwapAction {
return {
type: TypeKeys.SWAP_BITY_ORDER_STATUS_REQUESTED
};
}
export type TShapeshiftOrderStatusRequestedSwap = typeof shapeshiftOrderStatusRequested;
export function shapeshiftOrderStatusRequested(): interfaces.ShapeshiftOrderStatusRequestedSwapAction {
return {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED
};
}
export type TStartOrderTimerSwap = typeof startOrderTimerSwap;
export function startOrderTimerSwap(): interfaces.StartOrderTimerSwapAction {
return {
@ -136,9 +211,45 @@ export function startPollBityOrderStatus(): interfaces.StartPollBityOrderStatusA
};
}
export type TStartPollShapeshiftOrderStatus = typeof startPollShapeshiftOrderStatus;
export function startPollShapeshiftOrderStatus(): interfaces.StartPollShapeshiftOrderStatusAction {
return {
type: TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS
};
}
export type TStopPollBityOrderStatus = typeof stopPollBityOrderStatus;
export function stopPollBityOrderStatus(): interfaces.StopPollBityOrderStatusAction {
return {
type: TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS
};
}
export type TStopPollShapeshiftOrderStatus = typeof stopPollShapeshiftOrderStatus;
export function stopPollShapeshiftOrderStatus(): interfaces.StopPollShapeshiftOrderStatusAction {
return {
type: TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS
};
}
export type TConfigureLiteSend = typeof configureLiteSend;
export function configureLiteSend(): interfaces.ConfigureLiteSendAction {
return { type: TypeKeys.SWAP_CONFIGURE_LITE_SEND };
}
export type TShowLiteSend = typeof showLiteSend;
export function showLiteSend(
payload: interfaces.ShowLiteSendAction['payload']
): interfaces.ShowLiteSendAction {
return { type: TypeKeys.SWAP_SHOW_LITE_SEND, payload };
}
export type TChangeSwapProvider = typeof changeSwapProvider;
export function changeSwapProvider(
payload: interfaces.ProviderName
): interfaces.ChangeProviderSwapAcion {
return {
type: TypeKeys.SWAP_CHANGE_PROVIDER,
payload
};
}

View File

@ -9,7 +9,7 @@ export interface Pairs {
export interface SwapInput {
id: string;
amount: number;
amount: number | string;
}
export interface SwapInputs {
@ -24,6 +24,8 @@ export interface InitSwap {
export interface Option {
id: string;
status?: string;
image?: string;
}
export interface ApiResponseObj {
@ -41,6 +43,11 @@ export interface LoadBityRatesSucceededSwapAction {
payload: ApiResponse;
}
export interface LoadShapshiftRatesSucceededSwapAction {
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED;
payload: ApiResponse;
}
export interface DestinationAddressSwapAction {
type: TypeKeys.SWAP_DESTINATION_ADDRESS;
payload?: string;
@ -55,6 +62,11 @@ export interface LoadBityRatesRequestedSwapAction {
payload?: null;
}
export interface LoadShapeshiftRequestedSwapAction {
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED;
payload?: null;
}
export interface ChangeStepSwapAction {
type: TypeKeys.SWAP_STEP;
payload: number;
@ -64,13 +76,17 @@ export interface StopLoadBityRatesSwapAction {
type: TypeKeys.SWAP_STOP_LOAD_BITY_RATES;
}
export interface StopLoadShapeshiftRatesSwapAction {
type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES;
}
export interface OrderSwapTimeSwapAction {
type: TypeKeys.SWAP_ORDER_TIME;
payload: number;
}
export interface BityOrderCreateRequestedSwapAction {
type: TypeKeys.SWAP_ORDER_CREATE_REQUESTED;
type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED;
payload: {
amount: number;
destinationAddress: string;
@ -79,6 +95,16 @@ export interface BityOrderCreateRequestedSwapAction {
};
}
export interface ShapeshiftOrderCreateRequestedSwapAction {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED;
payload: {
withdrawal: string;
originKind: string;
destinationKind: string;
destinationAmount: number;
};
}
export interface BityOrderInput {
amount: string;
currency: string;
@ -99,6 +125,31 @@ export interface BityOrderResponse {
status: string;
}
export interface ShapeshiftOrderResponse {
apiPubKey?: string;
deposit: string;
depositAmount: string;
expiration: number;
expirationFormatted?: string;
inputCurrency?: string;
maxLimit: number;
minerFee: string;
orderId: string;
outputCurrency?: string;
pair: string; // e.g. eth_bat
provider?: ProviderName; // shapeshift
quotedRate: string;
withdrawal: string;
withdrawalAmount: string;
}
export interface ShapeshiftStatusResponse {
status: string;
address?: string;
withdraw?: string;
transaction: string;
}
export type BityOrderPostResponse = BityOrderResponse & {
payment_address: string;
status: string;
@ -109,23 +160,44 @@ export type BityOrderPostResponse = BityOrderResponse & {
id: string;
};
export type ProviderName = 'shapeshift' | 'bity';
export interface BityOrderCreateSucceededSwapAction {
type: TypeKeys.SWAP_BITY_ORDER_CREATE_SUCCEEDED;
payload: BityOrderPostResponse;
}
export interface BityOrderCreateFailedSwapAction {
type: TypeKeys.SWAP_ORDER_CREATE_FAILED;
export interface ShapeshiftOrderCreateSucceededSwapAction {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED;
payload: ShapeshiftOrderResponse;
}
export interface OrderStatusRequestedSwapAction {
export interface BityOrderCreateFailedSwapAction {
type: TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED;
}
export interface ShapeshiftOrderCreateFailedSwapAction {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED;
}
export interface BityOrderStatusRequestedSwapAction {
type: TypeKeys.SWAP_BITY_ORDER_STATUS_REQUESTED;
}
export interface OrderStatusSucceededSwapAction {
export interface ShapeshiftOrderStatusRequestedSwapAction {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED;
}
export interface BityOrderStatusSucceededSwapAction {
type: TypeKeys.SWAP_BITY_ORDER_STATUS_SUCCEEDED;
payload: BityOrderResponse;
}
export interface ShapeshiftOrderStatusSucceededSwapAction {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED;
payload: ShapeshiftStatusResponse;
}
export interface StartOrderTimerSwapAction {
type: TypeKeys.SWAP_ORDER_START_TIMER;
}
@ -138,22 +210,55 @@ export interface StartPollBityOrderStatusAction {
type: TypeKeys.SWAP_START_POLL_BITY_ORDER_STATUS;
}
export interface StartPollShapeshiftOrderStatusAction {
type: TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS;
}
export interface StopPollBityOrderStatusAction {
type: TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS;
}
export interface StopPollShapeshiftOrderStatusAction {
type: TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS;
}
export interface ChangeProviderSwapAcion {
type: TypeKeys.SWAP_CHANGE_PROVIDER;
payload: ProviderName;
}
export interface ConfigureLiteSendAction {
type: TypeKeys.SWAP_CONFIGURE_LITE_SEND;
}
export interface ShowLiteSendAction {
type: TypeKeys.SWAP_SHOW_LITE_SEND;
payload: boolean;
}
/*** Action Type Union ***/
export type SwapAction =
| ChangeStepSwapAction
| InitSwap
| LoadBityRatesSucceededSwapAction
| LoadShapshiftRatesSucceededSwapAction
| DestinationAddressSwapAction
| RestartSwapAction
| LoadBityRatesRequestedSwapAction
| LoadShapeshiftRequestedSwapAction
| StopLoadBityRatesSwapAction
| StopLoadShapeshiftRatesSwapAction
| BityOrderCreateRequestedSwapAction
| ShapeshiftOrderCreateRequestedSwapAction
| BityOrderCreateSucceededSwapAction
| OrderStatusSucceededSwapAction
| ShapeshiftOrderCreateSucceededSwapAction
| BityOrderStatusSucceededSwapAction
| ShapeshiftOrderStatusSucceededSwapAction
| StartPollBityOrderStatusAction
| StartPollShapeshiftOrderStatusAction
| BityOrderCreateFailedSwapAction
| OrderSwapTimeSwapAction;
| ShapeshiftOrderCreateFailedSwapAction
| OrderSwapTimeSwapAction
| ChangeProviderSwapAcion
| ConfigureLiteSendAction
| ShowLiteSendAction;

View File

@ -2,18 +2,31 @@ export enum TypeKeys {
SWAP_STEP = 'SWAP_STEP',
SWAP_INIT = 'SWAP_INIT',
SWAP_LOAD_BITY_RATES_SUCCEEDED = 'SWAP_LOAD_BITY_RATES_SUCCEEDED',
SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED = 'SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED',
SWAP_DESTINATION_ADDRESS = 'SWAP_DESTINATION_ADDRESS',
SWAP_RESTART = 'SWAP_RESTART',
SWAP_LOAD_BITY_RATES_REQUESTED = 'SWAP_LOAD_BITY_RATES_REQUESTED',
SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED = 'SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED',
SWAP_STOP_LOAD_BITY_RATES = 'SWAP_STOP_LOAD_BITY_RATES',
SWAP_STOP_LOAD_SHAPESHIFT_RATES = 'SWAP_STOP_LOAD_SHAPESHIFT_RATES',
SWAP_ORDER_TIME = 'SWAP_ORDER_TIME',
SWAP_BITY_ORDER_CREATE_SUCCEEDED = 'SWAP_BITY_ORDER_CREATE_SUCCEEDED',
SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED = 'SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED',
SWAP_BITY_ORDER_STATUS_SUCCEEDED = 'SWAP_BITY_ORDER_STATUS_SUCCEEDED',
SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED = 'SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED',
SWAP_BITY_ORDER_STATUS_REQUESTED = 'SWAP_BITY_ORDER_STATUS_REQUESTED',
SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED = 'SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED',
SWAP_ORDER_START_TIMER = 'SWAP_ORDER_START_TIMER',
SWAP_ORDER_STOP_TIMER = 'SWAP_ORDER_STOP_TIMER',
SWAP_START_POLL_BITY_ORDER_STATUS = 'SWAP_START_POLL_BITY_ORDER_STATUS',
SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS = 'SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS',
SWAP_STOP_POLL_BITY_ORDER_STATUS = 'SWAP_STOP_POLL_BITY_ORDER_STATUS',
SWAP_ORDER_CREATE_REQUESTED = 'SWAP_ORDER_CREATE_REQUESTED',
SWAP_ORDER_CREATE_FAILED = 'SWAP_ORDER_CREATE_FAILED'
SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS = 'SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS',
SWAP_BITY_ORDER_CREATE_REQUESTED = 'SWAP_ORDER_CREATE_REQUESTED',
SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED = 'SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED',
SWAP_BITY_ORDER_CREATE_FAILED = 'SWAP_ORDER_CREATE_FAILED',
SWAP_SHAPESHIFT_ORDER_CREATE_FAILED = 'SWAP_SHAPESHIFT_ORDER_CREATE_FAILED',
SWAP_CHANGE_PROVIDER = 'SWAP_CHANGE_PROVIDER',
SWAP_CONFIGURE_LITE_SEND = 'SWAP_CONFIGURE_LITE_SEND',
SWAP_SHOW_LITE_SEND = 'SWAP_SHOW_LITE_SEND'
}

View File

@ -5,6 +5,7 @@ import {
SetNonceFieldAction,
SetValueFieldAction,
InputGasLimitAction,
InputGasPriceAction,
InputDataAction,
InputNonceAction,
ResetAction,
@ -18,6 +19,12 @@ const inputGasLimit = (payload: InputGasLimitAction['payload']) => ({
payload
});
type TInputGasPrice = typeof inputGasPrice;
const inputGasPrice = (payload: InputGasPriceAction['payload']) => ({
type: TypeKeys.GAS_PRICE_INPUT,
payload
});
type TInputNonce = typeof inputNonce;
const inputNonce = (payload: InputNonceAction['payload']) => ({
type: TypeKeys.NONCE_INPUT,
@ -71,6 +78,7 @@ const reset = (): ResetAction => ({ type: TypeKeys.RESET });
export {
TInputGasLimit,
TInputGasPrice,
TInputNonce,
TInputData,
TSetGasLimitField,
@ -81,6 +89,7 @@ export {
TSetGasPriceField,
TReset,
inputGasLimit,
inputGasPrice,
inputNonce,
inputData,
setGasLimitField,

View File

@ -6,6 +6,10 @@ interface InputGasLimitAction {
type: TypeKeys.GAS_LIMIT_INPUT;
payload: string;
}
interface InputGasPriceAction {
type: TypeKeys.GAS_PRICE_INPUT;
payload: string;
}
interface InputDataAction {
type: TypeKeys.DATA_FIELD_INPUT;
payload: string;
@ -48,6 +52,7 @@ interface SetToFieldAction {
payload: {
raw: string;
value: Address | null;
error?: string | null;
};
}
@ -79,6 +84,7 @@ type FieldAction =
export {
InputGasLimitAction,
InputGasPriceAction,
InputDataAction,
InputNonceAction,
SetGasLimitFieldAction,

View File

@ -7,6 +7,7 @@ interface SetTokenToMetaAction {
payload: {
raw: string;
value: Address | null;
error?: string | null;
};
}

View File

@ -28,6 +28,7 @@ export enum TypeKeys {
DATA_FIELD_INPUT = 'DATA_FIELD_INPUT',
GAS_LIMIT_INPUT = 'GAS_LIMIT_INPUT',
GAS_PRICE_INPUT = 'GAS_PRICE_INPUT',
NONCE_INPUT = 'NONCE_INPUT',
DATA_FIELD_SET = 'DATA_FIELD_SET',

View File

@ -88,6 +88,34 @@ export function setTokenBalancesRejected(): types.SetTokenBalancesRejectedAction
};
}
export function setTokenBalancePending(
payload: types.SetTokenBalancePendingAction['payload']
): types.SetTokenBalancePendingAction {
return {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING,
payload
};
}
export type TSetTokenBalanceFulfilled = typeof setTokenBalanceFulfilled;
export function setTokenBalanceFulfilled(payload: {
[key: string]: {
balance: TokenValue;
error: string | null;
};
}): types.SetTokenBalanceFulfilledAction {
return {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_FULFILLED,
payload
};
}
export function setTokenBalanceRejected(): types.SetTokenBalanceRejectedAction {
return {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_REJECTED
};
}
export type TScanWalletForTokens = typeof scanWalletForTokens;
export function scanWalletForTokens(wallet: IWallet): types.ScanWalletForTokensAction {
return {

View File

@ -63,6 +63,25 @@ export interface SetTokenBalancesRejectedAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES_REJECTED;
}
export interface SetTokenBalancePendingAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING;
payload: { tokenSymbol: string };
}
export interface SetTokenBalanceFulfilledAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_FULFILLED;
payload: {
[key: string]: {
balance: TokenValue;
error: string | null;
};
};
}
export interface SetTokenBalanceRejectedAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_REJECTED;
}
export interface ScanWalletForTokensAction {
type: TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS;
payload: IWallet;
@ -108,6 +127,9 @@ export type WalletAction =
| SetTokenBalancesPendingAction
| SetTokenBalancesFulfilledAction
| SetTokenBalancesRejectedAction
| SetTokenBalancePendingAction
| SetTokenBalanceFulfilledAction
| SetTokenBalanceRejectedAction
| ScanWalletForTokensAction
| SetWalletTokensAction
| SetWalletConfigAction;

View File

@ -10,6 +10,9 @@ export enum TypeKeys {
WALLET_SET_TOKEN_BALANCES_PENDING = 'WALLET_SET_TOKEN_BALANCES_PENDING',
WALLET_SET_TOKEN_BALANCES_FULFILLED = 'WALLET_SET_TOKEN_BALANCES_FULFILLED',
WALLET_SET_TOKEN_BALANCES_REJECTED = 'WALLET_SET_TOKEN_BALANCES_REJECTED',
WALLET_SET_TOKEN_BALANCE_PENDING = 'WALLET_SET_TOKEN_BALANCE_PENDING',
WALLET_SET_TOKEN_BALANCE_FULFILLED = 'WALLET_SET_TOKEN_BALANCE_FULFILLED',
WALLET_SET_TOKEN_BALANCE_REJECTED = 'WALLET_SET_TOKEN_BALANCE_REJECTED',
WALLET_SCAN_WALLET_FOR_TOKENS = 'WALLET_SCAN_WALLET_FOR_TOKENS',
WALLET_SET_WALLET_TOKENS = 'WALLET_SET_WALLET_TOKENS',
WALLET_SET_CONFIG = 'WALLET_SET_CONFIG',

View File

@ -1,10 +1,34 @@
import bityConfig, { WhitelistedCoins } from 'config/bity';
import { checkHttpStatus, parseJSON, filter } from './utils';
import bitcoinIcon from 'assets/images/bitcoin.png';
import repIcon from 'assets/images/augur.png';
import etherIcon from 'assets/images/ether.png';
const isCryptoPair = (from: string, to: string, arr: WhitelistedCoins[]) => {
return filter(from, arr) && filter(to, arr);
};
const btcOptions = {
id: 'BTC',
status: 'available',
image: bitcoinIcon,
name: 'Bitcoin'
};
const ethOptions = {
id: 'ETH',
status: 'available',
image: etherIcon,
name: 'Ether'
};
const repOptions = {
id: 'REP',
status: 'available',
image: repIcon,
name: 'Augur'
};
export function getAllRates() {
const mappedRates = {};
return _getAllRates().then(bityRates => {
@ -14,9 +38,31 @@ export function getAllRates() {
const to = { id: pairName.substring(3, 6) };
// Check if rate exists= && check if the pair only crypto to crypto, not crypto to fiat, or any other combination
if (parseFloat(each.rate_we_sell) && isCryptoPair(from.id, to.id, ['BTC', 'ETH', 'REP'])) {
let fromOptions;
let toOptions;
switch (from.id) {
case 'BTC':
fromOptions = btcOptions;
break;
case 'ETH':
fromOptions = ethOptions;
break;
case 'REP':
fromOptions = repOptions;
}
switch (to.id) {
case 'BTC':
toOptions = btcOptions;
break;
case 'ETH':
toOptions = ethOptions;
break;
case 'REP':
toOptions = repOptions;
}
mappedRates[pairName] = {
id: pairName,
options: [from, to],
options: [fromOptions, toOptions],
rate: parseFloat(each.rate_we_sell)
};
}

175
common/api/shapeshift.ts Normal file
View File

@ -0,0 +1,175 @@
import { checkHttpStatus, parseJSON } from 'api/utils';
const SHAPESHIFT_BASE_URL = 'https://shapeshift.io';
export const SHAPESHIFT_TOKEN_WHITELIST = [
'OMG',
'REP',
'SNT',
'SNGLS',
'ZRX',
'SWT',
'ANT',
'BAT',
'BNT',
'CVC',
'DNT',
'1ST',
'GNO',
'GNT',
'EDG',
'FUN',
'RLC',
'TRST',
'GUP',
'ETH'
];
export const SHAPESHIFT_WHITELIST = [...SHAPESHIFT_TOKEN_WHITELIST, 'ETC', 'BTC'];
class ShapeshiftService {
public whitelist = SHAPESHIFT_WHITELIST;
private url = SHAPESHIFT_BASE_URL;
private apiKey = '0ca1ccd50b708a3f8c02327f0caeeece06d3ddc1b0ac749a987b453ee0f4a29bdb5da2e53bc35e57fb4bb7ae1f43c93bb098c3c4716375fc1001c55d8c94c160';
private postHeaders = {
'Content-Type': 'application/json'
};
public checkStatus(address) {
return fetch(`${this.url}/txStat/${address}`)
.then(checkHttpStatus)
.then(parseJSON);
}
public sendAmount(withdrawal, originKind, destinationKind, destinationAmount) {
const pair = `${originKind.toLowerCase()}_${destinationKind.toLowerCase()}`;
return fetch(`${this.url}/sendamount`, {
method: 'POST',
body: JSON.stringify({
amount: destinationAmount,
pair,
apiKey: this.apiKey,
withdrawal
}),
headers: new Headers(this.postHeaders)
})
.then(checkHttpStatus)
.then(parseJSON);
}
public getCoins() {
return fetch(`${this.url}/getcoins`)
.then(checkHttpStatus)
.then(parseJSON);
}
public getAllRates = async () => {
const marketInfo = await this.getMarketInfo();
const pairRates = await this.getPairRates(marketInfo);
const checkAvl = await this.checkAvl(pairRates);
const mappedRates = this.mapMarketInfo(checkAvl);
return mappedRates;
};
private getPairRates(marketInfo) {
const filteredMarketInfo = marketInfo.filter(obj => {
const { pair } = obj;
const pairArr = pair.split('_');
return this.whitelist.includes(pairArr[0]) && this.whitelist.includes(pairArr[1])
? true
: false;
});
const pairRates = filteredMarketInfo.map(p => {
const { pair } = p;
const singlePair = Promise.resolve(this.getSinglePairRate(pair));
return { ...p, ...singlePair };
});
return pairRates;
}
private async checkAvl(pairRates) {
const avlCoins = await this.getAvlCoins();
const mapAvl = pairRates.map(p => {
const { pair } = p;
const pairArr = pair.split('_');
if (pairArr[0] in avlCoins && pairArr[1] in avlCoins) {
return {
...p,
...{
[pairArr[0]]: {
name: avlCoins[pairArr[0]].name,
status: avlCoins[pairArr[0]].status,
image: avlCoins[pairArr[0]].image
},
[pairArr[1]]: {
name: avlCoins[pairArr[1]].name,
status: avlCoins[pairArr[1]].status,
image: avlCoins[pairArr[1]].image
}
}
};
}
});
return mapAvl;
}
private getAvlCoins() {
return fetch(`${this.url}/getcoins`)
.then(checkHttpStatus)
.then(parseJSON);
}
private getSinglePairRate(pair) {
return fetch(`${this.url}/rate/${pair}`)
.then(checkHttpStatus)
.then(parseJSON);
}
private getMarketInfo() {
return fetch(`${this.url}/marketinfo`)
.then(checkHttpStatus)
.then(parseJSON);
}
private isWhitelisted(coin) {
return this.whitelist.includes(coin);
}
private mapMarketInfo(marketInfo) {
const tokenMap = {};
marketInfo.forEach(m => {
const originKind = m.pair.substring(0, 3);
const destinationKind = m.pair.substring(4, 7);
if (this.isWhitelisted(originKind) && this.isWhitelisted(destinationKind)) {
const pairName = originKind + destinationKind;
const { rate, limit, min } = m;
tokenMap[pairName] = {
id: pairName,
options: [
{
id: originKind,
status: m[originKind].status,
image: m[originKind].image,
name: m[originKind].name
},
{
id: destinationKind,
status: m[destinationKind].status,
image: m[destinationKind].image,
name: m[destinationKind].name
}
],
rate,
limit,
min
};
}
});
return tokenMap;
}
}
const shapeshift = new ShapeshiftService();
export default shapeshift;

View File

@ -1,10 +1,10 @@
import { indexOf } from 'lodash';
import indexOf from 'lodash/indexOf';
export const filter = (i: any, arr: any[]) => {
return -1 !== indexOf(arr, i) ? true : false;
};
export function checkHttpStatus(response) {
export function checkHttpStatus(response: Response) {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
@ -12,11 +12,11 @@ export function checkHttpStatus(response) {
}
}
export function parseJSON(response) {
export function parseJSON(response: Response) {
return response.json();
}
export async function handleJSONResponse(response, errorMessage) {
export async function handleJSONResponse(response: Response, errorMessage: string) {
if (response.ok) {
return await response.json();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1050 350" style="enable-background:new 0 0 1050 350;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#273C51;}
.st2{fill:url(#SVGID_1_);}
.st3{fill:#466284;}
.st4{fill:#354D6A;}
.st5{fill:url(#SVGID_2_);}
.st6{fill:url(#SVGID_3_);}
.st7{fill:url(#SVGID_4_);}
.st8{fill:url(#SVGID_5_);}
.st9{fill:url(#SVGID_6_);}
.st10{fill:url(#SVGID_7_);}
.st11{fill:url(#SVGID_8_);}
.st12{fill:url(#SVGID_9_);}
.st13{fill:url(#SVGID_10_);}
.st14{fill:none;}
</style>
<g>
<g>
<g>
<path class="st0" d="M280.6,198.7c-15.2,0-31.5,6-31.5,20.6c0,13,14.9,16.8,32.6,19.7c24,3.8,47.6,8.6,47.6,35.6
c-0.2,26.9-25.9,35.6-48.8,35.6c-21.2,0-41.5-7.7-50.7-27.8l12.3-7.2c7.7,14.2,23.8,21.1,38.6,21.1c14.6,0,33.9-4.6,33.9-22.3
c0.2-14.9-16.6-19.2-34.6-21.9c-23.1-3.6-45.6-8.9-45.6-33.2c-0.3-25,25.2-33.6,45.9-33.6c17.8,0,34.8,3.6,45.4,21.8l-11.3,7
C307.9,203.7,294,198.9,280.6,198.7z"/>
<path class="st0" d="M353.8,188.1v49.2c7.2-11.1,18.5-14.9,29.3-15.1c23.8,0,35.5,15.8,35.5,39.1v46.6h-13.9v-46.4
c0-16.6-8.6-26-24-26S354,247.5,354,263v44.9h-14V187.9h13.9V188.1z"/>
<path class="st0" d="M504.4,308.2l-0.3-15.4c-6.7,11.7-19.5,17.1-31.2,17.1c-24.3,0-43.3-16.8-43.3-44.4
c0-27.4,19.4-43.9,43.5-43.7c12.7,0,25.2,5.8,31.4,16.8l0.2-15.4h13.7v84.6h-13.5L504.4,308.2z M473.5,235.2
c-16.8,0-30.3,12-30.3,30.8s13.5,31,30.3,31c40.8,0,40.8-62,0.2-62L473.5,235.2z"/>
<path class="st0" d="M529.5,223.6h13.4l0.7,16.3c6.7-11.3,19.2-17.8,32.6-17.8c24.3,0.5,42.1,17.6,42.1,43.7
c0,26.7-17.6,44-43,44c-12,0-25.4-5.1-32-17.1v55.2h-13.7V223.6z M604.2,266c0-19-12.5-30.3-29.8-30.3c-17.6,0-29.6,13-29.6,30.3
s12.5,30.3,29.6,30.5C591.3,296.5,604.2,285.1,604.2,266z"/>
<path class="st0" d="M708.9,294.3c-8.6,10.1-23.3,15.1-36.5,15.1c-26.2,0-44.5-17.3-44.5-44.2c0-25.5,18.3-43.9,43.9-43.9
c25.9,0,45.6,15.9,42.3,49.7h-72c1.5,15.6,14.4,25.4,30.7,25.4c9.6,0,21.2-3.8,26.9-10.6l9.4,8.6H708.9z M700.6,259.4
c-0.7-16.4-12-25.4-28.6-25.4c-14.7,0-27.6,8.9-30,25.2h58.6V259.4z"/>
</g>
<g>
<path class="st0" d="M771.2,198.7c-15.2,0-31.5,6-31.5,20.6c0,13,14.9,16.8,32.6,19.7c24,3.8,47.6,8.6,47.6,35.6
c-0.2,26.9-25.9,35.6-48.8,35.6c-21.2,0-41.5-7.7-50.7-27.8l12.3-7.2c7.7,14.2,23.8,21.1,38.6,21.1c14.6,0,33.9-4.6,33.9-22.3
c0.2-14.9-16.6-19.2-34.6-21.9c-23.1-3.6-45.6-8.9-45.6-33.2c-0.3-25,25.2-33.6,45.9-33.6c17.8,0,34.8,3.6,45.4,21.8l-11.3,7
C798.5,203.7,784.6,198.9,771.2,198.7z"/>
<path class="st0" d="M844.4,188.1v49.2c7.2-11.1,18.5-14.9,29.3-15.1c23.8,0,35.5,15.8,35.5,39.1v46.6h-13.9v-46.4
c0-16.6-8.6-26-24-26s-26.7,12.2-26.7,27.6v44.9h-14V187.9h13.9V188.1z"/>
<path class="st0" d="M920.6,307.8h14v-83.4h-14V307.8z M927.8,211.7l-11.1-12.2l11.1-12.2l11.1,12.2L927.8,211.7z"/>
<g>
<polygon class="st0" points="960.8,308 960.8,307.8 960.7,307.8 "/>
<path class="st0" d="M974.7,217.6c0-12.7,5.8-18.3,14.9-18.3c0.2,0,0.4,0,0.5,0l3.2-12.1c-1.3-0.2-2.7-0.3-4-0.3
c-17.8,0-28.4,11.3-28.4,30.7v6.7h-16.6v12.3h16.6v71.3h13.9v-71.3h16.8v-12.3h-16.8V217.6z"/>
</g>
<path class="st0" d="M1046.1,296.1c-1.1,0.2-2.2,0.3-3.3,0.3c-10.1,0-13.4-6.3-13.4-16.3v-43.6h18v-12.2h-17.9v-25.8l-14,1.5
v24.2h-17v12.2h17v43.6c0,18.7,8.6,29.3,26.7,29c2.2-0.1,4.4-0.3,6.5-0.8L1046.1,296.1z"/>
</g>
</g>
<polygon class="st1" points="216,82.1 230.5,-0.7 169.9,24.6 103.3,24.6 42.7,-0.8 57.3,82.1 43.6,128.3 56.5,136.4 0,186 0,232.6
64.6,321.6 125.5,342 125.6,342.1 173.2,317.7 173.3,317.6 173.3,281.5 146.3,266.7 146.3,266.7 146.3,266.7 188.7,153.8
188.4,153.8 229.6,128.3 "/>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="136.7364" y1="25.9921" x2="60.3198" y2="247.6647">
<stop offset="0.1345" style="stop-color:#2B415B"/>
<stop offset="0.3762" style="stop-color:#3B5676"/>
<stop offset="0.6923" style="stop-color:#54769E"/>
<stop offset="0.7901" style="stop-color:#52749B"/>
<stop offset="0.8614" style="stop-color:#4D6C92"/>
<stop offset="0.9244" style="stop-color:#436082"/>
<stop offset="0.9822" style="stop-color:#364F6C"/>
<stop offset="1" style="stop-color:#314863"/>
</linearGradient>
<polygon class="st2" points="97.7,100.3 0,186 136.1,264.3 136.6,102.9 "/>
<polygon class="st3" points="83.8,153.3 136.2,293.4 136.6,161.1 "/>
<polygon class="st4" points="188.7,153.8 136.2,293.4 136.6,161.1 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="230.1033" y1="127.4219" x2="34.0475" y2="14.229">
<stop offset="0" style="stop-color:#54769E"/>
<stop offset="0.4802" style="stop-color:#53749C"/>
<stop offset="0.6878" style="stop-color:#4F6F95"/>
<stop offset="0.8423" style="stop-color:#486588"/>
<stop offset="0.9095" style="stop-color:#435F80"/>
</linearGradient>
<polygon class="st5" points="230.5,-0.7 178.4,26.7 136.7,35.8 94.6,26.7 42.7,-0.8 60.6,81.9 43.6,128.3 103.2,165.3
136.3,201.3 136.3,201.6 136.5,201.4 136.7,201.6 136.7,201.3 169.8,165.3 229.6,128.3 212.6,82 "/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="342.5284" y1="63.8296" x2="150.4648" y2="63.8296">
<stop offset="0.2539" style="stop-color:#20344C"/>
<stop offset="0.4072" style="stop-color:#273D57"/>
<stop offset="0.6733" style="stop-color:#395373"/>
<stop offset="1" style="stop-color:#54769E"/>
</linearGradient>
<polygon class="st6" points="230.5,-0.7 216,82.1 229.6,128.3 212.6,82 "/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-74.3281" y1="63.7777" x2="124.2335" y2="63.7777">
<stop offset="0.2539" style="stop-color:#54769E"/>
<stop offset="0.4133" style="stop-color:#4D6E93"/>
<stop offset="0.6897" style="stop-color:#3C5777"/>
<stop offset="1" style="stop-color:#233850"/>
</linearGradient>
<polygon class="st7" points="42.7,-0.8 57.3,82.1 43.6,128.3 60.6,81.9 "/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="138.4299" y1="-77.4169" x2="134.5027" y2="85.5632">
<stop offset="6.545247e-03" style="stop-color:#54769E"/>
<stop offset="0.1993" style="stop-color:#507198"/>
<stop offset="0.4502" style="stop-color:#466488"/>
<stop offset="0.7318" style="stop-color:#354F6D"/>
<stop offset="1" style="stop-color:#21354D"/>
</linearGradient>
<polygon class="st8" points="42.7,-0.8 103.3,24.6 169.9,24.6 230.5,-0.7 178.4,26.7 136.7,35.8 94.6,26.7 "/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="173.2798" y1="-23.2345" x2="12.7505" y2="132.5687">
<stop offset="0.2539" style="stop-color:#54769E"/>
<stop offset="0.4102" style="stop-color:#4D6E93"/>
<stop offset="0.6813" style="stop-color:#3C5777"/>
<stop offset="1" style="stop-color:#22364E"/>
</linearGradient>
<polygon class="st9" points="60.6,81.9 57.6,90.2 120.5,32.2 "/>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="114.997" y1="-2.4443" x2="248.7759" y2="116.8474">
<stop offset="0.2539" style="stop-color:#54769E"/>
<stop offset="0.4102" style="stop-color:#4D6E93"/>
<stop offset="0.6813" style="stop-color:#3C5777"/>
<stop offset="1" style="stop-color:#22364E"/>
</linearGradient>
<polygon class="st10" points="212.6,82 153,32.2 215.5,89.8 "/>
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="-31.9333" y1="230.8414" x2="255.118" y2="333.2895">
<stop offset="0.2664" style="stop-color:#54769E"/>
<stop offset="1" style="stop-color:#425E7F"/>
</linearGradient>
<polygon class="st11" points="0,186 146.3,266.7 164.8,313 125.6,327.9 64.6,321.6 0,232.6 "/>
<polygon class="st0" points="121.1,252.8 64.8,321.4 64.6,321.6 125.5,342 125.6,342.1 173.2,317.7 173.3,317.6 173.3,281.5 "/>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="97.761" y1="-67.9411" x2="268.6103" y2="84.2801">
<stop offset="0.4609" style="stop-color:#54769E;stop-opacity:0"/>
<stop offset="0.5699" style="stop-color:#52739A;stop-opacity:0.2156"/>
<stop offset="0.6764" style="stop-color:#4A698E;stop-opacity:0.4266"/>
<stop offset="0.782" style="stop-color:#3D597B;stop-opacity:0.6356"/>
<stop offset="0.8863" style="stop-color:#2C435F;stop-opacity:0.8422"/>
<stop offset="0.9661" style="stop-color:#1B2E45"/>
</linearGradient>
<polygon class="st12" points="212.6,82 230.5,-0.7 178.4,26.7 153,32.2 "/>
<polygon class="st0" points="136.6,201.6 120.1,183.5 136.6,165.3 153.2,183.5 "/>
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="136.6099" y1="347.9733" x2="136.6099" y2="-96.2296">
<stop offset="0.2539" style="stop-color:#54769E"/>
<stop offset="0.4102" style="stop-color:#4D6E93"/>
<stop offset="0.6813" style="stop-color:#3C5777"/>
<stop offset="1" style="stop-color:#22364E"/>
</linearGradient>
<polygon class="st13" points="135,35.4 136.7,35.8 138.2,35.5 136.6,141 "/>
<path class="st14" d="M77.6,35.5"/>
<path class="st14" d="M75.1,35.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="620px" height="620px" viewBox="0 0 620 620" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>swap</title>
<desc>Created with Sketch.</desc>
<defs>
<circle id="path-1" cx="170" cy="170" r="170"></circle>
<circle id="path-3" cx="170" cy="170" r="170"></circle>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="swap">
<g id="Yellow-Coin" transform="translate(0.000000, 280.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<circle stroke="#0E97C0" stroke-width="20" cx="170" cy="170" r="160"></circle>
<rect id="Rectangle" fill="#FFE14D" mask="url(#mask-2)" x="-1.13333333" y="1.13333333" width="173.4" height="340"></rect>
<rect id="Rectangle" fill="#FFCC33" mask="url(#mask-2)" x="171.133333" y="1.13333333" width="173.4" height="340"></rect>
<circle id="Oval-2" stroke="#333333" stroke-width="20" mask="url(#mask-2)" cx="170" cy="170" r="160"></circle>
<circle id="Oval-3" stroke="#F28618" stroke-width="20" mask="url(#mask-2)" cx="170" cy="170" r="96.3333333"></circle>
</g>
<g id="Blue-Coin" transform="translate(280.000000, 0.000000)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<circle stroke="#0E97C0" stroke-width="20" cx="170" cy="170" r="160"></circle>
<rect id="Rectangle" fill="#6EA6E8" mask="url(#mask-4)" x="-1.13333333" y="1.13333333" width="173.4" height="340"></rect>
<rect id="Rectangle" fill="#5C9BE4" mask="url(#mask-4)" x="171.133333" y="1.13333333" width="173.4" height="340"></rect>
<circle id="Oval-2" stroke="#333333" stroke-width="20" mask="url(#mask-4)" cx="170" cy="170" r="160"></circle>
<circle id="Oval-3" stroke="#2F79CF" stroke-width="20" mask="url(#mask-4)" cx="170" cy="170" r="96.3333333"></circle>
</g>
<g id="Group" transform="translate(226.000000, 320.000000)" fill-rule="nonzero">
<polygon id="Shape" fill="#0492BE" points="311.67 8 385.42 8 385.42 171.31 144.71 171.31 144.71 234.53 13 134.44 144.71 34.34 144.71 97.56 311.67 97.56"></polygon>
<polygon id="Shape" fill="#103957" opacity="0.2" points="348.54 134.44 13 134.44 144.71 234.53 144.71 171.31 385.42 171.31 385.42 8 348.54 8"></polygon>
<path d="M152.66,250.36 L0,134.36 L152.66,18.36 L152.66,89.61 L303.82,89.61 L303.82,0 L393.38,0 L393.38,179.11 L152.66,179.11 L152.66,250.36 Z M26.11,134.36 L136.85,218.52 L136.85,163.31 L377.58,163.31 L377.58,15.8 L319.63,15.8 L319.63,105.36 L136.86,105.36 L136.86,50.17 L26.11,134.36 Z" id="Shape" fill="#000000"></path>
</g>
<g id="Group" transform="translate(197.000000, 174.500000) rotate(180.000000) translate(-197.000000, -174.500000) translate(0.000000, 49.000000)" fill-rule="nonzero">
<polygon id="Shape" fill="#0492BE" points="311.67 8 385.42 8 385.42 171.31 144.71 171.31 144.71 234.53 13 134.44 144.71 34.34 144.71 97.56 311.67 97.56"></polygon>
<polygon id="Shape" fill="#103957" opacity="0.2" points="348.54 134.44 13 134.44 144.71 234.53 144.71 171.31 385.42 171.31 385.42 8 348.54 8"></polygon>
<path d="M152.66,250.36 L0,134.36 L152.66,18.36 L152.66,89.61 L303.82,89.61 L303.82,0 L393.38,0 L393.38,179.11 L152.66,179.11 L152.66,250.36 Z M26.11,134.36 L136.85,218.52 L136.85,163.31 L377.58,163.31 L377.58,15.8 L319.63,15.8 L319.63,105.36 L136.86,105.36 L136.86,50.17 L26.11,134.36 Z" id="Shape" fill="#000000"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="180px" height="246px" viewBox="0 0 180 246" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.1 (47250) - http://www.bohemiancoding.com/sketch -->
<title>digital-bitbox</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="digital-bitbox">
<polygon id="Path-3" fill="#000000" points="90 0 126 17 126 107 180 132 180 206 90 246 0 206 0 132 54 107 54 17"></polygon>
<polygon id="Path-4" fill="#FFFFFF" points="170 136 89.7558594 171.179688 10 136 25 130 90 158 155 130"></polygon>
<polygon id="Path-5" fill="#FFFFFF" points="97 170 97 236 170 202"></polygon>
<polygon id="Path-5" fill="#FFFFFF" transform="translate(46.500000, 203.000000) scale(-1, 1) translate(-46.500000, -203.000000) " points="10 170 10 236 83 202"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="155px" height="155px" viewBox="0 0 155 155" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.1 (47250) - http://www.bohemiancoding.com/sketch -->
<title>ledger</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="ledger">
<image opacity="0.139605978" x="0" y="0" width="154" height="155" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJoAAACbCAYAAACao4KyAAAABGdBTUEAA1teXP8meAAAB+FJREFUeAHtnM9y00YcgHdXSqBMO/VACldz64EZzJ/CDBecIeVaeALCExSeIPAEkCcgPEHTc2EwFw4tUGXaA7f6XALjzrQ0xZZUrYiMldhBiqzJrvfzRZG0u/r9vt83K3ltR4oaX81mq/H5wkKrzCX8wbteEDwLyvQp0vbrVrvp+36zSFsX2/y9uRl0u0Gvrtz9aQysi3hYibbw5i7HUdT0fK+933HD+LNO0ndxv/0n9Ts87y8rqVYmnXf9+NETx8XRE1dTDOEg7EiluiLsP92KROdV0OlW5bNv0fRs1TixsCwieUMp+XHWUl7VmOh/wASGE4WaXz6SxNL65tsgkW6197a3vt9Zr7RoqWDHF26JWHyvhGoIdcBUuHztBNKJRM0/aHy1cK91fGm19+fm/bLCldKkdXHpVnKxP/QtSKlEMl5OEdA117XXDmgXyiRfaEbTz2CHPO9BMoO1mcHK4J3NttuTzL3T565899ebN9eLzG6fnNFOnV9qH57zfx3et2eTHVntg4B2Ip3dWpc+PqNPGGdP0Vpn28vznnrCbXICPQ6L1I25I0+0K3vhmCia7qjm5h/s1ZlzENAE0me3xJW9ZBsrmr5dIhkSlSbg+fdaE26ju0RLV9Cl+KH0RejgPIHsNqqXwHbC2CVa+u6SpYudnNgvSEDL9uWxY7smqpxoem2Ed5cFidJsIgHt0M7ntaFo6XQXCT4LnIiPE6UIJM9ro7fQoWiN5GOl9B5bajQaQ2A8Ae2Sdio7m4qWmpd8dpkdZAuBqRBInMpmtVQ0/S0MZrOpoGWQEQLprHa0cU0f+nDrTL7qM3KePyEwNQKx9FK3lF43y32fbGqXYCAICKHfgerbp0q/GQsRCNRI4ItjjbbSX7+u8RoMDQHhSf+y0t/xhwUE6iQQx3FL8UlAnYgZWxOQyW9Khgu2IIFAXQT0Mgei1UWXcXMEEC2Hg526CCBaXWQZN0cA0XI42KmLAKLVRZZxcwQQLYeDnboIIFpdZBk3RwDRcjjYqYsAotVFlnFzBBAth4OdugggWl1kGTdHANFyONipiwCi1UWWcXMEfP3/SnNHDnhHShkccAhcvgYC/saLx1P/x8Q1xMmQlhPg1ml5AW0JH9FsqZTlcSKa5QW0JXxEs6VSlseJaJYX0JbwEc2WSlkeJ6JZXkBbwkc0WypleZyIZnkBbQkf0WyplOVxIprlBbQlfESzpVKWx4lolhfQlvARzZZKWR4nolleQFvCRzRbKmV5nIhmeQFtCR/RbKmU5XEimuUFtCV8RLOlUpbH6bcuLN0xKofBoBu87KwZFRPBVCbgK6lWKo8yxQFC6XWS4damOCRDGUCAW6cBRXAhBERzocoG5IhoBhTBhRAQzYUqG5AjohlQBBdCQDQXqmxAjohmQBFcCAHRXKiyATkimgFFcCEERHOhygbkiGgGFMGFEBDNhSobkCOiGVAEF0JANBeqbECOiGZAEVwIAdFcqLIBOSKaAUVwIQREc6HKBuSIaAYUwYUQEM2FKhuQI6IZUAQXQkA0F6psQI6IZkARXAgB0VyosgE5IpoBRXAhBERzocoG5IhoBhTBhRAQzYUqG5AjohlQBBdCQDQXqmxAjohmQBFcCAHRXKiyATkimgFFcCEERHOhygbkiGgGFMGFEPz3YbRoUqJ++G+vjni23g/WfN/v1DE2Y0IAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAjUSkGcvXo1rHL/00OEg7Gy8eDz1X2a1LizdUVKtlA6oxg5RHN0Nfn50Z9qXOH3uyhPP99rTHrfKePyuswo9+hYmgGiFUdGwCgFEq0KPvoUJIFphVDSsQgDRqtCjb2ECiFYYFQ2rEEC0KvToW5gAohVGRcMqBBCtCj36FiaAaIVR0bAKAUSrQo++hQkgWmFUNKxCANGq0KNvYQKIVhgVDasQUFEU1fLPiasERd/ZIhD1w66KoziYrbTIxjQCsRRdJaVENNMqM2PxSE8+VSIabMxYXqRjGIEwHASq97a3blhchDNjBH570VlX3W7Qi8IBss1YcU1JJ3Prw/JGFP1oSmDEMVsEwjh6qDNKRQtedtZY5pitApuQjV7W0LfNoWhpUFKsmhAcMcwSgfBuls3wkwH9+0JtYHaCLQSqENAu6TtlNsZQNH0glOHt7ARbCFQhMFDy5mj/nGj6fpq9SxhtxN8QKEMgCqP7vz9/1BntkxNNn+htvr0Z8bHUKCP+LkFAu9Pb3Bw+m2Vdd4mm19VE/59ENj5szyCxLUZAO7PV719PHdrRZZdo+nwQPAtE/90isu2gxe5EAqkriTOvgk53XKOxoumGyDYOF8fGEcgkS50Z1yA5NlE03V533OoPzvDMNoEeh0X6TPZ68+RekmlMe4qmG+ipsPf69aJ+J6H3eUEgI6CdCH756cy4Z7KsTbb9pGi6oR4oeP7o9vswWmRRN0Pn7lbPYtoF7URRCn7Rhrrd9trIydbZ9rIQ3oqa85r6OC83CKSPUGF/dXTFv2jmpUTLBt2+0Nqp80ttX8obQsTXlFKN7Dzb2SHwYeVBrg/i+OHORdgyWe5LtOwC2xfuJPs3W61LLXHoSFtE8nQcRU0ZiyYzXkbKjq1+LNLf75dKdYWKN8R/7zqfesgvmtn/iX/FXd1m5AIAAAAASUVORK5CYII="></image>
<path d="M37,0 L37,37 L6.07153217e-16,37 L6.07153217e-16,21.9078947 C-0.100260417,17.3640351 2.48307292,12.5767544 7.75,7.54605263 C13.0169271,2.51535088 17.9335938,0 22.5,0 L37,0 Z" id="Path" fill="#000000"></path>
<path d="M155,1.42108547e-14 L155,96 L59.0072885,96 L59,27 C59.2044271,21.3216146 61.7555339,15.4020182 66.6533203,9.24121094 C71.5511068,3.08040365 78,4.73695157e-15 86,1.42108547e-14 L155,1.42108547e-14 Z" id="Path" fill="#000000" transform="translate(107.000000, 48.000000) scale(-1, 1) translate(-107.000000, -48.000000) "></path>
<path d="M37.0028093,118 L37.0028093,155 L0.00280933482,155 L0.00280933482,139.907895 C-0.0974510818,135.364035 2.48588225,130.576754 7.75280933,125.546053 C13.0197364,120.515351 17.9364031,118 22.5028093,118 L37.0028093,118 Z" id="Path" fill="#000000" transform="translate(18.501405, 136.500000) scale(1, -1) translate(-18.501405, -136.500000) "></path>
<path d="M155.002809,118 L155.002809,155 L118.002809,155 L118.002809,139.907895 C117.902549,135.364035 120.485882,130.576754 125.752809,125.546053 C131.019736,120.515351 135.936403,118 140.502809,118 L155.002809,118 Z" id="Path" fill="#000000" transform="translate(136.501405, 136.500000) scale(-1, -1) translate(-136.501405, -136.500000) "></path>
<polygon id="Path-2" fill="#000000" points="0 59 37 59 37 96 0 96"></polygon>
<polygon id="Path-2" fill="#000000" points="59 118 96 118 96 155 59 155"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="317px" height="331px" viewBox="0 0 317 331" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.1 (47250) - http://www.bohemiancoding.com/sketch -->
<title>metamask</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="metamask" fill-rule="nonzero">
<g id="Shape">
<polygon stroke="#BBBBBB" fill="#BBBBBB" points="300 229 280.763485 294 243 283.678788"></polygon>
<polygon stroke="#BBBBBB" fill="#BBBBBB" points="243 284 279.423237 233.834294 300 229"></polygon>
<path d="M272,179.716489 L300,229.151839 L279.643454,234 L272,179.716489 Z M272,179.716489 L290.016713,167 L300,229.151839 L272,179.716489 Z" fill="#DDDDDD"></path>
<path d="M249,142.560976 L301,121 L298.415663,132.810976 L249,142.560976 Z M296.771084,147 L249,142.560976 L298.415663,132.810976 L296.771084,147 Z" stroke="#000000" fill="#000000"></path>
<path d="M296.851563,146.862549 L290.689098,167 L249,142.498282 L296.851563,146.862549 Z M306.893674,125.979381 L298,132.776632 L300.620458,121 L306.893674,125.979381 Z M296,147.147766 L297.667564,133 L304.734859,138.848797 L296,147.147766 Z" stroke="#000000" fill="#000000"></path>
<path d="M290.501771,167 L296.750885,146.521729 L303,151.670968 L290.501771,167 Z M290.992432,167.10791 L235.792725,148.983398 L248.977783,141.554199 L290.992432,167.10791 Z" stroke="#000000" fill="#000000"></path>
<polygon stroke="#000000" fill="#000000" points="261.963379 80.3129883 249 142.452436 236 149"></polygon>
<polygon stroke="#000000" fill="#000000" points="301 121.351733 248.367188 143.222168 261.624512 80.5117188"></polygon>
<polygon stroke="#000000" fill="#000000" points="262 81.2691652 317 75 300.998047 121.375"></polygon>
<polygon fill="#DDDDDD" points="290.681641 166.88501 271.895501 180 235.297607 148.680664"></polygon>
<polygon stroke="#000000" fill="#000000" points="313.882812 31.1132812 317 75.1660156 261.748047 81.559082"></polygon>
<polygon stroke="#BBBBBB" fill="#BBBBBB" points="314 31 204.526123 111.583008 203 56.7398213"></polygon>
<polygon fill="#DDDDDD" points="122 50 203.574427 56.4313725 205.114014 110.804199"></polygon>
<polygon stroke="#000000" fill="#000000" points="236.105263 149 205 109.87239 262 81"></polygon>
<polygon stroke="#BBBBBB" fill="#BBBBBB" points="235.673406 149 272.702148 180.192871 220.705078 185.546875"></polygon>
<polygon stroke="#BBBBBB" fill="#BBBBBB" points="220.5 186.560059 205 110 236 148.993711"></polygon>
<polygon stroke="#000000" fill="#000000" points="261.821661 81.9235353 205 111 314 31"></polygon>
<polygon fill="#C0AD9E" points="122.280899 283.05679 148 310 113 278"></polygon>
<polygon stroke="#999999" fill="#999999" points="243 284 255.077922 237.475513 279.552002 234.016846"></polygon>
<polygon fill="#E2761B" points="18 153 63 106 23.8038869 147.391597"></polygon>
<path d="M279.638916,234.052734 L254.892334,237.791992 L272.25025,180.452211 L279.638916,234.052734 Z M204.874875,110.555789 L161.302302,109.130947 L122,50 L204.874875,110.555789 Z" fill="#DDDDDD"></path>
<polygon stroke="#AAAAAA" fill="#AAAAAA" points="272.860352 178.466309 255.035889 238.403076 253.430664 209.201538"></polygon>
<polygon stroke="#999999" fill="#999999" points="221 185.534946 272 180 254.07465 209"></polygon>
<polygon fill="#DDDDDD" points="161 109 205.040527 109.762451 221 186"></polygon>
<path d="M162.666504,110.812012 L43.8779297,0 L122.686523,49.7369804 L162.666504,110.812012 Z M122.605469,298.883789 L20.4082031,331 L0,252.227539 L122.605469,298.883789 Z" stroke="#BBBBBB" fill="#BBBBBB"></path>
<polygon stroke="#000000" fill="#000000" points="33 172 71.6265329 142 104 149.441253"></polygon>
<polygon stroke="#000000" fill="#000000" points="104 150 72 142.487487 89.1344743 71"></polygon>
<polygon stroke="#000000" fill="#000000" points="24 147.169713 72 142 33.4102142 172"></polygon>
<polygon stroke="#999999" fill="#999999" points="254 209 234.057554 198.873754 221 185"></polygon>
<polygon stroke="#000000" fill="#000000" points="24.0253906 147.559082 19.7363281 127.630127 72.6230469 142.035156"></polygon>
<polygon fill="#000000" points="229.504639 222.02832 233.890869 198.618896 254.018799 208.942871"></polygon>
<polygon fill="#DDDDDD" points="255 237 230 221.540845 253.676012 209"></polygon>
<path d="M72.0783081,142.372253 L20.1834513,129.755756 L16,114.651652 L72.0783081,142.372253 Z M89.4462891,70.630127 L72.0721436,142.393555 L16,114.651652 L89.4462891,70.630127 Z M89,71 L161,109.037037 L103.773544,150 L89,71 Z" stroke="#000000" fill="#000000"></path>
<path d="M102.939453,149.790527 L161.446777,108.118652 L187,187 L102.939453,149.790527 Z M187,187 L107.214355,184.98877 L103.306152,149.833008 L187,187 Z" stroke="#BBBBBB" fill="#BBBBBB"></path>
<path d="M32.0712891,171.77832 L103.916504,149.303223 L107.426532,185.340426 L32.0712891,171.77832 Z M221,185.81459 L186.509656,187 L161.016793,109 L221,185.81459 Z" fill="#DDDDDD"></path>
<polygon stroke="#999999" fill="#999999" points="234 198.617391 229.90303 221 221 185"></polygon>
<polygon stroke="#000000" fill="#000000" points="44 0 161 109 88.697998 71.2009277"></polygon>
<polygon stroke="#BBBBBB" fill="#BBBBBB" points="0 252.468262 98.902832 247.6875 122.921875 299.431641"></polygon>
<polygon stroke="#999999" fill="#999999" points="122.545455 299 99 248.383808 148 246"></polygon>
<path d="M230.672711,221.514124 L256.022229,237.016949 L265.330322,267.536621 L230.672711,221.514124 Z M108.110559,184.655367 L0,252.59887 L33.6414156,172 L108.110559,184.655367 Z M99.0289558,248.485876 L0,252.59887 L108.110559,184.655367 L99.0289558,248.485876 Z M221.749049,185.129944 L228.329102,209.351562 L196.20166,211.189697 L221.749049,185.129944 Z M196.142822,211.310303 L187.238959,186.316384 L221.749049,185.129944 L196.142822,211.310303 Z" stroke="#BBBBBB" fill="#BBBBBB"></path>
<polygon fill="#FBFBFB" points="148.453925 309.720812 122 299 214 315"></polygon>
<polygon stroke="#000000" fill="#000000" points="33.5405273 171.776367 18 152.599369 24.121582 146.518799"></polygon>
<polygon fill="#FFFFFF" points="225.334473 302.741699 214.44873 315.716797 122 299"></polygon>
<polygon stroke="#BBBBBB" fill="#BBBBBB" points="230.497559 275.182129 122 299 147.263158 246"></polygon>
<polygon fill="#FFFFFF" points="122 299.011173 224.425587 276.239335 230 275 225.026316 303"></polygon>
<path d="M16.5367115,114.898062 L13.1118164,53.6879883 L89.7861328,70.8154297 L16.5367115,114.898062 Z M24.1728516,147.156738 L13.6287487,137.309183 L20.7021717,130.023589 L24.1728516,147.156738 Z" stroke="#000000" fill="#000000"></path>
<polygon stroke="#999999" fill="#999999" points="169 202.545455 186.879395 185.943848 184.428571 225"></polygon>
<polygon stroke="#999999" fill="#999999" points="186 187 169.392822 203.094971 143 216"></polygon>
<path d="M264.222892,266.476346 L230,222 L264.222892,266.476346 Z" stroke="#BBBBBB" fill="#BBBBBB"></path>
<polygon stroke="#999999" fill="#999999" points="143.636727 216 108 185 187 186.677835"></polygon>
<polygon stroke="#AAAAAA" fill="#AAAAAA" points="184 225 186.960205 185.625732 196.302979 211.109863"></polygon>
<polygon stroke="#000000" fill="#000000" points="10 119.968586 16.6492537 114 21 129"></polygon>
<polygon fill="#000000" points="185.06543 225.883789 142.477783 216.035645 168.803089 202"></polygon>
<polygon stroke="#000000" fill="#000000" points="89 71 13 53.7854749 44.0444674 0"></polygon>
<polygon fill="#FBFBFB" points="214.661621 314.608398 219.323242 326.847656 148.149902 309.338379"></polygon>
<polygon fill="#DDDDDD" points="146.878378 247 143 216 184 225.323907"></polygon>
<polygon stroke="#AAAAAA" fill="#AAAAAA" points="108 185 144.087824 215.460746 147.286621 246.429199"></polygon>
<path d="M196.127808,211.066406 L228.076046,209.108317 L265,267 L196.127808,211.066406 Z M108.073194,185 L147.60076,246.618956 L98.7451172,248.597656 L108.073194,185 Z" fill="#DDDDDD"></path>
<polygon stroke="#AAAAAA" fill="#AAAAAA" points="196.217929 211 235 270 183.407227 225.09375"></polygon>
<polygon stroke="#BBBBBB" fill="#BBBBBB" points="183.709229 224.863281 235.486816 270.005859 228.958496 275.549316"></polygon>
<path d="M229.403226,275 L147,246.555556 L183.877016,225.064198 L229.403226,275 Z M265.065247,267.038452 L235.010742,270.378784 L195.155273,209.339355 L265.065247,267.038452 Z" fill="#DDDDDD"></path>
<path d="M272.00824,294.410675 L259.356948,320.362963 L218.346049,327 L272.00824,294.410675 Z M219.55719,327.282715 L214,315.148148 L225.11438,302.943848 L219.55719,327.282715 Z" fill="#FBFBFB"></path>
<path d="M225.051514,302.968872 L232.809204,299.898437 L219,327 L225.051514,302.968872 Z M219,327 L232.710693,299.850464 L272.031677,294.415588 L219,327 Z" fill="#FBFBFB"></path>
<polygon fill="#000000" points="265.13369 267 274.98291 272.750488 245 276"></polygon>
<path d="M244.534709,276.545455 L235,270.340909 L265.137085,267.049438 L244.534709,276.545455 Z M241.146341,281.238636 L277,276.863636 L272.036133,294.474121 L241.146341,281.238636 Z" fill="#000000"></path>
<path d="M272.066265,294.415274 L232.674699,299.928401 L241.36747,280.789976 L272.066265,294.415274 Z M232.858643,299.856323 L225,303 L229.64389,274.996735 L232.858643,299.856323 Z M229.646622,274.991623 L235.259036,270 L244.73494,276.143198 L229.646622,274.991623 Z M274.82019,272.612183 L277.009399,276.895203 L240.491821,281.35498 L274.82019,272.612183 Z" fill="#000000"></path>
<path d="M241.280029,281.16687 L244.26709,275.812866 L274.876221,272.649902 L241.280029,281.16687 Z M229.641754,274.986938 L241.410302,281.092219 L232.735346,300 L229.641754,274.986938 Z" fill="#000000"></path>
<polygon fill="#000000" points="244.553223 276.001831 241.346558 281.346558 229.65329 274.99649"></polygon>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="260px" height="260px" viewBox="0 0 260 260" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.1 (47250) - http://www.bohemiancoding.com/sketch -->
<title>mist</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="mist">
<circle id="Oval-3" fill="#000000" cx="130" cy="130" r="130"></circle>
<g id="ether" transform="translate(66.000000, 26.000000)">
<polygon id="Path-4" fill="#DDDDDD" points="0 106 63.9893369 0 64 77.1016184"></polygon>
<polygon id="Path-5" fill="#AAAAAA" points="128 106 64.0106631 0 64 77.1016184"></polygon>
<polygon id="Path-6" fill="#AAAAAA" points="64 77 0 105.714286 64 144"></polygon>
<polygon id="Path-6" fill="#999999" points="64 77 128 105.714286 64 144"></polygon>
<polygon id="Path-7" fill="#888888" points="128 117 64.0106631 208 64 155.541176"></polygon>
<polygon id="Path-7" fill="#DDDDDD" points="0 117 63.9893369 208 64 155.541176"></polygon>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="156px" height="258px" viewBox="0 0 156 258" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.1 (47250) - http://www.bohemiancoding.com/sketch -->
<title>trezor</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="trezor" fill="#000000" fill-rule="nonzero">
<g id="path7">
<path d="M78.1255028,0 C47.0321802,0 21.8688656,28.6428571 21.8688656,64.0357143 L21.8688656,88.0357143 C10.9501207,90.2857143 0,93.2857143 0,97.1785714 L0,222.428571 C0,222.428571 0,225.892857 3.41995173,227.535714 C15.8133548,233.25 64.5711987,252.892857 75.772325,257.392857 C77.2156074,258 77.6234916,258 78,258 C78.533387,258 78.7843926,258 80.227675,257.392857 C91.4288013,252.892857 140.312148,233.25 152.705551,227.535714 C155.874497,226.035714 156,222.571429 156,222.571429 L156,97.1785714 C156,93.2857143 145.206758,90.1428571 134.256637,88.0357143 L134.256637,64.0357143 C134.413516,28.6428571 109.093323,0 78.1255028,0 Z M78.1255028,30.6071429 C96.4489139,30.6071429 107.524537,43.2142857 107.524537,64.0714286 L107.524537,84.9285714 C86.9734513,83.2857143 69.4344328,83.2857143 48.7578439,84.9285714 L48.7578439,64.0714286 C48.7578439,43.1785714 59.8334674,30.6071429 78.1255028,30.6071429 Z M78,115.642857 C103.571199,115.642857 125.03218,117.892857 125.03218,121.928571 L125.03218,200.071429 C125.03218,201.285714 124.906677,201.428571 123.965406,201.857143 C123.055511,202.321429 80.3531778,219.857143 80.3531778,219.857143 C80.3531778,219.857143 78.6275141,220.464286 78.1255028,220.464286 C77.5921158,220.464286 75.8978278,219.714286 75.8978278,219.714286 C75.8978278,219.714286 33.1954948,202.178571 32.2855994,201.714286 C31.3757039,201.25 31.2188254,201.107143 31.2188254,199.928571 L31.2188254,121.785714 C30.9678198,117.75 52.4288013,115.642857 78,115.642857 Z"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,30 @@
import React from 'react';
import { AddressFieldFactory } from './AddressFieldFactory';
import { donationAddressMap } from 'config/data';
import { Aux } from 'components/ui';
interface Props {
isReadOnly?: boolean;
}
export const AddressField: React.SFC<Props> = ({ isReadOnly }) => (
<AddressFieldFactory
withProps={({ currentTo, isValid, onChange, readOnly, errorMsg }) => (
<Aux>
<input
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
type="text"
value={currentTo.raw}
placeholder={donationAddressMap.ETH}
readOnly={!!(isReadOnly || readOnly)}
onChange={onChange}
/>
{errorMsg && (
<div className="has-error">
<span className="help-block">{errorMsg}</span>
</div>
)}
</Aux>
)}
/>
);

View File

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

View File

@ -1,8 +1,9 @@
import { Query } from 'components/renderCbs';
import { setCurrentTo, TSetCurrentTo } from 'actions/transaction';
import { AddressInput } from './AddressInput';
import { AddressInputFactory } from './AddressInputFactory';
import React from 'react';
import { connect } from 'react-redux';
import { ICurrentTo } from 'selectors/transaction';
interface DispatchProps {
setCurrentTo: TSetCurrentTo;
@ -10,12 +11,21 @@ interface DispatchProps {
interface OwnProps {
to: string | null;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
export interface CallbackProps {
isValid: boolean;
readOnly: boolean;
currentTo: ICurrentTo;
errorMsg?: string | null;
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
type Props = DispatchProps & DispatchProps & OwnProps;
//TODO: add ens resolving
class AddressFieldClass extends React.Component<Props, {}> {
class AddressFieldFactoryClass extends React.Component<Props, {}> {
public componentDidMount() {
// this 'to' parameter can be either token or actual field related
const { to } = this.props;
@ -25,7 +35,7 @@ class AddressFieldClass extends React.Component<Props, {}> {
}
public render() {
return <AddressInput onChange={this.setAddress} />;
return <AddressInputFactory onChange={this.setAddress} withProps={this.props.withProps} />;
}
private setAddress = (ev: React.FormEvent<HTMLInputElement>) => {
@ -34,10 +44,14 @@ class AddressFieldClass extends React.Component<Props, {}> {
};
}
const AddressField = connect(null, { setCurrentTo })(AddressFieldClass);
const AddressField = connect(null, { setCurrentTo })(AddressFieldFactoryClass);
const DefaultAddressField: React.SFC<{}> = () => (
<Query params={['to']} withQuery={({ to }) => <AddressField to={to} />} />
interface DefaultAddressFieldProps {
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({ withProps }) => (
<Query params={['to']} withQuery={({ to }) => <AddressField to={to} withProps={withProps} />} />
);
export { DefaultAddressField as AddressField };
export { DefaultAddressField as AddressFieldFactory };

View File

@ -3,10 +3,10 @@ import { Identicon } from 'components/ui';
import translate from 'translations';
//import { EnsAddress } from './components';
import { Query } from 'components/renderCbs';
import { donationAddressMap } from 'config/data';
import { ICurrentTo, getCurrentTo, isValidCurrentTo } from 'selectors/transaction';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { CallbackProps } from 'components/AddressFieldFactory';
interface StateProps {
currentTo: ICurrentTo;
@ -14,14 +14,15 @@ interface StateProps {
}
interface OwnProps {
onChange(ev: React.FormEvent<HTMLInputElement>): void;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
type Props = OwnProps & StateProps;
//TODO: ENS handling
class AddressInputClass extends Component<Props> {
class AddressInputFactoryClass extends Component<Props> {
public render() {
const { currentTo, onChange, isValid } = this.props;
const { currentTo, onChange, isValid, withProps } = this.props;
const { raw } = currentTo;
return (
<div className="row form-group">
@ -29,16 +30,15 @@ class AddressInputClass extends Component<Props> {
<label>{translate('SEND_addr')}:</label>
<Query
params={['readOnly']}
withQuery={({ readOnly }) => (
<input
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
type="text"
value={raw}
placeholder={donationAddressMap.ETH}
readOnly={!!readOnly}
onChange={onChange}
/>
)}
withQuery={({ readOnly }) =>
withProps({
currentTo,
isValid,
onChange,
readOnly: !!readOnly,
errorMsg: currentTo.error
})
}
/>
{/*<EnsAddress ensAddress={ensAddress} />*/}
</div>
@ -50,7 +50,7 @@ class AddressInputClass extends Component<Props> {
}
}
export const AddressInput = connect((state: AppState) => ({
export const AddressInputFactory = connect((state: AppState) => ({
currentTo: getCurrentTo(state),
isValid: isValidCurrentTo(state)
}))(AddressInputClass);
}))(AddressInputFactoryClass);

View File

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

View File

@ -6,26 +6,36 @@ import translate, { translateRaw } from 'translations';
interface Props {
hasUnitDropdown?: boolean;
showAllTokens?: boolean;
customValidator?(rawAmount: string): boolean;
}
export const AmountField: React.SFC<Props> = ({ hasUnitDropdown }) => (
export const AmountField: React.SFC<Props> = ({
hasUnitDropdown,
showAllTokens,
customValidator
}) => (
<AmountFieldFactory
withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => (
<Aux>
<label>{translate('SEND_amount')}</label>
<div className="input-group">
<input
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
className={`form-control ${
isAmountValid(raw, customValidator, isValid) ? 'is-valid' : 'is-invalid'
}`}
type="number"
placeholder={translateRaw('SEND_amount_short')}
value={raw}
readOnly={!!readOnly}
onChange={onChange}
/>
{hasUnitDropdown && <UnitDropDown />}
{hasUnitDropdown && <UnitDropDown showAllTokens={showAllTokens} />}
</div>
</Aux>
)}
/>
);
const isAmountValid = (raw, customValidator, isValid) =>
customValidator ? customValidator(raw) : isValid;

View File

@ -11,7 +11,7 @@ export interface CallbackProps {
currentValue:
| AppState['transaction']['fields']['value']
| AppState['transaction']['meta']['tokenValue'];
onChange(ev: React.FormEvent<HTMLInputElement>);
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
interface DispatchProps {

View File

@ -6,7 +6,7 @@ import { connect } from 'react-redux';
import { CallbackProps } from 'components/AmountFieldFactory';
interface OwnProps {
onChange(ev: React.FormEvent<HTMLInputElement>);
onChange(ev: React.FormEvent<HTMLInputElement>): void;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}

View File

@ -32,7 +32,7 @@ export default class EquivalentValues extends React.Component<Props, CmpState> {
private decimalLookup: { [key: string]: number } = {};
private requestedCurrencies: string[] | null = null;
public constructor(props) {
public constructor(props: Props) {
super(props);
this.makeBalanceLookup(props);
@ -41,7 +41,7 @@ export default class EquivalentValues extends React.Component<Props, CmpState> {
}
}
public componentWillReceiveProps(nextProps) {
public componentWillReceiveProps(nextProps: Props) {
const { balance, tokenBalances } = this.props;
if (nextProps.balance !== balance || nextProps.tokenBalances !== tokenBalances) {
this.makeBalanceLookup(nextProps);

View File

@ -1,52 +0,0 @@
import React from 'react';
import { forceOfflineConfig as dForceOfflineConfig, TForceOfflineConfig } from 'actions/config';
import OfflineSymbol from 'components/ui/OfflineSymbol';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
type sizeType = 'small' | 'medium' | 'large';
interface OfflineToggleProps {
offline: boolean;
forceOffline: boolean;
forceOfflineConfig: TForceOfflineConfig;
size?: sizeType;
}
class OfflineToggle extends React.Component<OfflineToggleProps, {}> {
public render() {
const { forceOfflineConfig, offline, forceOffline, size } = this.props;
return (
<div>
{!offline ? (
<div className="row text-center">
<div className="col-xs-3">
<OfflineSymbol offline={offline || forceOffline} size={size} />
</div>
<div className="col-xs-6">
<button className="btn-xs btn-info" onClick={forceOfflineConfig}>
{forceOffline ? 'Go Online' : 'Go Offline'}
</button>
</div>
</div>
) : (
<div className="text-center">
<h5>You are currently offline.</h5>
</div>
)}
</div>
);
}
}
function mapStateToProps(state: AppState) {
return {
offline: state.config.offline,
forceOffline: state.config.forceOffline
};
}
export default connect(mapStateToProps, {
forceOfflineConfig: dForceOfflineConfig
})(OfflineToggle);

View File

@ -12,15 +12,19 @@ interface Props {
toggleForm(): void;
}
interface IGenerateSymbolLookup {
[tokenSymbol: string]: boolean;
}
interface State {
tokenSymbolLookup: { [symbol: string]: boolean };
tokenSymbolLookup: IGenerateSymbolLookup;
address: string;
symbol: string;
decimal: string;
}
export default class AddCustomTokenForm extends React.Component<Props, State> {
public state = {
public state: State = {
tokenSymbolLookup: {},
address: '',
symbol: '',
@ -130,14 +134,14 @@ export default class AddCustomTokenForm extends React.Component<Props, State> {
return !Object.keys(this.getErrors()).length && address && symbol && decimal;
}
public onFieldChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
public onFieldChange = (e: React.FormEvent<HTMLInputElement>) => {
// TODO: typescript bug: https://github.com/Microsoft/TypeScript/issues/13948
const name: any = (e.target as HTMLInputElement).name;
const value = (e.target as HTMLInputElement).value;
const name: any = e.currentTarget.name;
const value = e.currentTarget.value;
this.setState({ [name]: value });
};
public onSave = (ev: React.SyntheticEvent<HTMLFormElement>) => {
public onSave = (ev: React.FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (!this.isValid()) {
return;
@ -148,9 +152,12 @@ export default class AddCustomTokenForm extends React.Component<Props, State> {
};
private generateSymbolLookup(tokens: Token[]) {
return tokens.reduce((prev, tk) => {
prev[tk.symbol] = true;
return prev;
}, {});
return tokens.reduce(
(prev, tk) => {
prev[tk.symbol] = true;
return prev;
},
{} as IGenerateSymbolLookup
);
}
}

View File

@ -40,7 +40,9 @@ export default class TokenBalances extends React.Component<Props, State> {
const { showCustomTokenForm, trackedTokens } = this.state;
let bottom;
if (!hasSavedWalletTokens) {
let help;
if (tokenBalances.length && !hasSavedWalletTokens) {
help = 'Select which tokens you would like to keep track of';
bottom = (
<div className="TokenBalances-buttons">
<button className="btn btn-primary btn-block" onClick={this.handleSetWalletTokens}>
@ -76,28 +78,31 @@ export default class TokenBalances extends React.Component<Props, State> {
return (
<div>
{!hasSavedWalletTokens && (
<p className="TokenBalances-help">Select which tokens you would like to keep track of</p>
{help && <p className="TokenBalances-help">{help}</p>}
{tokenBalances.length ? (
<table className="TokenBalances-rows">
<tbody>
{tokenBalances.map(
token =>
token ? (
<TokenRow
key={token.symbol}
balance={token.balance}
symbol={token.symbol}
custom={token.custom}
decimal={token.decimal}
tracked={trackedTokens[token.symbol]}
toggleTracked={!hasSavedWalletTokens && this.toggleTrack}
onRemove={this.props.onRemoveCustomToken}
/>
) : null
)}
</tbody>
</table>
) : (
<div className="well well-sm text-center">No tokens found</div>
)}
<table className="TokenBalances-rows">
<tbody>
{tokenBalances.map(
token =>
token ? (
<TokenRow
key={token.symbol}
balance={token.balance}
symbol={token.symbol}
custom={token.custom}
decimal={token.decimal}
tracked={trackedTokens[token.symbol]}
toggleTracked={!hasSavedWalletTokens && this.toggleTrack}
onRemove={this.props.onRemoveCustomToken}
/>
) : null
)}
</tbody>
</table>
{bottom}
</div>
);

View File

@ -54,7 +54,7 @@ export default class TokenRow extends React.Component<Props, State> {
{!!custom && (
<img
src={removeIcon}
className="TokenRow-balance-remove"
className="TokenRow-symbol-remove"
title="Remove Token"
onClick={this.onRemove}
tabIndex={0}
@ -65,7 +65,7 @@ export default class TokenRow extends React.Component<Props, State> {
);
}
public toggleShowLongBalance = (e: React.SyntheticEvent<HTMLTableDataCellElement>) => {
public toggleShowLongBalance = (e: React.FormEvent<HTMLTableDataCellElement>) => {
e.preventDefault();
this.setState(state => {
return {

View File

@ -14,6 +14,11 @@
margin-top: 10px;
}
&-none {
text-align: center;
margin-bottom: -5px;
}
&-loader {
padding: 25px 0;
text-align: center;

View File

@ -38,7 +38,7 @@ interface ActionProps {
}
type Props = StateProps & ActionProps;
class TokenBalances extends React.Component<Props, {}> {
class TokenBalances extends React.Component<Props> {
public render() {
const {
tokens,
@ -96,6 +96,7 @@ class TokenBalances extends React.Component<Props, {}> {
private scanWalletForTokens = () => {
if (this.props.wallet) {
this.props.scanWalletForTokens(this.props.wallet);
this.setState({ hasScanned: true });
}
};
}

View File

@ -10,7 +10,6 @@ import AccountInfo from './AccountInfo';
import EquivalentValues from './EquivalentValues';
import Promos from './Promos';
import TokenBalances from './TokenBalances';
import OfflineToggle from './OfflineToggle';
interface Props {
wallet: IWallet;
@ -37,10 +36,6 @@ export class BalanceSidebar extends React.Component<Props, {}> {
}
const blocks: Block[] = [
{
name: 'Go Offline',
content: <OfflineToggle />
},
{
name: 'Account Info',
content: <AccountInfo wallet={wallet} balance={balance} network={network} />

View File

@ -9,10 +9,10 @@ export interface CallBackProps {
data: AppState['transaction']['fields']['data'];
dataExists: boolean;
readOnly: boolean;
onChange(ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>);
onChange(ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>): void;
}
interface DispatchProps {
isEtherTransaction;
isEtherTransaction: boolean;
inputData: TInputData;
}
interface OwnProps {

View File

@ -7,7 +7,7 @@ import { CallBackProps } from 'components/DataFieldFactory';
interface OwnProps {
withProps(props: CallBackProps): React.ReactElement<any> | null;
onChange(ev: React.FormEvent<HTMLInputElement>);
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
interface StateProps {
data: AppState['transaction']['fields']['data'];

View File

@ -15,7 +15,7 @@ import PreFooter from './PreFooter';
import Modal, { IButton } from 'components/ui/Modal';
import { NewTabLink } from 'components/ui';
const AffiliateTag = ({ link, text }) => {
const AffiliateTag = ({ link, text }: Link) => {
return (
<li className="Footer-affiliate-tag" key={link}>
<NewTabLink href={link}>{text}</NewTabLink>
@ -23,7 +23,7 @@ const AffiliateTag = ({ link, text }) => {
);
};
const SocialMediaLink = ({ link, text }) => {
const SocialMediaLink = ({ link, text }: Link) => {
return (
<NewTabLink className="Footer-social-media-link" key={link} href={link}>
<i className={`sm-icon sm-logo-${text} sm-24px`} />
@ -108,7 +108,7 @@ interface State {
}
export default class Footer extends React.Component<Props, State> {
constructor(props) {
constructor(props: Props) {
super(props);
this.state = { isOpen: false };
}

View File

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

View File

@ -1,12 +1,12 @@
import React from 'react';
import { GasFieldFactory } from './GasFieldFactory';
import { GasLimitFieldFactory } from './GasLimitFieldFactory';
import translate from 'translations';
import { Aux } from 'components/ui';
export const GasField: React.SFC<{}> = () => (
export const GasLimitField: React.SFC<{}> = () => (
<Aux>
<label>{translate('TRANS_gas')} </label>
<GasFieldFactory
<GasLimitFieldFactory
withProps={({ gasLimit: { raw, value }, onChange, readOnly }) => (
<input
className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`}

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { GasQuery } from 'components/renderCbs';
import { GasInput } from './GasInputFactory';
import { GasLimitInput } from './GasLimitInputFactory';
import { inputGasLimit, TInputGasLimit } from 'actions/transaction';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
@ -18,7 +18,7 @@ interface DispatchProps {
}
interface OwnProps {
gasLimit: string | null;
withProps(props: CallBackProps);
withProps(props: CallBackProps): React.ReactElement<any> | null;
}
type Props = DispatchProps & OwnProps;
@ -34,7 +34,7 @@ class GasLimitFieldClass extends Component<Props, {}> {
}
public render() {
return <GasInput onChange={this.setGas} withProps={this.props.withProps} />;
return <GasLimitInput onChange={this.setGas} withProps={this.props.withProps} />;
}
private setGas = (ev: React.FormEvent<HTMLInputElement>) => {
@ -45,13 +45,13 @@ class GasLimitFieldClass extends Component<Props, {}> {
const GasLimitField = connect(null, { inputGasLimit })(GasLimitFieldClass);
interface DefaultGasFieldProps {
withProps(props: CallBackProps);
interface DefaultGasLimitFieldProps {
withProps(props: CallBackProps): React.ReactElement<any> | null;
}
const DefaultGasField: React.SFC<DefaultGasFieldProps> = ({ withProps }) => (
const DefaultGasLimitField: React.SFC<DefaultGasLimitFieldProps> = ({ withProps }) => (
<GasQuery
withQuery={({ gasLimit }) => <GasLimitField gasLimit={gasLimit} withProps={withProps} />}
/>
);
export { DefaultGasField as GasFieldFactory };
export { DefaultGasLimitField as GasLimitFieldFactory };

View File

@ -3,7 +3,7 @@ import { Query } from 'components/renderCbs';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getGasLimit } from 'selectors/transaction';
import { CallBackProps } from 'components/GasFieldFactory';
import { CallBackProps } from 'components/GasLimitFieldFactory';
interface StateProps {
gasLimit: AppState['transaction']['fields']['gasLimit'];
@ -15,7 +15,7 @@ interface OwnProps {
}
type Props = StateProps & OwnProps;
class GasInputClass extends Component<Props> {
class GasLimitInputClass extends Component<Props> {
public render() {
const { gasLimit, onChange } = this.props;
return (
@ -29,6 +29,6 @@ class GasInputClass extends Component<Props> {
}
}
export const GasInput = connect((state: AppState) => ({ gasLimit: getGasLimit(state) }))(
GasInputClass
export const GasLimitInput = connect((state: AppState) => ({ gasLimit: getGasLimit(state) }))(
GasLimitInputClass
);

View File

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

View File

@ -0,0 +1,10 @@
@import 'common/sass/variables';
.GasSlider {
&-toggle {
display: inline-block;
position: relative;
margin-top: $space-sm;
left: -8px;
}
}

View File

@ -0,0 +1,99 @@
import React from 'react';
import { translateRaw } from 'translations';
import { connect } from 'react-redux';
import {
inputGasPrice,
TInputGasPrice,
inputGasLimit,
TInputGasLimit,
inputNonce,
TInputNonce
} from 'actions/transaction';
import { fetchCCRates, TFetchCCRates } from 'actions/rates';
import { getNetworkConfig } from 'selectors/config';
import { AppState } from 'reducers';
import SimpleGas from './components/SimpleGas';
import AdvancedGas from './components/AdvancedGas';
import './GasSlider.scss';
interface Props {
// Component configuration
disableAdvanced?: boolean;
// Data
gasPrice: AppState['transaction']['fields']['gasPrice'];
gasLimit: AppState['transaction']['fields']['gasLimit'];
offline: AppState['config']['offline'];
network: AppState['config']['network'];
// Actions
inputGasPrice: TInputGasPrice;
inputGasLimit: TInputGasLimit;
inputNonce: TInputNonce;
fetchCCRates: TFetchCCRates;
}
interface State {
showAdvanced: boolean;
}
class GasSlider extends React.Component<Props, State> {
public state: State = {
showAdvanced: false
};
public componentDidMount() {
this.props.fetchCCRates([this.props.network.unit]);
}
public render() {
const { gasPrice, gasLimit, offline, disableAdvanced } = this.props;
const showAdvanced = (this.state.showAdvanced || offline) && !disableAdvanced;
return (
<div className="GasSlider">
{showAdvanced ? (
<AdvancedGas
gasPrice={gasPrice.raw}
gasLimit={gasLimit.raw}
changeGasPrice={this.props.inputGasPrice}
changeGasLimit={this.props.inputGasLimit}
/>
) : (
<SimpleGas gasPrice={gasPrice.raw} changeGasPrice={this.props.inputGasPrice} />
)}
{!offline &&
!disableAdvanced && (
<div className="help-block">
<a className="GasSlider-toggle" onClick={this.toggleAdvanced}>
<strong>
{showAdvanced
? `- ${translateRaw('Back to simple')}`
: `+ ${translateRaw('Advanced: Data, Gas Price, Gas Limit')}`}
</strong>
</a>
</div>
)}
</div>
);
}
private toggleAdvanced = () => {
this.setState({ showAdvanced: !this.state.showAdvanced });
};
}
function mapStateToProps(state: AppState) {
return {
gasPrice: state.transaction.fields.gasPrice,
gasLimit: state.transaction.fields.gasLimit,
offline: state.config.offline,
network: getNetworkConfig(state)
};
}
export default connect(mapStateToProps, {
inputGasPrice,
inputGasLimit,
inputNonce,
fetchCCRates
})(GasSlider);

View File

@ -0,0 +1,4 @@
.AdvancedGas {
margin-top: 0;
margin-bottom: 0;
}

View File

@ -0,0 +1,72 @@
import React from 'react';
import translate from 'translations';
import { DataFieldFactory } from 'components/DataFieldFactory';
import FeeSummary from './FeeSummary';
import './AdvancedGas.scss';
interface Props {
gasPrice: string;
gasLimit: string;
changeGasPrice(gwei: string): void;
changeGasLimit(wei: string): void;
}
export default class AdvancedGas extends React.Component<Props> {
public render() {
return (
<div className="AdvancedGas row form-group">
<div className="col-md-3 col-sm-6 col-xs-12">
<label>{translate('OFFLINE_Step2_Label_3')} (gwei)</label>
<input
className="form-control"
type="number"
value={this.props.gasPrice}
onChange={this.handleGasPriceChange}
/>
</div>
<div className="col-md-3 col-sm-6 col-xs-12">
<label>{translate('OFFLINE_Step2_Label_4')}</label>
<input
className="form-control"
type="number"
value={this.props.gasLimit}
onChange={this.handleGasLimitChange}
/>
</div>
<div className="col-md-6 col-sm-12">
<label>{translate('OFFLINE_Step2_Label_6')}</label>
<DataFieldFactory
withProps={({ data, onChange }) => (
<input
className="form-control"
value={data.raw}
onChange={onChange}
placeholder="0x7cB57B5A..."
/>
)}
/>
</div>
<div className="col-sm-12">
<FeeSummary
render={({ gasPriceWei, gasLimit, fee, usd }) => (
<span>
{gasPriceWei} * {gasLimit} = {fee} {usd && <span>~= ${usd} USD</span>}
</span>
)}
/>
</div>
</div>
);
}
private handleGasPriceChange = (ev: React.FormEvent<HTMLInputElement>) => {
this.props.changeGasPrice(ev.currentTarget.value);
};
private handleGasLimitChange = (ev: React.FormEvent<HTMLInputElement>) => {
this.props.changeGasLimit(ev.currentTarget.value);
};
}

View File

@ -0,0 +1,11 @@
@import 'common/sass/variables';
.FeeSummary {
background: $gray-lighter;
height: 42px;
line-height: 42px;
padding: 0 12px;
font-family: $font-family-monospace;
text-align: center;
font-size: 14px;
}

View File

@ -0,0 +1,78 @@
import React from 'react';
import BN from 'bn.js';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getNetworkConfig } from 'selectors/config';
import { UnitDisplay } from 'components/ui';
import './FeeSummary.scss';
interface RenderData {
gasPriceWei: string;
gasPriceGwei: string;
gasLimit: string;
fee: React.ReactElement<string>;
usd: React.ReactElement<string> | null;
}
interface Props {
// Redux props
gasPrice: AppState['transaction']['fields']['gasPrice'];
gasLimit: AppState['transaction']['fields']['gasLimit'];
rates: AppState['rates']['rates'];
network: AppState['config']['network'];
// Component props
render(data: RenderData): React.ReactElement<string> | string;
}
class FeeSummary extends React.Component<Props> {
public render() {
const { gasPrice, gasLimit, rates, network } = this.props;
const feeBig = gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value);
const fee = (
<UnitDisplay
value={feeBig}
unit="ether"
symbol={network.unit}
displayShortBalance={6}
checkOffline={false}
/>
);
const usdBig = network.isTestnet
? new BN(0)
: feeBig && rates[network.unit] && feeBig.muln(rates[network.unit].USD);
const usd = (
<UnitDisplay
value={usdBig}
unit="ether"
displayShortBalance={2}
displayTrailingZeroes={true}
checkOffline={true}
/>
);
return (
<div className="FeeSummary">
{this.props.render({
gasPriceWei: gasPrice.value.toString(),
gasPriceGwei: gasPrice.raw,
fee,
usd,
gasLimit: gasLimit.raw
})}
</div>
);
}
}
function mapStateToProps(state: AppState) {
return {
gasPrice: state.transaction.fields.gasPrice,
gasLimit: state.transaction.fields.gasLimit,
rates: state.rates.rates,
network: getNetworkConfig(state)
};
}
export default connect(mapStateToProps)(FeeSummary);

View File

@ -0,0 +1,36 @@
@import 'common/sass/variables';
.SimpleGas {
margin-top: 0;
margin-bottom: 0;
&-label {
display: block;
}
&-slider {
padding-top: 8px;
margin-bottom: $space-xs;
&-labels {
margin-top: 4px;
display: flex;
> span {
flex: 1;
padding: 0 $space-xs;
text-align: center;
color: $gray-light;
font-size: $font-size-xs;
&:first-child {
text-align: left;
}
&:last-child {
text-align: right;
}
}
}
}
}

View File

@ -0,0 +1,54 @@
import React from 'react';
import Slider from 'rc-slider';
import translate from 'translations';
import { gasPriceDefaults } from 'config/data';
import FeeSummary from './FeeSummary';
import './SimpleGas.scss';
interface Props {
gasPrice: string;
changeGasPrice(gwei: string): void;
}
export default class SimpleGas extends React.Component<Props> {
public render() {
const { gasPrice } = this.props;
return (
<div className="SimpleGas row form-group">
<div className="col-md-12">
<label className="SimpleGas-label">{translate('Transaction Fee')}</label>
</div>
<div className="col-md-8 col-sm-12">
<div className="SimpleGas-slider">
<Slider
onChange={this.handleSlider}
min={gasPriceDefaults.gasPriceMinGwei}
max={gasPriceDefaults.gasPriceMaxGwei}
value={parseFloat(gasPrice)}
/>
<div className="SimpleGas-slider-labels">
<span>{translate('Cheap')}</span>
<span>{translate('Balanced')}</span>
<span>{translate('Fast')}</span>
</div>
</div>
</div>
<div className="col-md-4 col-sm-12">
<FeeSummary
render={({ fee, usd }) => (
<span>
{fee} {usd && <span>/ ${usd}</span>}
</span>
)}
/>
</div>
</div>
);
}
private handleSlider = (gasGwei: number) => {
this.props.changeGasPrice(gasGwei.toString());
};
}

View File

@ -0,0 +1,2 @@
import GasSlider from './GasSlider';
export default GasSlider;

View File

@ -233,7 +233,7 @@ export default class CustomNodeModal extends React.Component<Props, State> {
'form-control': true,
'is-invalid': this.state[input.name] && invalids[input.name]
})}
value={this.state[name]}
value={this.state[input.name]}
onChange={this.handleChange}
{...input}
/>
@ -252,7 +252,7 @@ export default class CustomNodeModal extends React.Component<Props, State> {
customNetworkUnit,
customNetworkChainId
} = this.state;
const required = ['name', 'url', 'port', 'network'];
const required: (keyof State)[] = ['name', 'url', 'port', 'network'];
const invalids: { [key: string]: boolean } = {};
// Required fields
@ -344,7 +344,7 @@ export default class CustomNodeModal extends React.Component<Props, State> {
private handleCheckbox = (ev: React.FormEvent<HTMLInputElement>) => {
const { name } = ev.currentTarget;
this.setState({ [name as any]: !this.state[name] });
this.setState({ [name as any]: !this.state[name as keyof State] });
};
private saveAndAdd = () => {

View File

@ -1,13 +1,18 @@
import React, { Component } from 'react';
import NavigationLink from './NavigationLink';
import { knowledgeBaseURL } from 'config/data';
import './Navigation.scss';
const tabs = [
export interface TabLink {
name: string;
to: string;
external?: boolean;
}
const tabs: TabLink[] = [
{
name: 'NAV_GenerateWallet',
to: '/'
to: '/generate'
},
{
@ -90,7 +95,7 @@ export default class Navigation extends Component<Props, State> {
<div className="Navigation-scroll container">
<ul className="Navigation-links">
{tabs.map(link => {
return <NavigationLink key={link.name} link={link} />;
return <NavigationLink key={link.name} link={link} isHomepage={link === tabs[0]} />;
})}
</ul>
</div>

View File

@ -44,3 +44,17 @@
}
}
}
#NAV_Swap a:before {
content:"";
display: inline-block;
margin-top: -.1rem;
width: 1.3rem;
height: 1.3rem;
background-image: url('~assets/images/swap.svg');
background-position: center;
background-repeat: no-repeat;
background-size: contain;
vertical-align: middle;
margin-right: 4px;
}

View File

@ -1,32 +1,35 @@
import classnames from 'classnames';
import React from 'react';
import { Link, withRouter } from 'react-router-dom';
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
import translate, { translateRaw } from 'translations';
import { TabLink } from './Navigation';
import './NavigationLink.scss';
interface Props {
link: {
name: string;
to?: string;
external?: boolean;
};
}
interface InjectedLocation extends Props {
location: { pathname: string };
interface Props extends RouteComponentProps<{}> {
link: TabLink;
isHomepage: boolean;
}
class NavigationLink extends React.Component<Props, {}> {
get injected() {
return this.props as InjectedLocation;
}
public render() {
const { link } = this.props;
const { location } = this.injected;
const { link, location, isHomepage } = this.props;
const isExternalLink = link.to.includes('http');
let isActive = false;
if (!isExternalLink) {
// isActive if
// 1) Current path is the same as link
// 2) the first path is the same for both links (/account and /account/send)
// 3) we're at the root path and this is the "homepage" nav item
const isSubRoute = location.pathname.split('/')[1] === link.to.split('/')[1];
isActive =
location.pathname === link.to || isSubRoute || (isHomepage && location.pathname === '/');
}
const linkClasses = classnames({
'NavigationLink-link': true,
'is-disabled': !link.to,
'is-active': location.pathname === link.to
'is-active': isActive
});
const linkLabel = `nav item: ${translateRaw(link.name)}`;
@ -41,9 +44,13 @@ class NavigationLink extends React.Component<Props, {}> {
</Link>
);
return <li className="NavigationLink">{linkEl}</li>;
return (
<li id={link.name} className="NavigationLink">
{linkEl}
</li>
);
}
}
// withRouter is a HOC which provides NavigationLink with a react-router location prop
export default withRouter(NavigationLink);
export default withRouter<Props>(NavigationLink);

View File

@ -0,0 +1,87 @@
import React from 'react';
import { connect } from 'react-redux';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import Modal, { IButton } from 'components/ui/Modal';
import { AppState } from 'reducers';
import { resetWallet, TResetWallet } from 'actions/wallet';
interface Props extends RouteComponentProps<{}> {
// State
wallet: AppState['wallet']['inst'];
// Actions
resetWallet: TResetWallet;
}
interface State {
nextLocation: RouteComponentProps<{}>['location'] | null;
openModal: boolean;
}
class LogOutPromptClass extends React.Component<Props, State> {
constructor(props) {
super(props);
this.state = {
nextLocation: null,
openModal: false
};
this.props.history.block(nextLocation => {
if (this.props.wallet && nextLocation.pathname !== this.props.location.pathname) {
const isSubTab =
nextLocation.pathname.split('/')[1] === this.props.location.pathname.split('/')[1];
if (!isSubTab) {
this.setState({
openModal: true,
nextLocation
});
return false;
}
}
});
}
public render() {
const buttons: IButton[] = [
{ text: 'Log Out', type: 'primary', onClick: this.onConfirm },
{ text: 'Cancel', type: 'default', onClick: this.onCancel }
];
return (
<Modal
title="You are about to log out"
isOpen={this.state.openModal}
handleClose={this.onCancel}
buttons={buttons}
>
<p>Leaving this page will log you out. Are you sure you want to continue?</p>
</Modal>
);
}
private onCancel = () => {
this.setState({ nextLocation: null, openModal: false });
};
private onConfirm = () => {
const { nextLocation } = this.state;
this.props.resetWallet();
this.setState(
{
openModal: false,
nextLocation: null
},
() => {
if (nextLocation) {
this.props.history.push(nextLocation.pathname);
}
}
);
};
}
function mapStateToProps(state: AppState) {
return { wallet: state.wallet.inst };
}
export default connect(mapStateToProps, {
resetWallet
})(withRouter<Props>(LogOutPromptClass));

View File

@ -27,8 +27,8 @@ class NonceInputClass extends Component<Props> {
const { nonce: { raw, value }, onChange, shouldDisplay } = this.props;
const content = (
<Aux>
{nonceHelp}
<label>Nonce</label>
{nonceHelp}
<Query
params={['readOnly']}

View File

@ -6,10 +6,10 @@ import { connect } from 'react-redux';
import { AppState } from 'reducers';
interface Props {
allowReadOnly: boolean;
disabledWallets?: string[];
}
export const OfflineAwareUnlockHeader: React.SFC<Props> = ({ allowReadOnly }) => (
<UnlockHeader title={<Title />} allowReadOnly={allowReadOnly} />
export const OfflineAwareUnlockHeader: React.SFC<Props> = ({ disabledWallets }) => (
<UnlockHeader title={<Title />} disabledWallets={disabledWallets} />
);
interface StateProps {

View File

@ -0,0 +1,9 @@
.PageNotFound {
text-align: center;
&-content {
padding: 100px 0;
font-size: 30px;
max-width: 800px;
margin: 0 auto;
}
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import { Link, RouteComponentProps } from 'react-router-dom';
import TabSection from '../../containers/TabSection/index';
import './PageNotFound.scss';
const PageNotFound: React.SFC<RouteComponentProps<{}>> = () => (
<TabSection>
<section className="Tab-content PageNotFound">
<div className="Tab-content-pane">
<h1 className="PageNotFound-header">/ \</h1>
<main role="main">
<p className="PageNotFound-content">
Meow! Something went wrong and the page you were looking for doesn't yet exist. Try the{' '}
<Link to="/">home page</Link>.
</p>
</main>
</div>
</section>
</TabSection>
);
export default PageNotFound;

View File

@ -0,0 +1,3 @@
import PageNotFound from './PageNotFound';
export default PageNotFound;

View File

@ -1,7 +1,7 @@
import { PaperWallet } from 'components';
import { IFullWallet } from 'ethereumjs-wallet';
import React from 'react';
import translate from 'translations';
import { translateRaw } from 'translations';
import printElement from 'utils/printElement';
import { stripHexPrefix } from 'libs/values';
@ -39,13 +39,13 @@ const PrintableWallet: React.SFC<{ wallet: IFullWallet }> = ({ wallet }) => {
<PaperWallet address={address} privateKey={privateKey} />
<a
role="button"
aria-label={translate('x_Print')}
aria-label={translateRaw('x_Print')}
aria-describedby="x_PrintDesc"
className={'btn btn-lg btn-primary'}
className="btn btn-lg btn-primary btn-block"
onClick={print(address, privateKey)}
style={{ marginTop: 10 }}
style={{ margin: '10px auto 0', maxWidth: '260px' }}
>
{translate('x_Print')}
{translateRaw('x_Print')}
</a>
</div>
);

View File

@ -2,8 +2,11 @@ import React from 'react';
import { SendButtonFactory } from './SendButtonFactory';
import translate from 'translations';
export const SendButton: React.SFC<{}> = () => (
export const SendButton: React.SFC<{ onlyTransactionParameters?: boolean }> = ({
onlyTransactionParameters
}) => (
<SendButtonFactory
onlyTransactionParameters={!!onlyTransactionParameters}
withProps={({ onClick }) => (
<div className="row form-group">
<div className="col-xs-12">

View File

@ -18,6 +18,7 @@ interface StateProps {
walletType: IWalletType;
}
interface OwnProps {
onlyTransactionParameters?: boolean;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
@ -27,11 +28,13 @@ const getStringifiedTx = (serializedTransaction: string) =>
type Props = StateProps & OwnProps;
class SendButtonFactoryClass extends Component<Props> {
public render() {
const { onlyTransactionParameters } = this.props;
const columnSize = onlyTransactionParameters ? 12 : 6;
return (
<SerializedTransaction
withSerializedTransaction={serializedTransaction => (
<Aux>
<div className="col-sm-6">
<div className={`col-sm-${columnSize}`}>
<label>
{this.props.walletType.isWeb3Wallet
? 'Transaction Parameters'
@ -44,19 +47,21 @@ class SendButtonFactoryClass extends Component<Props> {
readOnly={true}
/>
</div>
<div className="col-sm-6">
<label>
{this.props.walletType.isWeb3Wallet
? 'Serialized Transaction Parameters'
: translate('SEND_signed')}
</label>
<textarea
className="form-control"
value={addHexPrefix(serializedTransaction)}
rows={4}
readOnly={true}
/>
</div>
{!onlyTransactionParameters && (
<div className="col-sm-6">
<label>
{this.props.walletType.isWeb3Wallet
? 'Serialized Transaction Parameters'
: translate('SEND_signed')}
</label>
<textarea
className="form-control"
value={addHexPrefix(serializedTransaction)}
rows={4}
readOnly={true}
/>
</div>
)}
<OfflineBroadcast />
<OnlineSend withProps={this.props.withProps} />
</Aux>

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { setUnitMeta, TSetUnitMeta } from 'actions/transaction';
import Dropdown from 'components/ui/Dropdown';
import { withConditional } from 'components/hocs';
import { TokenBalance, getShownTokenBalances } from 'selectors/wallet';
import { TokenBalance, MergedToken, getShownTokenBalances, getTokens } from 'selectors/wallet';
import { Query } from 'components/renderCbs';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
@ -15,6 +15,8 @@ interface DispatchProps {
interface StateProps {
unit: string;
tokens: TokenBalance[];
allTokens: MergedToken[];
showAllTokens?: boolean;
}
const StringDropdown = Dropdown as new () => Dropdown<string>;
@ -22,14 +24,15 @@ const ConditionalStringDropDown = withConditional(StringDropdown);
class UnitDropdownClass extends Component<DispatchProps & StateProps> {
public render() {
const { tokens, unit } = this.props;
const { tokens, allTokens, showAllTokens, unit } = this.props;
const focusedTokens = showAllTokens ? allTokens : tokens;
return (
<div className="input-group-btn">
<Query
params={['readOnly']}
withQuery={({ readOnly }) => (
<ConditionalStringDropDown
options={['ether', ...getTokenSymbols(tokens)]}
options={['ether', ...getTokenSymbols(focusedTokens)]}
value={unit}
condition={!readOnly}
conditionalProps={{
@ -46,11 +49,12 @@ class UnitDropdownClass extends Component<DispatchProps & StateProps> {
this.props.setUnitMeta(unit);
};
}
const getTokenSymbols = (tokens: TokenBalance[]) => tokens.map(t => t.symbol);
const getTokenSymbols = (tokens: (TokenBalance | MergedToken)[]) => tokens.map(t => t.symbol);
function mapStateToProps(state: AppState) {
return {
tokens: getShownTokenBalances(state, true),
allTokens: getTokens(state),
unit: getUnit(state)
};
}

View File

@ -1,101 +0,0 @@
import { isKeystorePassRequired } from 'libs/wallet';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
export interface KeystoreValue {
file: string;
password: string;
valid: boolean;
}
function isPassRequired(file: string): boolean {
let passReq = false;
try {
passReq = isKeystorePassRequired(file);
} catch (e) {
// TODO: communicate invalid file to user
}
return passReq;
}
export default class KeystoreDecrypt extends Component {
public props: {
value: KeystoreValue;
onChange(value: KeystoreValue): void;
onUnlock(): void;
};
public render() {
const { file, password } = this.props.value;
const passReq = isPassRequired(file);
return (
<section className="col-md-4 col-sm-6">
<div id="selectedUploadKey">
<h4>{translate('ADD_Radio_2_alt')}</h4>
<div className="form-group">
<input
className={'hidden'}
type="file"
id="fselector"
onChange={this.handleFileSelection}
/>
<label htmlFor="fselector" style={{ width: '100%' }}>
<a className="btn btn-default btn-block" id="aria1" tabIndex={0} role="button">
{translate('ADD_Radio_2_short')}
</a>
</label>
<div className={file.length && passReq ? '' : 'hidden'}>
<p>{translate('ADD_Label_3')}</p>
<input
className={`form-control ${password.length > 0 ? 'is-valid' : 'is-invalid'}`}
value={password}
onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown}
placeholder={translateRaw('x_Password')}
type="password"
/>
</div>
</div>
</div>
</section>
);
}
public onKeyDown = (e: any) => {
if (e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
this.props.onUnlock();
}
};
public onPasswordChange = (e: any) => {
const valid = this.props.value.file.length && e.target.value.length;
this.props.onChange({
...this.props.value,
password: e.target.value,
valid
});
};
public handleFileSelection = (e: any) => {
const fileReader = new FileReader();
const target = e.target;
const inputFile = target.files[0];
fileReader.onload = () => {
const keystore = fileReader.result;
const passReq = isPassRequired(keystore);
this.props.onChange({
...this.props.value,
file: keystore,
valid: keystore.length && !passReq
});
};
fileReader.readAsText(inputFile, 'utf-8');
};
}

View File

@ -1,100 +0,0 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import Modal, { IButton } from 'components/ui/Modal';
import { Location, History } from 'history';
interface Props {
when: boolean;
onConfirm?: any;
onCancel?: any;
}
interface InjectedProps extends Props {
location: Location;
history: History;
}
interface State {
nextLocation: Location | null;
openModal: boolean;
}
class NavigationPrompt extends React.Component<Props, State> {
public unblock;
get injected() {
return this.props as InjectedProps;
}
constructor(props) {
super(props);
this.state = {
nextLocation: null,
openModal: false
};
}
public setupUnblock() {
this.unblock = this.injected.history.block(nextLocation => {
if (this.props.when && nextLocation.pathname !== this.injected.location.pathname) {
const isSubTab =
nextLocation.pathname.split('/')[1] === this.injected.location.pathname.split('/')[1];
if (!isSubTab) {
this.setState({
openModal: true,
nextLocation
});
return false;
}
}
});
}
public componentDidMount() {
this.setupUnblock();
}
public componentWillUnmount() {
this.unblock();
}
public onCancel = () => {
if (this.props.onCancel) {
this.props.onCancel();
}
this.setState({ nextLocation: null, openModal: false });
};
public onConfirm = () => {
if (this.props.onConfirm) {
this.props.onConfirm();
}
// Lock Wallet
this.navigateToNextLocation();
};
public navigateToNextLocation() {
this.unblock();
if (this.state.nextLocation) {
this.injected.history.push(this.state.nextLocation.pathname);
}
}
public render() {
const buttons: IButton[] = [
{ text: 'Log Out', type: 'primary', onClick: this.onConfirm },
{ text: 'Cancel', type: 'default', onClick: this.onCancel }
];
return (
<Modal
title="You are about to log out"
isOpen={this.state.openModal}
handleClose={this.onCancel}
buttons={buttons}
>
<p>Leaving this page will log you out. Are you sure you want to continue?</p>
</Modal>
);
}
}
export default withRouter(NavigationPrompt);

View File

@ -1,113 +0,0 @@
import { isValidEncryptedPrivKey, isValidPrivKey } from 'libs/validators';
import { stripHexPrefix } from 'libs/values';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
export interface PrivateKeyValue {
key: string;
password: string;
valid: boolean;
}
interface Validated {
fixedPkey: string;
isValidPkey: boolean;
isPassRequired: boolean;
valid: boolean;
}
function validatePkeyAndPass(pkey: string, pass: string): Validated {
const fixedPkey = stripHexPrefix(pkey);
const validPkey = isValidPrivKey(fixedPkey);
const validEncPkey = isValidEncryptedPrivKey(fixedPkey);
const isValidPkey = validPkey || validEncPkey;
let isValidPass = false;
if (validPkey) {
isValidPass = true;
} else if (validEncPkey) {
isValidPass = pass.length > 0;
}
return {
fixedPkey,
isValidPkey,
isPassRequired: validEncPkey,
valid: isValidPkey && isValidPass
};
}
export default class PrivateKeyDecrypt extends Component {
public props: {
value: PrivateKeyValue;
onChange(value: PrivateKeyValue): void;
onUnlock(): void;
};
public render() {
const { key, password } = this.props.value;
const { isValidPkey, isPassRequired } = validatePkeyAndPass(key, password);
return (
<section className="col-md-4 col-sm-6">
<div id="selectedTypeKey">
<h4>{translate('ADD_Radio_3')}</h4>
<div className="form-group">
<textarea
id="aria-private-key"
className={`form-control ${isValidPkey ? 'is-valid' : 'is-invalid'}`}
value={key}
onChange={this.onPkeyChange}
onKeyDown={this.onKeyDown}
placeholder={translateRaw('x_PrivKey2')}
rows={4}
/>
</div>
{isValidPkey &&
isPassRequired && (
<div className="form-group">
<p>{translate('ADD_Label_3')}</p>
<input
className={`form-control ${password.length > 0 ? 'is-valid' : 'is-invalid'}`}
value={password}
onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown}
placeholder={translateRaw('x_Password')}
type="password"
/>
</div>
)}
</div>
</section>
);
}
public onPkeyChange = (e: React.SyntheticEvent<HTMLTextAreaElement>) => {
const pkey = (e.target as HTMLInputElement).value;
const pass = this.props.value.password;
const { fixedPkey, valid } = validatePkeyAndPass(pkey, pass);
this.props.onChange({ ...this.props.value, key: fixedPkey, valid });
};
public onPasswordChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
const pkey = this.props.value.key;
const pass = (e.target as HTMLInputElement).value;
const { valid } = validatePkeyAndPass(pkey, pass);
this.props.onChange({
...this.props.value,
password: pass,
valid
});
};
public onKeyDown = (e: any) => {
if (e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
this.props.onUnlock();
}
};
}

View File

@ -1,60 +0,0 @@
import React, { Component } from 'react';
import translate from 'translations';
import { donationAddressMap } from 'config/data';
import { isValidETHAddress } from 'libs/validators';
import { AddressOnlyWallet } from 'libs/wallet';
interface Props {
onUnlock(param: any): void;
}
interface State {
address: string;
}
export default class ViewOnlyDecrypt extends Component<Props, State> {
public state = {
address: ''
};
public render() {
const { address } = this.state;
const isValid = isValidETHAddress(address);
return (
<section className="col-md-4 col-sm-6">
<div id="selectedUploadKey">
<h4>{translate('MYWAL_Address')}</h4>
<form className="form-group" onSubmit={this.openWallet}>
<input
className={`form-control
${isValid ? 'is-valid' : 'is-invalid'}
`}
onChange={this.changeAddress}
value={address}
placeholder={donationAddressMap.ETH}
/>
<button className="btn btn-primary btn-block" disabled={!isValid}>
{translate('NAV_ViewWallet')}
</button>
</form>
</div>
</section>
);
}
private changeAddress = (ev: React.FormEvent<HTMLInputElement>) => {
this.setState({ address: ev.currentTarget.value });
};
private openWallet = (ev: React.SyntheticEvent<HTMLFormElement>) => {
const { address } = this.state;
ev.preventDefault();
if (isValidETHAddress(address)) {
const wallet = new AddressOnlyWallet(address);
this.props.onUnlock(wallet);
}
};
}

View File

@ -0,0 +1,123 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
$speed: 500ms;
@keyframes decrypt-enter {
0% {
opacity: 0;
transform: translateY(8px);
}
100% {
opacity: 1;
transform: translateY(0px);
}
}
@mixin decrypt-title {
text-align: center;
line-height: 1;
margin: 0 0 30px;
font-weight: normal;
animation: decrypt-enter $speed ease 1;
}
.WalletDecrypt {
position: relative;
&-wallets {
&-title {
@include decrypt-title;
}
&-row {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 10px;
@media screen and (max-width: $screen-xs) {
margin: 0;
}
&:last-child {
margin: 0;
}
}
}
&-decrypt {
position: relative;
text-align: center;
padding-bottom: $space;
&-back {
@include reset-button;
position: absolute;
top: 0;
left: 0;
line-height: $font-size-large;
opacity: 0.4;
transition: opacity 120ms ease, transform 120ms ease;
@media (max-width: $screen-md) {
top: auto;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
}
&:hover,
&:focus {
opacity: 0.8;
}
&:active {
outline: none;
opacity: 1;
}
.fa {
position: relative;
top: -2px;
font-size: 11px;
}
}
&-title {
@include decrypt-title;
}
&-form {
max-width: 360px;
margin: 0 auto;
}
}
}
// Animation between two slides
.DecryptContent {
&-enter {
opacity: 0;
transition: opacity $speed * .25 ease $speed * .125;
&-active {
opacity: 1;
}
}
&-exit {
position: absolute;
top: 0;
left: 0;
width: 100%;
opacity: 1;
transition: opacity $speed * .25 ease;
pointer-events: none;
&-active {
opacity: 0;
}
}
}

View File

@ -0,0 +1,391 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import isEmpty from 'lodash/isEmpty';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import {
setWallet,
TSetWallet,
unlockKeystore,
TUnlockKeystore,
unlockMnemonic,
TUnlockMnemonic,
unlockPrivateKey,
TUnlockPrivateKey,
unlockWeb3,
TUnlockWeb3,
resetWallet,
TResetWallet
} from 'actions/wallet';
import { reset, TReset } from 'actions/transaction';
import translate from 'translations';
import {
DigitalBitboxDecrypt,
KeystoreDecrypt,
LedgerNanoSDecrypt,
MnemonicDecrypt,
PrivateKeyDecrypt,
PrivateKeyValue,
TrezorDecrypt,
ViewOnlyDecrypt,
Web3Decrypt,
WalletButton
} from './components';
import { AppState } from 'reducers';
import { knowledgeBaseURL, isWeb3NodeAvailable } from 'config/data';
import { IWallet } from 'libs/wallet';
import DigitalBitboxIcon from 'assets/images/wallets/digital-bitbox.svg';
import LedgerIcon from 'assets/images/wallets/ledger.svg';
import MetamaskIcon from 'assets/images/wallets/metamask.svg';
import MistIcon from 'assets/images/wallets/mist.svg';
import TrezorIcon from 'assets/images/wallets/trezor.svg';
import './WalletDecrypt.scss';
type UnlockParams = {} | PrivateKeyValue;
interface Props {
resetTransactionState: TReset;
unlockKeystore: TUnlockKeystore;
unlockMnemonic: TUnlockMnemonic;
unlockPrivateKey: TUnlockPrivateKey;
setWallet: TSetWallet;
unlockWeb3: TUnlockWeb3;
resetWallet: TResetWallet;
wallet: IWallet;
hidden?: boolean;
offline: boolean;
disabledWallets?: string[];
}
interface State {
selectedWalletKey: string | null;
value: UnlockParams | null;
}
interface BaseWalletInfo {
lid: string;
component: any;
initialParams: object;
unlock: any;
helpLink?: string;
isReadOnly?: boolean;
attemptUnlock?: boolean;
}
export interface SecureWalletInfo extends BaseWalletInfo {
icon?: string | null;
description: string;
}
export interface InsecureWalletInfo extends BaseWalletInfo {
example: string;
}
const WEB3_TYPES = {
MetamaskInpageProvider: {
lid: 'x_MetaMask',
icon: MetamaskIcon
},
EthereumProvider: {
lid: 'x_Mist',
icon: MistIcon
}
};
const WEB3_TYPE: string | false =
(window as any).web3 && (window as any).web3.currentProvider.constructor.name;
const SECURE_WALLETS = ['web3', 'ledger-nano-s', 'trezor', 'digital-bitbox'];
const INSECURE_WALLETS = ['private-key', 'keystore-file', 'mnemonic-phrase'];
export class WalletDecrypt extends Component<Props, State> {
public WALLETS: { [key: string]: SecureWalletInfo | InsecureWalletInfo } = {
web3: {
lid: WEB3_TYPE ? WEB3_TYPES[WEB3_TYPE].lid : 'x_Web3',
icon: WEB3_TYPE && WEB3_TYPES[WEB3_TYPE].icon,
description: 'ADD_Web3Desc',
component: Web3Decrypt,
initialParams: {},
unlock: this.props.unlockWeb3,
attemptUnlock: true,
helpLink: `${knowledgeBaseURL}/migration/moving-from-private-key-to-metamask`
},
'ledger-nano-s': {
lid: 'x_Ledger',
icon: LedgerIcon,
description: 'ADD_HardwareDesc',
component: LedgerNanoSDecrypt,
initialParams: {},
unlock: this.props.setWallet,
helpLink:
'https://ledger.zendesk.com/hc/en-us/articles/115005200009-How-to-use-MyEtherWallet-with-Ledger'
},
trezor: {
lid: 'x_Trezor',
icon: TrezorIcon,
description: 'ADD_HardwareDesc',
component: TrezorDecrypt,
initialParams: {},
unlock: this.props.setWallet,
helpLink: 'https://doc.satoshilabs.com/trezor-apps/mew.html'
},
'digital-bitbox': {
lid: 'x_DigitalBitbox',
icon: DigitalBitboxIcon,
description: 'ADD_HardwareDesc',
component: DigitalBitboxDecrypt,
initialParams: {},
unlock: this.props.setWallet,
helpLink: 'https://digitalbitbox.com/ethereum'
},
'keystore-file': {
lid: 'x_Keystore2',
example: 'UTC--2017-12-15T17-35-22.547Z--6be6e49e82425a5aa56396db03512f2cc10e95e8',
component: KeystoreDecrypt,
initialParams: {
file: '',
password: ''
},
unlock: this.props.unlockKeystore,
helpLink: `${knowledgeBaseURL}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'mnemonic-phrase': {
lid: 'x_Mnemonic',
example: 'brain surround have swap horror cheese file distinct',
component: MnemonicDecrypt,
initialParams: {},
unlock: this.props.unlockMnemonic,
helpLink: `${knowledgeBaseURL}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'private-key': {
lid: 'x_PrivKey2',
example: 'f1d0e0789c6d40f399ca90cc674b7858de4c719e0d5752a60d5d2f6baa45d4c9',
component: PrivateKeyDecrypt,
initialParams: {
key: '',
password: ''
},
unlock: this.props.unlockPrivateKey,
helpLink: `${knowledgeBaseURL}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'view-only': {
lid: 'View Address',
example: '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8',
component: ViewOnlyDecrypt,
initialParams: {},
unlock: this.props.setWallet,
helpLink: '',
isReadOnly: true
}
};
public state: State = {
selectedWalletKey: null,
value: null
};
public componentWillReceiveProps(nextProps) {
// Reset state when unlock is hidden / revealed
if (nextProps.hidden !== this.props.hidden) {
this.setState({
value: null,
selectedWalletKey: null
});
}
}
public getSelectedWallet() {
const { selectedWalletKey } = this.state;
if (!selectedWalletKey) {
return null;
}
return this.WALLETS[selectedWalletKey];
}
public getDecryptionComponent() {
const selectedWallet = this.getSelectedWallet();
if (!selectedWallet) {
return null;
}
return (
<selectedWallet.component
value={this.state.value}
onChange={this.onChange}
onUnlock={this.onUnlock}
/>
);
}
public isOnlineRequiredWalletAndOffline(selectedWalletKey) {
const onlineRequiredWallets = ['trezor', 'ledger-nano-s'];
return this.props.offline && onlineRequiredWallets.includes(selectedWalletKey);
}
public buildWalletOptions() {
const viewOnly = this.WALLETS['view-only'] as InsecureWalletInfo;
return (
<div className="WalletDecrypt-wallets">
<h2 className="WalletDecrypt-wallets-title">{translate('decrypt_Access')}</h2>
<div className="WalletDecrypt-wallets-row">
{SECURE_WALLETS.map(type => {
const wallet = this.WALLETS[type] as SecureWalletInfo;
return (
<WalletButton
key={type}
name={translate(wallet.lid)}
description={translate(wallet.description)}
icon={wallet.icon}
helpLink={wallet.helpLink}
walletType={type}
isSecure={true}
isDisabled={this.isWalletDisabled(type)}
onClick={this.handleWalletChoice}
/>
);
})}
</div>
<div className="WalletDecrypt-wallets-row">
{INSECURE_WALLETS.map(type => {
const wallet = this.WALLETS[type] as InsecureWalletInfo;
return (
<WalletButton
key={type}
name={translate(wallet.lid)}
example={wallet.example}
helpLink={wallet.helpLink}
walletType={type}
isSecure={false}
isDisabled={this.isWalletDisabled(type)}
onClick={this.handleWalletChoice}
/>
);
})}
<WalletButton
key="view-only"
name={translate(viewOnly.lid)}
example={viewOnly.example}
helpLink={viewOnly.helpLink}
walletType="view-only"
isReadOnly={true}
isDisabled={this.isWalletDisabled('view-only')}
onClick={this.handleWalletChoice}
/>
</div>
</div>
);
}
public handleWalletChoice = async (walletType: string) => {
const wallet = this.WALLETS[walletType];
if (!wallet) {
return;
}
let timeout = 0;
const web3Available = await isWeb3NodeAvailable();
if (wallet.attemptUnlock && web3Available) {
// timeout is only the maximum wait time before secondary view is shown
// send view will be shown immediately on web3 resolve
timeout = 1000;
wallet.unlock();
}
setTimeout(() => {
this.setState({
selectedWalletKey: walletType,
value: wallet.initialParams
});
}, timeout);
};
public clearWalletChoice = () => {
this.setState({
selectedWalletKey: null,
value: null
});
};
public render() {
const { hidden } = this.props;
const selectedWallet = this.getSelectedWallet();
const decryptionComponent = this.getDecryptionComponent();
return (
<div>
{!hidden && (
<article className="Tab-content-pane">
<div className="WalletDecrypt">
<TransitionGroup>
{decryptionComponent && selectedWallet ? (
<CSSTransition classNames="DecryptContent" timeout={500} key="decrypt">
<div className="WalletDecrypt-decrypt">
<button
className="WalletDecrypt-decrypt-back"
onClick={this.clearWalletChoice}
>
<i className="fa fa-arrow-left" /> {translate('Change Wallet')}
</button>
<h2 className="WalletDecrypt-decrypt-title">
{!selectedWallet.isReadOnly && 'Unlock your'}{' '}
{translate(selectedWallet.lid)}
</h2>
<section className="WalletDecrypt-decrypt-form">
{decryptionComponent}
</section>
</div>
</CSSTransition>
) : (
<CSSTransition classNames="DecryptContent" timeout={500} key="wallets">
{this.buildWalletOptions()}
</CSSTransition>
)}
</TransitionGroup>
</div>
</article>
)}
</div>
);
}
public onChange = (value: UnlockParams) => {
this.setState({ value });
};
public onUnlock = (payload: any) => {
const { value, selectedWalletKey } = this.state;
if (!selectedWalletKey) {
return;
}
// some components (TrezorDecrypt) don't take an onChange prop, and thus
// this.state.value will remain unpopulated. in this case, we can expect
// the payload to contain the unlocked wallet info.
const unlockValue = value && !isEmpty(value) ? value : payload;
this.WALLETS[selectedWalletKey].unlock(unlockValue);
this.props.resetTransactionState();
};
private isWalletDisabled = (walletKey: string) => {
if (!this.props.disabledWallets) {
return false;
}
return this.props.disabledWallets.indexOf(walletKey) !== -1;
};
}
function mapStateToProps(state: AppState) {
return {
offline: state.config.offline,
wallet: state.wallet.inst
};
}
export default connect(mapStateToProps, {
unlockKeystore,
unlockMnemonic,
unlockPrivateKey,
unlockWeb3,
setWallet,
resetWallet,
resetTransactionState: reset
})(WalletDecrypt);

View File

@ -30,8 +30,8 @@ interface Props {
seed?: string;
// Redux state
wallets: DeterministicWalletData[];
desiredToken: string;
wallets: AppState['deterministicWallets']['wallets'];
desiredToken: AppState['deterministicWallets']['desiredToken'];
network: NetworkConfig;
tokens: MergedToken[];
@ -52,7 +52,7 @@ interface State {
page: number;
}
class DeterministicWalletsModal extends React.Component<Props, State> {
class DeterministicWalletsModalClass extends React.Component<Props, State> {
public state = {
selectedAddress: '',
selectedAddrIndex: 0,
@ -200,8 +200,8 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
}
}
private handleChangePath = (ev: React.SyntheticEvent<HTMLSelectElement>) => {
const { value } = ev.target as HTMLSelectElement;
private handleChangePath = (ev: React.FormEvent<HTMLSelectElement>) => {
const { value } = ev.currentTarget;
if (value === 'custom') {
this.setState({ isCustomPath: true });
@ -213,11 +213,11 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
}
};
private handleChangeCustomPath = (ev: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ customPath: (ev.target as HTMLInputElement).value });
private handleChangeCustomPath = (ev: React.FormEvent<HTMLInputElement>) => {
this.setState({ customPath: ev.currentTarget.value });
};
private handleSubmitCustomPath = (ev: React.SyntheticEvent<HTMLFormElement>) => {
private handleSubmitCustomPath = (ev: React.FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (!isValidPath(this.state.customPath)) {
return;
@ -225,8 +225,8 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
this.props.onPathChange(this.state.customPath);
};
private handleChangeToken = (ev: React.SyntheticEvent<HTMLSelectElement>) => {
this.props.setDesiredToken((ev.target as HTMLSelectElement).value || undefined);
private handleChangeToken = (ev: React.FormEvent<HTMLSelectElement>) => {
this.props.setDesiredToken(ev.currentTarget.value || undefined);
};
private handleConfirmAddress = () => {
@ -252,7 +252,7 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
const { selectedAddress } = this.state;
// Get renderable values, but keep 'em short
const token = wallet.tokenValues[desiredToken];
const token = desiredToken ? wallet.tokenValues[desiredToken] : null;
return (
<tr
@ -310,7 +310,7 @@ function mapStateToProps(state: AppState) {
};
}
export default connect(mapStateToProps, {
export const DeterministicWalletsModal = connect(mapStateToProps, {
getDeterministicWallets,
setDesiredToken
})(DeterministicWalletsModal);
})(DeterministicWalletsModalClass);

View File

@ -0,0 +1,7 @@
import React from 'react';
export class DigitalBitboxDecrypt extends React.Component<{}, {}> {
public render() {
return <strong>Not yet implemented</strong>;
}
}

View File

@ -0,0 +1,106 @@
import { isKeystorePassRequired } from 'libs/wallet';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
export interface KeystoreValue {
file: string;
password: string;
valid: boolean;
}
function isPassRequired(file: string): boolean {
let passReq = false;
try {
passReq = isKeystorePassRequired(file);
} catch (e) {
// TODO: communicate invalid file to user
}
return passReq;
}
export class KeystoreDecrypt extends Component {
public props: {
value: KeystoreValue;
onChange(value: KeystoreValue): void;
onUnlock(): void;
};
public render() {
const { file, password } = this.props.value;
const passReq = isPassRequired(file);
const unlockDisabled = !file || (passReq && !password);
return (
<form id="selectedUploadKey" onSubmit={this.unlock}>
<div className="form-group">
<input
className={'hidden'}
type="file"
id="fselector"
onChange={this.handleFileSelection}
/>
<label htmlFor="fselector" style={{ width: '100%' }}>
<a className="btn btn-default btn-block" id="aria1" tabIndex={0} role="button">
{translate('ADD_Radio_2_short')}
</a>
</label>
<div className={file.length && passReq ? '' : 'hidden'}>
<p>{translate('ADD_Label_3')}</p>
<input
className={`form-control ${password.length > 0 ? 'is-valid' : 'is-invalid'}`}
value={password}
onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown}
placeholder={translateRaw('x_Password')}
type="password"
/>
</div>
</div>
<button className="btn btn-primary btn-block" disabled={unlockDisabled}>
{translate('ADD_Label_6_short')}
</button>
</form>
);
}
private onKeyDown = (e: any) => {
if (e.keyCode === 13) {
this.unlock(e);
}
};
private unlock = (e: React.SyntheticEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
this.props.onUnlock();
};
private onPasswordChange = (e: any) => {
const valid = this.props.value.file.length && e.target.value.length;
this.props.onChange({
...this.props.value,
password: e.target.value,
valid
});
};
private handleFileSelection = (e: any) => {
const fileReader = new FileReader();
const target = e.target;
const inputFile = target.files[0];
fileReader.onload = () => {
const keystore = fileReader.result;
const passReq = isPassRequired(keystore);
this.props.onChange({
...this.props.value,
file: keystore,
valid: keystore.length && !passReq
});
};
fileReader.readAsText(inputFile, 'utf-8');
};
}

View File

@ -1,10 +1,5 @@
.LedgerDecrypt {
text-align: center;
padding-top: 30px;
&-decrypt {
width: 100%;
}
&-help {
margin-top: 10px;
@ -21,6 +16,16 @@
}
&-buy {
margin-top: 10px;
margin: 10px 0;
}
}
&-message {
display: flex;
justify-content: center;
align-items: center;
.Spinner {
margin-right: 16px;
}
}
}

View File

@ -1,11 +1,12 @@
import './LedgerNano.scss';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import { DeterministicWalletsModal } from './DeterministicWalletsModal';
import { LedgerWallet } from 'libs/wallet';
import Ledger3 from 'vendor/ledger3';
import LedgerEth from 'vendor/ledger-eth';
import DPATHS from 'config/dpaths';
import { Spinner } from 'components/ui';
const DEFAULT_PATH = DPATHS.LEDGER[0].value;
@ -19,31 +20,74 @@ interface State {
dPath: string;
error: string | null;
isLoading: boolean;
showTip: boolean;
}
export default class LedgerNanoSDecrypt extends Component<Props, State> {
export class LedgerNanoSDecrypt extends Component<Props, State> {
public state: State = {
publicKey: '',
chainCode: '',
dPath: DEFAULT_PATH,
error: null,
isLoading: false
isLoading: false,
showTip: false
};
public showTip = () => {
this.setState({
showTip: true
});
};
public render() {
const { dPath, publicKey, chainCode, error, isLoading } = this.state;
const { dPath, publicKey, chainCode, error, isLoading, showTip } = this.state;
const showErr = error ? 'is-showing' : '';
if (window.location.protocol !== 'https:') {
return (
<div className="LedgerDecrypt">
<div className="alert alert-danger">
Unlocking a Ledger hardware wallet is only possible on pages served over HTTPS. You can
unlock your wallet at <a href="https://myetherwallet.com">MyEtherWallet.com</a>
</div>
</div>
);
}
return (
<section className="LedgerDecrypt col-md-4 col-sm-6">
<div className="LedgerDecrypt">
{showTip && (
<p>
<strong>Tip: </strong>Make sure you're logged into the ethereum app on your hardware
wallet
</p>
)}
<button
className="LedgerDecrypt-decrypt btn btn-primary btn-lg"
className="LedgerDecrypt-decrypt btn btn-primary btn-lg btn-block"
onClick={this.handleNullConnect}
disabled={isLoading}
>
{isLoading ? 'Unlocking...' : translate('ADD_Ledger_scan')}
{isLoading ? (
<div className="LedgerDecrypt-message">
<Spinner light={true} />
Unlocking...
</div>
) : (
translate('ADD_Ledger_scan')
)}
</button>
<a
className="LedgerDecrypt-buy btn btn-sm btn-default"
href="https://www.ledgerwallet.com/r/fa4b?path=/products/"
target="_blank"
rel="noopener"
>
{translate('Dont have a Ledger? Order one now!')}
</a>
<div className={`LedgerDecrypt-error alert alert-danger ${showErr}`}>{error || '-'}</div>
<div className="LedgerDecrypt-help">
Guides:
<div>
@ -66,17 +110,6 @@ export default class LedgerNanoSDecrypt extends Component<Props, State> {
</div>
</div>
<div className={`LedgerDecrypt-error alert alert-danger ${showErr}`}>{error || '-'}</div>
<a
className="LedgerDecrypt-buy btn btn-sm btn-default"
href="https://www.ledgerwallet.com/r/fa4b?path=/products/"
target="_blank"
rel="noopener"
>
{translate('Dont have a Ledger? Order one now!')}
</a>
<DeterministicWalletsModal
isOpen={!!publicKey && !!chainCode}
publicKey={publicKey}
@ -88,7 +121,7 @@ export default class LedgerNanoSDecrypt extends Component<Props, State> {
onPathChange={this.handlePathChange}
walletType={translateRaw('x_Ledger')}
/>
</section>
</div>
);
}
@ -99,7 +132,8 @@ export default class LedgerNanoSDecrypt extends Component<Props, State> {
private handleConnect = (dPath: string = this.state.dPath) => {
this.setState({
isLoading: true,
error: null
error: null,
showTip: false
});
const ledger = new Ledger3('w0w');
@ -109,6 +143,9 @@ export default class LedgerNanoSDecrypt extends Component<Props, State> {
dPath,
(res, err) => {
if (err) {
if (err.errorCode === 5) {
this.showTip();
}
err = ethApp.getError(err);
}

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