Merge branch 'develop' into hide-button
This commit is contained in:
commit
46ccc15665
|
@ -1,36 +1,35 @@
|
|||
// Mixins
|
||||
// --------------------------------------------------
|
||||
// Utilities
|
||||
@import "mixins/hide-text.less";
|
||||
@import "mixins/opacity.less";
|
||||
@import "mixins/image.less";
|
||||
@import "mixins/labels.less";
|
||||
@import "mixins/reset-filter.less";
|
||||
@import "mixins/resize.less";
|
||||
@import "mixins/responsive-visibility.less";
|
||||
@import "mixins/size.less";
|
||||
@import "mixins/tab-focus.less";
|
||||
@import "mixins/reset-text.less";
|
||||
@import "mixins/text-emphasis.less";
|
||||
@import "mixins/text-overflow.less";
|
||||
@import "mixins/vendor-prefixes.less";
|
||||
@import 'mixins/hide-text.less';
|
||||
@import 'mixins/opacity.less';
|
||||
@import 'mixins/image.less';
|
||||
@import 'mixins/labels.less';
|
||||
@import 'mixins/reset-filter.less';
|
||||
@import 'mixins/resize.less';
|
||||
@import 'mixins/responsive-visibility.less';
|
||||
@import 'mixins/size.less';
|
||||
@import 'mixins/reset-text.less';
|
||||
@import 'mixins/text-emphasis.less';
|
||||
@import 'mixins/text-overflow.less';
|
||||
@import 'mixins/vendor-prefixes.less';
|
||||
// Components
|
||||
@import "mixins/alerts.less";
|
||||
@import "mixins/buttons.less";
|
||||
@import "mixins/panels.less";
|
||||
@import "mixins/pagination.less";
|
||||
@import "mixins/list-group.less";
|
||||
@import "mixins/nav-divider.less";
|
||||
@import "mixins/forms.less";
|
||||
@import "mixins/progress-bar.less";
|
||||
@import "mixins/table-row.less";
|
||||
@import 'mixins/alerts.less';
|
||||
@import 'mixins/buttons.less';
|
||||
@import 'mixins/panels.less';
|
||||
@import 'mixins/pagination.less';
|
||||
@import 'mixins/list-group.less';
|
||||
@import 'mixins/nav-divider.less';
|
||||
@import 'mixins/forms.less';
|
||||
@import 'mixins/progress-bar.less';
|
||||
@import 'mixins/table-row.less';
|
||||
// Skins
|
||||
@import "mixins/background-variant.less";
|
||||
@import "mixins/border-radius.less";
|
||||
@import "mixins/gradients.less";
|
||||
@import 'mixins/background-variant.less';
|
||||
@import 'mixins/border-radius.less';
|
||||
@import 'mixins/gradients.less';
|
||||
// Layout
|
||||
@import "mixins/clearfix.less";
|
||||
@import "mixins/center-block.less";
|
||||
@import "mixins/nav-vertical-align.less";
|
||||
@import "mixins/grid-framework.less";
|
||||
@import "mixins/grid.less";
|
||||
@import 'mixins/clearfix.less';
|
||||
@import 'mixins/center-block.less';
|
||||
@import 'mixins/nav-vertical-align.less';
|
||||
@import 'mixins/grid-framework.less';
|
||||
@import 'mixins/grid.less';
|
||||
|
|
|
@ -20,10 +20,10 @@
|
|||
// Set the border and box shadow on specific inputs to match
|
||||
.form-control {
|
||||
border-color: @border-color;
|
||||
.box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075)); // Redeclare so transitions work
|
||||
.box-shadow(inset 0 1px 1px rgba(0, 0, 0, 0.075)); // Redeclare so transitions work
|
||||
&:focus {
|
||||
border-color: darken(@border-color, 10%);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 3px rgba(@brand-primary, .5);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 3px rgba(@brand-primary, 0.5);
|
||||
}
|
||||
}
|
||||
// Set validation states also for addons
|
||||
|
@ -51,11 +51,10 @@
|
|||
// Example usage: change the default blue border and shadow to white for better
|
||||
// contrast against a dark gray background.
|
||||
.form-control-focus(@color: @input-border-focus) {
|
||||
@color-rgba: rgba(red(@color), green(@color), blue(@color), .6);
|
||||
@color-rgba: rgba(red(@color), green(@color), blue(@color), 0.6);
|
||||
&:focus {
|
||||
border-color: @color;
|
||||
outline: 0;
|
||||
.box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}");
|
||||
.box-shadow(~'inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
// WebKit-style focus
|
||||
|
||||
.tab-focus() {
|
||||
outline: thin dotted;
|
||||
outline-offset: 3px;
|
||||
}
|
|
@ -7,10 +7,6 @@
|
|||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
&:hover,
|
||||
&:active {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export const AmountField: React.SFC<Props> = ({
|
|||
<AmountFieldFactory
|
||||
withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => (
|
||||
<div className="input-group-wrapper">
|
||||
<label className="input-group input-group-inline-dropdown">
|
||||
<label className="input-group input-group-inline">
|
||||
<div className="input-group-header">{translate('SEND_amount')}</div>
|
||||
<Input
|
||||
className={`input-group-input ${
|
||||
|
|
|
@ -17,7 +17,7 @@ export const Coinbase: React.SFC<Props> = ({ address }) => (
|
|||
<h5 key="2">Buy ETH with USD</h5>
|
||||
</div>
|
||||
<div className="Promos-promo-images">
|
||||
<img src={CoinbaseLogo} />
|
||||
<img src={CoinbaseLogo} alt="Coinbase logo" />
|
||||
</div>
|
||||
</div>
|
||||
</NewTabLink>
|
||||
|
|
|
@ -11,8 +11,8 @@ export const HardwareWallets: React.SFC = () => (
|
|||
<h6>Learn more about protecting your funds.</h6>
|
||||
</div>
|
||||
<div className="Promos-promo-images">
|
||||
<img src={ledgerLogo} />
|
||||
<img src={trezorLogo} />
|
||||
<img src={ledgerLogo} alt="Ledger Logo" />
|
||||
<img src={trezorLogo} alt="Trezor Logo" />
|
||||
</div>
|
||||
</div>
|
||||
</HelpLink>
|
||||
|
|
|
@ -13,7 +13,7 @@ export const Shapeshift: React.SFC = () => (
|
|||
</h5>
|
||||
</div>
|
||||
<div className="Promos-promo-images">
|
||||
<img src={ShapeshiftLogo} />
|
||||
<img src={ShapeshiftLogo} alt="Shapeshift Logo" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
|
|
@ -85,7 +85,6 @@
|
|||
height: 12px;
|
||||
border: 3px solid $gray-lightest;
|
||||
border-radius: 100%;
|
||||
outline: none;
|
||||
opacity: 0.6;
|
||||
|
||||
&.is-active {
|
||||
|
@ -96,7 +95,7 @@
|
|||
|
||||
// Per-promo customizations
|
||||
&--shapeshift {
|
||||
background-color: #263A52;
|
||||
background-color: #263a52;
|
||||
|
||||
.Promos-promo-images {
|
||||
max-width: 130px;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "common/sass/variables";
|
||||
@import 'common/sass/variables';
|
||||
|
||||
.AddCustom {
|
||||
&-field {
|
||||
|
@ -11,23 +11,20 @@
|
|||
|
||||
&-buttons {
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&-help {
|
||||
text-align: center;
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&-btn {
|
||||
margin-right: 10px;
|
||||
|
||||
&.btn-primary {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
&.btn-default {
|
||||
width: 110px;
|
||||
}
|
||||
padding: 0.5rem 1.5rem;
|
||||
margin: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,11 +66,11 @@ export default class AddCustomTokenForm extends React.PureComponent<Props, State
|
|||
{fields.map(field => {
|
||||
return (
|
||||
<label className="AddCustom-field form-group" key={field.name}>
|
||||
<span className="AddCustom-field-label">{field.label}</span>
|
||||
<div className="input-group-header">{field.label}</div>
|
||||
<Input
|
||||
className={`${
|
||||
errors[field.name] ? 'invalid' : field.value ? 'valid' : ''
|
||||
} AddCustom-field-input input-sm`}
|
||||
} input-group-input-small`}
|
||||
type="text"
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
|
@ -83,22 +83,22 @@ export default class AddCustomTokenForm extends React.PureComponent<Props, State
|
|||
);
|
||||
})}
|
||||
|
||||
<div className="AddCustom-buttons">
|
||||
<HelpLink article={HELP_ARTICLE.ADDING_NEW_TOKENS} className="AddCustom-buttons-help">
|
||||
{translate('Need help? Learn how to add custom tokens.')}
|
||||
</HelpLink>
|
||||
<button
|
||||
className="AddCustom-buttons-btn btn btn-primary btn-sm"
|
||||
disabled={!this.isValid()}
|
||||
>
|
||||
{translate('x_Save')}
|
||||
</button>
|
||||
<div className="AddCustom-buttons">
|
||||
<button
|
||||
className="AddCustom-buttons-btn btn btn-sm btn-default"
|
||||
onClick={this.props.toggleForm}
|
||||
>
|
||||
{translate('x_Cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="AddCustom-buttons-btn btn btn-primary btn-sm"
|
||||
disabled={!this.isValid()}
|
||||
>
|
||||
{translate('x_Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -54,6 +54,7 @@ export default class TokenRow extends React.PureComponent<Props, State> {
|
|||
{!!custom && (
|
||||
<img
|
||||
src={removeIcon}
|
||||
alt="Remove"
|
||||
className="TokenRow-symbol-remove"
|
||||
title="Remove Token"
|
||||
onClick={this.onRemove}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "common/sass/variables";
|
||||
@import 'common/sass/variables';
|
||||
|
||||
.TokenBalances {
|
||||
&-title {
|
||||
|
@ -34,6 +34,12 @@
|
|||
}
|
||||
|
||||
&-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
& > &-btn {
|
||||
margin: 0.25rem 0.5rem;
|
||||
}
|
||||
&-help {
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@import "common/sass/variables";
|
||||
@import "common/sass/mixins";
|
||||
@import 'common/sass/variables';
|
||||
@import 'common/sass/mixins';
|
||||
|
||||
.BetaAgreement {
|
||||
@include cover-message;
|
||||
|
@ -20,7 +20,6 @@
|
|||
margin: 0 auto;
|
||||
border: none;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
transition: $transition;
|
||||
|
||||
&.is-continue {
|
||||
|
|
|
@ -55,13 +55,13 @@ export default class GenerateKeystoreModal extends React.Component<Props, State>
|
|||
|
||||
return (
|
||||
<Modal
|
||||
title={translate('Generate Keystore File')}
|
||||
title={translateRaw('Generate Keystore File')}
|
||||
isOpen={this.props.isOpen}
|
||||
handleClose={this.handleClose}
|
||||
>
|
||||
<form className="GenKeystore" onSubmit={this.handleSubmit}>
|
||||
<div className="input-group-wrapper GenKeystore-field">
|
||||
<label className="input-group input-group-inline-dropdown">
|
||||
<label className="input-group input-group-inline">
|
||||
<div className="input-group-header">Private Key</div>
|
||||
<TogglablePassword
|
||||
name="privateKey"
|
||||
|
@ -74,7 +74,7 @@ export default class GenerateKeystoreModal extends React.Component<Props, State>
|
|||
</label>
|
||||
</div>
|
||||
<div className="input-group-wrapper GenKeystore-field">
|
||||
<label className="input-group input-group-inline-dropdown">
|
||||
<label className="input-group input-group-inline">
|
||||
<div className="input-group-header">Password</div>
|
||||
<TogglablePassword
|
||||
name="password"
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
.CustomNodeModal {
|
||||
.flex-wrapper {
|
||||
margin: 0px -8px;
|
||||
> .input-group {
|
||||
margin: 0px 8px;
|
||||
> .input-group-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
input[type='checkbox'] {
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import Modal, { IButton } from 'components/ui/Modal';
|
||||
import translate from 'translations';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import { CustomNetworkConfig } from 'types/network';
|
||||
import { CustomNodeConfig } from 'types/node';
|
||||
import { TAddCustomNetwork, addCustomNetwork, AddCustomNodeAction } from 'actions/config';
|
||||
|
@ -13,19 +13,13 @@ import {
|
|||
} from 'selectors/config';
|
||||
import { CustomNode } from 'libs/nodes';
|
||||
import { Input } from 'components/ui';
|
||||
import Dropdown from 'components/ui/Dropdown';
|
||||
import './CustomNodeModal.scss';
|
||||
|
||||
const CUSTOM = 'custom';
|
||||
|
||||
interface InputProps {
|
||||
name: keyof Omit<State, 'hasAuth'>;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
autoComplete?: 'off';
|
||||
onFocus?(): void;
|
||||
onBlur?(): void;
|
||||
}
|
||||
const CUSTOM = { label: 'Custom', value: 'custom' };
|
||||
|
||||
interface OwnProps {
|
||||
isOpen: boolean;
|
||||
addCustomNode(payload: AddCustomNodeAction['payload']): void;
|
||||
handleClose(): void;
|
||||
}
|
||||
|
@ -55,7 +49,7 @@ interface State {
|
|||
type Props = OwnProps & StateProps & DispatchProps;
|
||||
|
||||
class CustomNodeModal extends React.Component<Props, State> {
|
||||
public state: State = {
|
||||
public INITIAL_STATE = {
|
||||
name: '',
|
||||
url: '',
|
||||
network: Object.keys(this.props.staticNetworks)[0],
|
||||
|
@ -66,9 +60,17 @@ class CustomNodeModal extends React.Component<Props, State> {
|
|||
username: '',
|
||||
password: ''
|
||||
};
|
||||
public state: State = this.INITIAL_STATE;
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
// Reset state when modal opens
|
||||
if (!prevProps.isOpen && prevProps.isOpen !== this.props.isOpen) {
|
||||
this.setState(this.INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { customNetworks, handleClose, staticNetworks } = this.props;
|
||||
const { customNetworks, handleClose, staticNetworks, isOpen } = this.props;
|
||||
const { network } = this.state;
|
||||
const isHttps = window.location.protocol.includes('https');
|
||||
const invalids = this.getInvalids();
|
||||
|
@ -88,16 +90,21 @@ class CustomNodeModal extends React.Component<Props, State> {
|
|||
];
|
||||
|
||||
const conflictedNode = this.getConflictedNode();
|
||||
|
||||
const staticNetwrks = Object.keys(staticNetworks).map(net => {
|
||||
return { label: net, value: net };
|
||||
});
|
||||
const customNetwrks = Object.entries(customNetworks).map(([id, net]) => {
|
||||
return { label: net.name + ' (Custom)', value: id };
|
||||
});
|
||||
const options = [...staticNetwrks, ...customNetwrks, CUSTOM];
|
||||
return (
|
||||
<Modal
|
||||
title={translate('NODE_Title')}
|
||||
isOpen={true}
|
||||
title={translateRaw('NODE_Title')}
|
||||
isOpen={isOpen}
|
||||
buttons={buttons}
|
||||
handleClose={handleClose}
|
||||
maxWidth={580}
|
||||
>
|
||||
<div>
|
||||
{isHttps && <div className="alert alert-warning small">{translate('NODE_Warning')}</div>}
|
||||
|
||||
{conflictedNode && (
|
||||
|
@ -107,139 +114,128 @@ class CustomNodeModal extends React.Component<Props, State> {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<form>
|
||||
<div className="row">
|
||||
<div className="col-sm-7">
|
||||
<label>{translate('NODE_Name')}</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'name',
|
||||
placeholder: 'My Node'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-5">
|
||||
<label>Network</label>
|
||||
<select
|
||||
className="form-control"
|
||||
name="network"
|
||||
<form className="CustomNodeModal">
|
||||
<div className="flex-wrapper">
|
||||
<label className="col-sm-9 input-group flex-grow-1">
|
||||
<div className="input-group-header">Node Name</div>
|
||||
<Input
|
||||
className={`input-group-input ${this.state.name && invalids.name ? 'invalid' : ''}`}
|
||||
type="text"
|
||||
placeholder="My Node"
|
||||
value={this.state.name}
|
||||
onChange={e => this.setState({ name: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="col-sm-3 input-group">
|
||||
<div className="input-group-header">Network</div>
|
||||
<Dropdown
|
||||
className="input-group-dropdown"
|
||||
value={network}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
{Object.keys(staticNetworks).map(net => (
|
||||
<option key={net} value={net}>
|
||||
{net}
|
||||
</option>
|
||||
))}
|
||||
{Object.entries(customNetworks).map(([id, net]) => (
|
||||
<option key={id} value={id}>
|
||||
{net.name} (Custom)
|
||||
</option>
|
||||
))}
|
||||
<option value={CUSTOM}>Custom...</option>
|
||||
</select>
|
||||
</div>
|
||||
options={options}
|
||||
clearable={false}
|
||||
onChange={(e: { label: string; value: string }) =>
|
||||
this.setState({ network: e.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{network === CUSTOM && (
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<label className="is-required">Network Name</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'customNetworkId',
|
||||
placeholder: 'My Custom Network'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<label className="is-required">Currency</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'customNetworkUnit',
|
||||
placeholder: 'ETH'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<label>Chain ID</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'customNetworkChainId',
|
||||
placeholder: 'e.g. 1'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
{network === CUSTOM.value && (
|
||||
<div className="flex-wrapper">
|
||||
<label className="col-sm-6 input-group input-group-inline">
|
||||
<div className="input-group-header">Network Name</div>
|
||||
<Input
|
||||
className={`input-group-input ${
|
||||
this.state.customNetworkId && invalids.customNetworkId ? 'invalid' : ''
|
||||
}`}
|
||||
type="text"
|
||||
placeholder="My Custom Network"
|
||||
value={this.state.customNetworkId}
|
||||
onChange={e => this.setState({ customNetworkId: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="col-sm-3 input-group input-group-inline">
|
||||
<div className="input-group-header">Currency</div>
|
||||
<Input
|
||||
className={`input-group-input ${
|
||||
this.state.customNetworkUnit && invalids.customNetworkUnit ? 'invalid' : ''
|
||||
}`}
|
||||
type="text"
|
||||
placeholder="ETH"
|
||||
value={this.state.customNetworkUnit}
|
||||
onChange={e => this.setState({ customNetworkUnit: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="col-sm-3 input-group input-group-inline">
|
||||
<div className="input-group-header">Chain ID</div>
|
||||
<Input
|
||||
className={`input-group-input ${
|
||||
this.state.customNetworkChainId && invalids.customNetworkChainId
|
||||
? 'invalid'
|
||||
: ''
|
||||
}`}
|
||||
type="text"
|
||||
placeholder="1"
|
||||
value={this.state.customNetworkChainId}
|
||||
onChange={e => this.setState({ customNetworkChainId: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<label>URL</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'url',
|
||||
placeholder: 'e.g. https://127.0.0.1:8545/',
|
||||
autoComplete: 'off'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<label className="input-group input-group-inline">
|
||||
<div className="input-group-header">URL</div>
|
||||
<Input
|
||||
className={`input-group-input ${this.state.url && invalids.url ? 'invalid' : ''}`}
|
||||
type="text"
|
||||
placeholder="https://127.0.0.1:8545/"
|
||||
value={this.state.url}
|
||||
onChange={e => this.setState({ url: e.currentTarget.value })}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="hasAuth"
|
||||
checked={this.state.hasAuth}
|
||||
onChange={this.handleCheckbox}
|
||||
/>{' '}
|
||||
onChange={() => this.setState({ hasAuth: !this.state.hasAuth })}
|
||||
/>
|
||||
<span>HTTP Basic Authentication</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.hasAuth && (
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<label className="is-required">Username</label>
|
||||
{this.renderInput({ name: 'username' }, invalids)}
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<label className="is-required">Password</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-wrapper ">
|
||||
<label className="col-sm-6 input-group input-group-inline">
|
||||
<div className="input-group-header">Username</div>
|
||||
<Input
|
||||
className={`input-group-input ${
|
||||
this.state.username && invalids.username ? 'invalid' : ''
|
||||
}`}
|
||||
type="text"
|
||||
value={this.state.username}
|
||||
onChange={e => this.setState({ username: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="col-sm-6 input-group input-group-inline">
|
||||
<div className="input-group-header">Password</div>
|
||||
<Input
|
||||
className={`input-group-input ${
|
||||
this.state.password && invalids.password ? 'invalid' : ''
|
||||
}`}
|
||||
type="password"
|
||||
value={this.state.password}
|
||||
onChange={e => this.setState({ password: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
private renderInput(input: InputProps, invalids: { [key: string]: boolean }) {
|
||||
return (
|
||||
<Input
|
||||
className={`${this.state[input.name] && invalids[input.name] ? 'invalid' : ''}`}
|
||||
value={this.state[input.name]}
|
||||
onChange={this.handleChange}
|
||||
autoComplete="off"
|
||||
{...input}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private getInvalids(): { [key: string]: boolean } {
|
||||
const {
|
||||
url,
|
||||
|
@ -278,7 +274,7 @@ class CustomNodeModal extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
// If they have a custom network, make sure info is provided
|
||||
if (network === CUSTOM) {
|
||||
if (network === CUSTOM.value) {
|
||||
if (!customNetworkId) {
|
||||
invalids.customNetworkId = true;
|
||||
}
|
||||
|
@ -315,7 +311,7 @@ class CustomNodeModal extends React.Component<Props, State> {
|
|||
const { network, url, name, username, password } = this.state;
|
||||
|
||||
const networkId =
|
||||
network === CUSTOM
|
||||
network === CUSTOM.value
|
||||
? this.makeCustomNetworkId(this.makeCustomNetworkConfigFromState())
|
||||
: network;
|
||||
|
||||
|
@ -348,20 +344,10 @@ class CustomNodeModal extends React.Component<Props, State> {
|
|||
return customNodes[config.id];
|
||||
}
|
||||
|
||||
private handleChange = (ev: React.FormEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = ev.currentTarget;
|
||||
this.setState({ [name as any]: value });
|
||||
};
|
||||
|
||||
private handleCheckbox = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const { name } = ev.currentTarget;
|
||||
this.setState({ [name as any]: !this.state[name as keyof State] });
|
||||
};
|
||||
|
||||
private saveAndAdd = () => {
|
||||
const node = this.makeCustomNodeConfigFromState();
|
||||
|
||||
if (this.state.network === CUSTOM) {
|
||||
if (this.state.network === CUSTOM.value) {
|
||||
const network = this.makeCustomNetworkConfigFromState();
|
||||
|
||||
this.props.addCustomNetwork({ config: network, id: node.network });
|
||||
|
|
|
@ -192,12 +192,11 @@ class Header extends Component<Props, State> {
|
|||
|
||||
<Navigation color={!network.isCustom && network.color} />
|
||||
|
||||
{isAddingCustomNode && (
|
||||
<CustomNodeModal
|
||||
isOpen={isAddingCustomNode}
|
||||
addCustomNode={this.addCustomNode}
|
||||
handleClose={this.closeCustomNodeModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -100,8 +100,8 @@ export default class PaperWallet extends React.Component<Props, {}> {
|
|||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<img src={sidebarImg} style={styles.sidebar} />
|
||||
<img src={ethLogo} style={styles.ethLogo} />
|
||||
<img src={sidebarImg} style={styles.sidebar} alt="MyCrypto Logo" />
|
||||
<img src={ethLogo} style={styles.ethLogo} alt="ETH Logo" />
|
||||
|
||||
<div style={styles.block}>
|
||||
<div style={styles.box}>
|
||||
|
@ -111,7 +111,7 @@ export default class PaperWallet extends React.Component<Props, {}> {
|
|||
</div>
|
||||
|
||||
<div style={styles.block}>
|
||||
<img src={notesBg} style={styles.box} />
|
||||
<img src={notesBg} style={styles.box} aria-hidden={true} />
|
||||
<p style={styles.blockText}>AMOUNT / NOTES</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ interface Props {
|
|||
isValid?: boolean;
|
||||
isVisible?: boolean;
|
||||
validity?: 'valid' | 'invalid' | 'semivalid';
|
||||
readOnly?: boolean;
|
||||
|
||||
// Textarea-only props
|
||||
isTextareaWhenVisible?: boolean;
|
||||
|
@ -61,18 +62,16 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
|||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
handleToggleVisibility
|
||||
handleToggleVisibility,
|
||||
readOnly
|
||||
} = this.props;
|
||||
const { isVisible } = this.state;
|
||||
const validClass = validity
|
||||
? `is-${validity}`
|
||||
: isValid === null || isValid === undefined ? '' : isValid ? 'is-valid' : 'is-invalid';
|
||||
|
||||
return (
|
||||
<div className={`TogglablePassword input-group input-group-inline-dropdown ${className}`}>
|
||||
<div className={`TogglablePassword input-group input-group-inline ${className}`}>
|
||||
{isTextareaWhenVisible && isVisible ? (
|
||||
<TextArea
|
||||
className={validClass}
|
||||
className={validity || !isValid ? 'invalid' : ''}
|
||||
value={value}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
|
@ -83,6 +82,7 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
|||
placeholder={placeholder}
|
||||
rows={this.props.rows || 3}
|
||||
aria-label={ariaLabel}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
|
@ -90,12 +90,13 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
|||
name={name}
|
||||
disabled={disabled}
|
||||
type={isVisible ? 'text' : 'password'}
|
||||
className={`${validClass}`}
|
||||
className={`${validity || !isValid ? 'invalid' : ''} border-rad-right-0`}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
aria-label={ariaLabel}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
|
|
|
@ -40,7 +40,6 @@ $speed: 500ms;
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -80,7 +79,6 @@ $speed: 500ms;
|
|||
}
|
||||
|
||||
&:active {
|
||||
outline: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
@ -106,7 +104,7 @@ $speed: 500ms;
|
|||
.DecryptContent {
|
||||
&-enter {
|
||||
opacity: 0;
|
||||
transition: opacity $speed * .25 ease $speed * .125;
|
||||
transition: opacity $speed * 0.25 ease $speed * 0.125;
|
||||
|
||||
&-active {
|
||||
opacity: 1;
|
||||
|
@ -119,7 +117,7 @@ $speed: 500ms;
|
|||
left: 0;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
transition: opacity $speed * .25 ease;
|
||||
transition: opacity $speed * 0.25 ease;
|
||||
pointer-events: none;
|
||||
|
||||
&-active {
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
transition: transform 150ms ease, box-shadow 150ms ease;
|
||||
animation: wallet-button-enter 400ms ease 1;
|
||||
animation-fill-mode: backwards;
|
||||
outline: none;
|
||||
|
||||
@for $i from 0 to 5 {
|
||||
&:nth-child(#{$i}) {
|
||||
|
@ -60,7 +59,6 @@
|
|||
}
|
||||
|
||||
&.is-disabled {
|
||||
outline: none;
|
||||
cursor: not-allowed;
|
||||
@include show-tooltip-on-hover;
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ interface Icon {
|
|||
icon: string;
|
||||
tooltip: string;
|
||||
href?: string;
|
||||
arialabel: string;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps;
|
||||
|
@ -50,18 +51,21 @@ export class WalletButton extends React.PureComponent<Props> {
|
|||
if (isReadOnly) {
|
||||
icons.push({
|
||||
icon: 'eye',
|
||||
tooltip: translateRaw('You cannot send using address only')
|
||||
tooltip: translateRaw('You cannot send using address only'),
|
||||
arialabel: 'Read Only'
|
||||
});
|
||||
} else {
|
||||
if (isSecure) {
|
||||
icons.push({
|
||||
icon: 'shield',
|
||||
tooltip: translateRaw('This wallet type is secure')
|
||||
tooltip: translateRaw('This wallet type is secure'),
|
||||
arialabel: 'Secure wallet type'
|
||||
});
|
||||
} else {
|
||||
icons.push({
|
||||
icon: 'exclamation-triangle',
|
||||
tooltip: translateRaw('This wallet type is insecure')
|
||||
tooltip: translateRaw('This wallet type is insecure'),
|
||||
arialabel: 'Insecure wallet type'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +73,8 @@ export class WalletButton extends React.PureComponent<Props> {
|
|||
icons.push({
|
||||
icon: 'question-circle',
|
||||
tooltip: translateRaw('NAV_Help'),
|
||||
href: helpLink
|
||||
href: helpLink,
|
||||
arialabel: 'More info'
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -86,22 +91,30 @@ export class WalletButton extends React.PureComponent<Props> {
|
|||
>
|
||||
<div className="WalletButton-inner">
|
||||
<div className="WalletButton-title">
|
||||
{icon && <img className="WalletButton-title-icon" src={icon} />}
|
||||
{icon && <img className="WalletButton-title-icon" src={icon} alt={name + ' logo'} />}
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
|
||||
{description && <div className="WalletButton-description">{description}</div>}
|
||||
{example && <div className="WalletButton-example">{example}</div>}
|
||||
{description && (
|
||||
<div className="WalletButton-description" aria-label="description">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
{example && (
|
||||
<div className="WalletButton-example" aria-label="example" aria-hidden={true}>
|
||||
{example}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="WalletButton-icons">
|
||||
{icons.map(i => (
|
||||
<span className="WalletButton-icons-icon" key={i.icon} onClick={this.stopPropogation}>
|
||||
{i.href ? (
|
||||
<NewTabLink href={i.href} onClick={this.stopPropogation}>
|
||||
<NewTabLink href={i.href} onClick={this.stopPropogation} aria-label={i.arialabel}>
|
||||
<i className={`fa fa-${i.icon}`} />
|
||||
</NewTabLink>
|
||||
) : (
|
||||
<i className={`fa fa-${i.icon}`} />
|
||||
<i className={`fa fa-${i.icon}`} aria-label={i.arialabel} />
|
||||
)}
|
||||
{!isDisabled && <Tooltip size="sm">{i.tooltip}</Tooltip>}
|
||||
</span>
|
||||
|
|
|
@ -92,6 +92,7 @@ export default class ColorDropdown<T> extends PureComponent<Props<T>, {}> {
|
|||
className="ColorDropdown-item-remove"
|
||||
onClick={this.onRemove.bind(null, option.onRemove)}
|
||||
src={removeIcon}
|
||||
alt="remove"
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
|
|
|
@ -12,7 +12,7 @@ interface Props {
|
|||
const Help = ({ size = 'x1', link }: Props) => {
|
||||
return (
|
||||
<a href={link} className={`Help Help-${size}`} target="_blank" rel="noopener noreferrer">
|
||||
<img src={icon} />
|
||||
<img src={icon} alt="help" />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@ export default function Identicon(props: Props) {
|
|||
<React.Fragment>
|
||||
<img
|
||||
src={identiconDataUrl}
|
||||
alt="Unique Address Image"
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
|
|
|
@ -12,6 +12,22 @@
|
|||
> .TogglablePassword {
|
||||
width: 100%;
|
||||
}
|
||||
&-inline {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 1rem;
|
||||
flex-wrap: wrap;
|
||||
> .input-group-header {
|
||||
width: 100%;
|
||||
}
|
||||
> .input-group-input {
|
||||
flex-grow: 1;
|
||||
width: auto;
|
||||
}
|
||||
> .Select {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
&-header {
|
||||
display: flex;
|
||||
font-size: 1rem;
|
||||
|
@ -29,6 +45,9 @@
|
|||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
}
|
||||
&-dropdown {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
&-input {
|
||||
width: 100%;
|
||||
border: 1px solid #e5ecf3;
|
||||
|
@ -40,6 +59,17 @@
|
|||
box-shadow: inset 0 1px 0 0 rgba(63, 63, 68, 0.05);
|
||||
transition: border-color 120ms, box-shadow 120ms;
|
||||
margin-bottom: 1rem;
|
||||
&.border-rad-right-0 {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
&.border-rad-left-0 {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
&-small {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
&::placeholder {
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
@ -60,23 +90,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.input-group-inline-dropdown {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 1rem;
|
||||
flex-wrap: wrap;
|
||||
> .input-group-header {
|
||||
width: 100%;
|
||||
}
|
||||
> .input-group-input {
|
||||
flex-grow: 1;
|
||||
width: auto;
|
||||
}
|
||||
> .Select {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.Swap-dropdown {
|
||||
.Select-input {
|
||||
left: 24px;
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
import closeIcon from 'assets/images/close.svg';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
import './Modal.scss';
|
||||
|
||||
export interface IButton {
|
||||
text: string | React.ReactElement<string>;
|
||||
type?: 'default' | 'primary' | 'success' | 'info' | 'warning' | 'danger' | 'link';
|
||||
disabled?: boolean;
|
||||
onClick?(): void;
|
||||
}
|
||||
interface Props {
|
||||
isOpen?: boolean;
|
||||
title?: string | React.ReactElement<any>;
|
||||
disableButtons?: boolean;
|
||||
children: any;
|
||||
buttons?: IButton[];
|
||||
maxWidth?: number;
|
||||
handleClose?(): void;
|
||||
}
|
||||
interface ModalStyle {
|
||||
width?: string;
|
||||
maxWidth?: string;
|
||||
}
|
||||
|
||||
const Fade = ({ children, ...props }: any) => (
|
||||
<CSSTransition {...props} timeout={300} classNames="animate-modal">
|
||||
{children}
|
||||
</CSSTransition>
|
||||
);
|
||||
|
||||
export default class Modal extends PureComponent<Props, {}> {
|
||||
private modalContent: HTMLElement | null = null;
|
||||
|
||||
public componentDidMount() {
|
||||
this.updateBodyClass();
|
||||
document.addEventListener('keydown', this.escapeListner);
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.updateBodyClass();
|
||||
}
|
||||
|
||||
public updateBodyClass() {
|
||||
document.body.classList.toggle('no-scroll', !!this.props.isOpen);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.escapeListner);
|
||||
document.body.classList.remove('no-scroll');
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isOpen, title, children, buttons, handleClose, maxWidth } = this.props;
|
||||
const hasButtons = buttons && buttons.length;
|
||||
const modalStyle: ModalStyle = {};
|
||||
|
||||
if (maxWidth) {
|
||||
modalStyle.width = '100%';
|
||||
modalStyle.maxWidth = `${maxWidth}px`;
|
||||
}
|
||||
|
||||
return (
|
||||
<TransitionGroup>
|
||||
{isOpen && (
|
||||
<Fade>
|
||||
<div>
|
||||
<div className="Modalshade" />
|
||||
<div className="Modal" style={modalStyle}>
|
||||
{title && (
|
||||
<div className="Modal-header flex-wrapper">
|
||||
<h2 className="Modal-header-title">{title}</h2>
|
||||
<div className="flex-spacer" />
|
||||
<button className="Modal-header-close" onClick={handleClose}>
|
||||
<img className="Modal-header-close-icon" src={closeIcon} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="Modal-content" ref={el => (this.modalContent = el)}>
|
||||
{isOpen && children}
|
||||
<div className="Modal-fade" />
|
||||
</div>
|
||||
{hasButtons && <div className="Modal-footer">{this.renderButtons()}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</Fade>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
|
||||
public scrollContentToTop = () => {
|
||||
if (this.modalContent) {
|
||||
this.modalContent.scrollTop = 0;
|
||||
}
|
||||
};
|
||||
|
||||
private escapeListner = (ev: KeyboardEvent) => {
|
||||
if (!this.props.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't trigger if they hit escape while on an input
|
||||
if (ev.target) {
|
||||
if (
|
||||
(ev.target as HTMLElement).tagName === 'INPUT' ||
|
||||
(ev.target as HTMLElement).tagName === 'SELECT' ||
|
||||
(ev.target as HTMLElement).tagName === 'TEXTAREA' ||
|
||||
(ev.target as HTMLElement).isContentEditable
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.key === 'Escape' || ev.keyCode === 27) {
|
||||
if (!this.props.handleClose) {
|
||||
return;
|
||||
}
|
||||
this.props.handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
private renderButtons = () => {
|
||||
const { disableButtons, buttons } = this.props;
|
||||
if (!buttons || !buttons.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return buttons.map((btn, idx) => {
|
||||
let btnClass = 'Modal-footer-btn btn';
|
||||
|
||||
if (btn.type) {
|
||||
btnClass += ` btn-${btn.type}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={btnClass}
|
||||
onClick={btn.onClick}
|
||||
key={idx}
|
||||
disabled={disableButtons || btn.disabled}
|
||||
>
|
||||
{btn.text}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
import React, { CSSProperties } from 'react';
|
||||
import closeIcon from 'assets/images/close.svg';
|
||||
import { IButton } from 'components/ui/Modal';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
children: any;
|
||||
modalStyle?: CSSProperties;
|
||||
hasButtons?: number;
|
||||
buttons?: IButton[];
|
||||
disableButtons?: any;
|
||||
handleClose(): void;
|
||||
}
|
||||
|
||||
export default class ModalBody extends React.Component<Props> {
|
||||
private modal: HTMLElement;
|
||||
private modalContent: HTMLElement;
|
||||
private focusedElementBeforeModal: HTMLElement;
|
||||
private firstTabStop: HTMLElement;
|
||||
private lastTabStop: HTMLElement;
|
||||
|
||||
public componentDidMount() {
|
||||
this.focusedElementBeforeModal = document.activeElement as HTMLElement;
|
||||
// Find all focusable children
|
||||
const focusableElementsString =
|
||||
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]';
|
||||
const focusableElements = Array.prototype.slice.call(
|
||||
this.modal.querySelectorAll(focusableElementsString)
|
||||
);
|
||||
|
||||
// Convert NodeList to Array
|
||||
this.firstTabStop = focusableElements[0];
|
||||
this.lastTabStop = focusableElements[focusableElements.length - 1];
|
||||
|
||||
// Focus first child
|
||||
this.firstTabStop.focus();
|
||||
|
||||
this.modal.addEventListener('keydown', this.keyDownListener);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.keyDownListener);
|
||||
}
|
||||
|
||||
public scrollContentToTop = () => {
|
||||
this.modalContent.scrollTop = 0;
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { title, children, modalStyle, hasButtons, handleClose } = this.props;
|
||||
return (
|
||||
<div
|
||||
className="Modal"
|
||||
style={modalStyle}
|
||||
role="dialog"
|
||||
aria-labelledby="Modal-header-title"
|
||||
ref={div => {
|
||||
this.modal = div as HTMLElement;
|
||||
}}
|
||||
>
|
||||
{title && (
|
||||
<div className="Modal-header flex-wrapper">
|
||||
<h2 className="Modal-header-title">{title}</h2>
|
||||
<div className="flex-spacer" />
|
||||
<button className="Modal-header-close" aria-label="Close" onClick={handleClose}>
|
||||
<img className="Modal-header-close-icon" src={closeIcon} alt="Close" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="Modal-content" ref={div => (this.modalContent = div as HTMLElement)}>
|
||||
{children}
|
||||
<div className="Modal-fade" />
|
||||
</div>
|
||||
{hasButtons && <div className="Modal-footer">{this.renderButtons()}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderButtons = () => {
|
||||
const { disableButtons, buttons } = this.props;
|
||||
if (!buttons || !buttons.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return buttons.map((btn, idx: number) => {
|
||||
let btnClass = 'Modal-footer-btn btn';
|
||||
|
||||
if (btn.type) {
|
||||
btnClass += ` btn-${btn.type}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={btnClass}
|
||||
onClick={btn.onClick}
|
||||
key={idx}
|
||||
disabled={disableButtons || btn.disabled}
|
||||
>
|
||||
{btn.text}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
private keyDownListener = (e: KeyboardEvent) => {
|
||||
// Check for TAB key press
|
||||
if (e.keyCode === 9) {
|
||||
// SHIFT + TAB
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === this.firstTabStop) {
|
||||
e.preventDefault();
|
||||
this.lastTabStop.focus();
|
||||
}
|
||||
|
||||
// TAB
|
||||
} else {
|
||||
if (document.activeElement === this.lastTabStop) {
|
||||
e.preventDefault();
|
||||
this.firstTabStop.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for ESC key press
|
||||
if (e.keyCode === 27) {
|
||||
this.focusedElementBeforeModal.focus();
|
||||
this.props.handleClose();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -13,17 +13,6 @@ $m-footer-padding: 0.5rem 2rem 1rem 2rem;
|
|||
$m-close-size: 26px;
|
||||
$m-anim-speed: 400ms;
|
||||
|
||||
.Modalshade {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(#000, 0.54);
|
||||
z-index: $zindex-modal-background;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
position: fixed;
|
||||
top: $m-window-padding-h;
|
||||
|
@ -45,6 +34,17 @@ $m-anim-speed: 400ms;
|
|||
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14),
|
||||
0px 6px 30px 5px rgba(0, 0, 0, 0.12);
|
||||
|
||||
&-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(#000, 0.54);
|
||||
z-index: $zindex-modal-background;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&-fade {
|
||||
background: linear-gradient(to bottom, #fff0, #fff);
|
||||
position: fixed;
|
||||
|
@ -112,7 +112,7 @@ $m-anim-speed: 400ms;
|
|||
}
|
||||
|
||||
// Mobile styles
|
||||
@media(max-width: $screen-sm) {
|
||||
@media (max-width: $screen-sm) {
|
||||
top: $m-window-padding-h-mobile;
|
||||
width: calc(100% - #{$m-window-padding-w-mobile}) !important;
|
||||
max-width: calc(100% - #{$m-window-padding-w-mobile * 2});
|
|
@ -0,0 +1,70 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
import ModalBody from './ModalBody';
|
||||
import './index.scss';
|
||||
|
||||
export interface IButton {
|
||||
text: string | React.ReactElement<string>;
|
||||
type?: 'default' | 'primary' | 'success' | 'info' | 'warning' | 'danger' | 'link';
|
||||
disabled?: boolean;
|
||||
onClick?(): void;
|
||||
}
|
||||
interface Props {
|
||||
isOpen?: boolean;
|
||||
title?: string;
|
||||
disableButtons?: boolean;
|
||||
children: any;
|
||||
buttons?: IButton[];
|
||||
maxWidth?: number;
|
||||
handleClose(): void;
|
||||
}
|
||||
interface ModalStyle {
|
||||
width?: string;
|
||||
maxWidth?: string;
|
||||
}
|
||||
|
||||
const Fade = ({ children, ...props }: any) => (
|
||||
<CSSTransition {...props} timeout={300} classNames="animate-modal">
|
||||
{children}
|
||||
</CSSTransition>
|
||||
);
|
||||
|
||||
export default class Modal extends PureComponent<Props, {}> {
|
||||
public modalBody: ModalBody;
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.isOpen !== this.props.isOpen) {
|
||||
document.body.classList.toggle('no-scroll', !!this.props.isOpen);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.body.classList.remove('no-scroll');
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isOpen, title, children, buttons, handleClose, maxWidth } = this.props;
|
||||
const hasButtons = buttons && buttons.length;
|
||||
const modalStyle: ModalStyle = {};
|
||||
|
||||
if (maxWidth) {
|
||||
modalStyle.width = '100%';
|
||||
modalStyle.maxWidth = `${maxWidth}px`;
|
||||
}
|
||||
|
||||
const modalBodyProps = { title, children, modalStyle, hasButtons, buttons, handleClose };
|
||||
|
||||
return (
|
||||
<TransitionGroup>
|
||||
{isOpen && (
|
||||
<Fade>
|
||||
<div>
|
||||
<div className="Modal-overlay" onClick={handleClose} />
|
||||
<ModalBody {...modalBodyProps} ref={div => (this.modalBody = div as ModalBody)} />
|
||||
</div>
|
||||
</Fade>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ const OfflineSymbol = ({ offline, size }: OfflineSymbolProps) => {
|
|||
break;
|
||||
}
|
||||
|
||||
return <img src={offline ? wifiOff : wifiOn} width={width} height={height} />;
|
||||
return <img src={offline ? wifiOff : wifiOn} alt="wifi status" width={width} height={height} />;
|
||||
};
|
||||
|
||||
export default OfflineSymbol;
|
||||
|
|
|
@ -35,6 +35,7 @@ export default class QRCode extends React.PureComponent<Props, State> {
|
|||
return (
|
||||
<img
|
||||
src={qr}
|
||||
alt="QR Code"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
|
|
|
@ -11,7 +11,7 @@ interface SpinnerProps {
|
|||
const Spinner = ({ size = 'x1', light = false }: SpinnerProps) => {
|
||||
const color = light ? 'Spinner-light' : 'Spinner-dark';
|
||||
return (
|
||||
<svg className={`Spinner Spinner-${size} ${color}`} viewBox="0 0 50 50">
|
||||
<svg className={`Spinner Spinner-${size} ${color}`} viewBox="0 0 50 50" aria-busy="true">
|
||||
<circle className="path" cx="25" cy="25" r="20" fill="none" strokeWidth="5" />
|
||||
</svg>
|
||||
);
|
||||
|
|
|
@ -8,9 +8,6 @@
|
|||
padding: 0.4rem 1rem;
|
||||
border-radius: 2px;
|
||||
height: 2.5rem;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:active,
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
|
|
|
@ -18,7 +18,7 @@ interface Props<T> {
|
|||
const ValueComp: React.SFC = (props: any) => {
|
||||
return (
|
||||
<div className={`${props.className} swap-option-wrapper`}>
|
||||
<img src={props.value.img} className="swap-option-img" />
|
||||
<img src={props.value.img} className="swap-option-img" alt={props.value.label + ' logo'} />
|
||||
<span className="swap-option-label">{props.value.label}</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -46,7 +46,7 @@ const OptionComp: React.SFC = (props: any) => {
|
|||
onMouseEnter={handleMouseEnter}
|
||||
onMouseMove={handleMouseMove}
|
||||
>
|
||||
<img src={props.option.img} className="swap-option-img" />
|
||||
<img src={props.option.img} className="swap-option-img" alt={props.option.label + ' logo'} />
|
||||
<span className="swap-option-label">{props.option.label}</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -18,8 +18,9 @@ export const ANNOUNCEMENT_TYPE = '';
|
|||
export const ANNOUNCEMENT_MESSAGE = (
|
||||
<React.Fragment>
|
||||
This is a Beta version of MyCrypto. Please submit any bug reports to our{' '}
|
||||
<NewTabLink href="https://github.com/MyCryptoHQ/MyCrypto/issues">GitHub</NewTabLink>, and join
|
||||
the discussion on <NewTabLink href={discordURL}>Discord</NewTabLink>.
|
||||
<NewTabLink href="https://github.com/MyCryptoHQ/MyCrypto/issues">GitHub</NewTabLink> and use{' '}
|
||||
<NewTabLink href="https://hackerone.com/mycrypto">HackerOne</NewTabLink> for critical
|
||||
vulnerabilities. Join the discussion on <NewTabLink href={discordURL}>Discord</NewTabLink>.
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
|
|
|
@ -120,7 +120,11 @@ class OnboardModal extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<div className="OnboardModal">
|
||||
<Modal isOpen={isOpen} buttons={buttons} ref={el => (this.modal = el)}>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
buttons={buttons}
|
||||
handleClose={() => (slideNumber === NUMBER_OF_SLIDES ? this.closeModal : null)}
|
||||
>
|
||||
<div className="OnboardModal-stepper">
|
||||
<Stepper
|
||||
steps={steps}
|
||||
|
@ -171,7 +175,7 @@ class OnboardModal extends React.Component<Props, State> {
|
|||
localStorage.setItem(ONBOARD_LOCAL_STORAGE_KEY, String(prevSlideNum));
|
||||
this.props.decrementSlide();
|
||||
if (this.modal) {
|
||||
this.modal.scrollContentToTop();
|
||||
this.modal.modalBody.scrollContentToTop();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -180,7 +184,7 @@ class OnboardModal extends React.Component<Props, State> {
|
|||
localStorage.setItem(ONBOARD_LOCAL_STORAGE_KEY, String(nextSlideNum));
|
||||
this.props.incrementSlide();
|
||||
if (this.modal) {
|
||||
this.modal.scrollContentToTop();
|
||||
this.modal.modalBody.scrollContentToTop();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ interface Props {
|
|||
export class Notifications extends React.Component<Props, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<TransitionGroup className="Notifications">
|
||||
<TransitionGroup className="Notifications" aria-live="polite">
|
||||
{this.props.notifications.map(n => {
|
||||
return (
|
||||
<CSSTransition classNames="NotificationAnimation" timeout={500} key={n.id}>
|
||||
|
|
|
@ -33,10 +33,12 @@ class NameInput extends Component<Props, State> {
|
|||
return (
|
||||
<form className="ENSInput" onSubmit={this.onSubmit}>
|
||||
<div className="input-group-wrapper">
|
||||
<label className="input-group input-group-inline-dropdown ENSInput-name">
|
||||
<label className="input-group input-group-inline ENSInput-name">
|
||||
<Input
|
||||
value={domainToCheck}
|
||||
className={!domainToCheck ? '' : isValidDomain ? 'is-valid' : 'is-invalid'}
|
||||
className={`${
|
||||
!domainToCheck ? '' : isValidDomain ? '' : 'invalid'
|
||||
} border-rad-right-0`}
|
||||
type="text"
|
||||
placeholder="mycrypto"
|
||||
onChange={this.onChange}
|
||||
|
|
|
@ -53,7 +53,11 @@ const CryptoWarning: React.SFC<{}> = () => (
|
|||
className="CryptoWarning-browsers-browser"
|
||||
>
|
||||
<div>
|
||||
<img className="CryptoWarning-browsers-browser-icon" src={browser.icon} />
|
||||
<img
|
||||
className="CryptoWarning-browsers-browser-icon"
|
||||
src={browser.icon}
|
||||
alt={browser.name + ' logo'}
|
||||
/>
|
||||
<div className="CryptoWarning-browsers-browser-name">{browser.name}</div>
|
||||
</div>
|
||||
</NewTabLink>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "common/sass/variables";
|
||||
@import 'common/sass/variables';
|
||||
|
||||
.GenPaper {
|
||||
&-title {
|
||||
|
@ -6,8 +6,17 @@
|
|||
}
|
||||
|
||||
&-private {
|
||||
max-width: 700px;
|
||||
max-width: 680px;
|
||||
margin: 0 auto $space * 3;
|
||||
margin-top: 12px;
|
||||
> .input-group-header {
|
||||
margin-bottom: 1rem;
|
||||
// This selector is an exception, it targets the span returned using `translate`.
|
||||
> span {
|
||||
font-size: 2rem;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-paper,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import PrintableWallet from 'components/PrintableWallet';
|
||||
import { IV3Wallet } from 'ethereumjs-wallet';
|
||||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import { stripHexPrefix } from 'libs/values';
|
||||
import './PaperWallet.scss';
|
||||
import Template from '../Template';
|
||||
|
@ -17,18 +17,20 @@ const PaperWallet: React.SFC<Props> = props => (
|
|||
<Template>
|
||||
<div className="GenPaper">
|
||||
{/* Private Key */}
|
||||
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
|
||||
<label className="input-group GenPaper-private">
|
||||
{/* translateRaw isn't used here because it wont properly render the ` characters as a string of code in markdown*/}
|
||||
<h1 className="input-group-header">{translate('GEN_Label_5')}</h1>
|
||||
<Input
|
||||
className="GenPaper-private"
|
||||
value={stripHexPrefix(props.privateKey)}
|
||||
aria-label={translate('x_PrivKey', true)}
|
||||
aria-label={translateRaw('x_PrivKey')}
|
||||
aria-describedby="x_PrivKeyDesc"
|
||||
type="text"
|
||||
readOnly={true}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* Download Paper Wallet */}
|
||||
<h1 className="GenPaper-title">{translate('x_Print')}</h1>
|
||||
<h2 className="GenPaper-title">{translate('x_Print')}</h2>
|
||||
<div className="GenPaper-paper">
|
||||
<PrintableWallet address={props.keystore.address} privateKey={props.privateKey} />
|
||||
</div>
|
||||
|
|
|
@ -28,10 +28,10 @@ export default class MnemonicWord extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<div className="input-group-wrapper MnemonicWord">
|
||||
<label className="input-group input-group-inline-dropdown ENSInput-name">
|
||||
<label className="input-group input-group-inline ENSInput-name">
|
||||
<span className="input-group-addon input-group-addon--transparent">{index + 1}.</span>
|
||||
<Input
|
||||
className={classnames('MnemonicWord-word-input', word === value && 'valid')}
|
||||
className={`MnemonicWord-word-input ${!isReadOnly && 'border-rad-right-0'}`}
|
||||
value={readOnly ? word : value}
|
||||
onChange={this.handleChange}
|
||||
readOnly={readOnly}
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
top: 36px;
|
||||
left: 30px;
|
||||
opacity: 0.3;
|
||||
outline: none;
|
||||
color: $text-color;
|
||||
|
||||
@media (max-width: $screen-sm) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { UnlockHeader } from 'components/ui';
|
|||
import { SideBar } from './components/index';
|
||||
import { getWalletInst } from 'selectors/wallet';
|
||||
import { AppState } from 'reducers';
|
||||
import { RouteComponentProps, Route, Switch } from 'react-router';
|
||||
import { RouteComponentProps, Route, Switch, Redirect } from 'react-router';
|
||||
import { RedirectWithQuery } from 'components/RedirectWithQuery';
|
||||
import {
|
||||
WalletInfo,
|
||||
|
@ -74,7 +74,13 @@ class SendTransaction extends React.Component<Props> {
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
<Route exact={true} path={`${currentPath}/send`} component={Send} />
|
||||
<Route
|
||||
exact={true}
|
||||
path={`${currentPath}/send`}
|
||||
render={() => {
|
||||
return wallet.isReadOnly ? <Redirect to={`${currentPath}/info`} /> : <Send />;
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={`${currentPath}/info`}
|
||||
exact={true}
|
||||
|
|
|
@ -341,7 +341,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
|
|||
<div className="flex-spacer" />
|
||||
<div className="input-group-wrapper">
|
||||
<div className="input-group-header">Deposit</div>
|
||||
<label className="input-group input-group-inline-dropdown">
|
||||
<label className="input-group input-group-inline">
|
||||
<Input
|
||||
id="origin-swap-input"
|
||||
className={`input-group-input ${
|
||||
|
@ -365,7 +365,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
|
|||
</div>
|
||||
|
||||
<div className="input-group-wrapper">
|
||||
<label className="input-group input-group-inline-dropdown">
|
||||
<label className="input-group input-group-inline">
|
||||
<div className="input-group-header">Recieve</div>
|
||||
<Input
|
||||
id="destination-swap-input"
|
||||
|
|
|
@ -132,7 +132,7 @@ class CurrentRates extends PureComponent<Props> {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img src={providerLogo} width={120} height={49} />
|
||||
<img src={providerLogo} width={120} height={49} alt="Shapeshift Logo" />
|
||||
</a>
|
||||
</section>
|
||||
</article>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { RestartSwapAction } from 'actions/swap';
|
||||
import bityLogo from 'assets/images/logo-bity.svg';
|
||||
import shapeshiftLogo from 'assets/images/shapeshift-dark.svg';
|
||||
import { bityReferralURL } from 'config';
|
||||
import { shapeshiftReferralURL, bitboxReferralURL } from 'config';
|
||||
import React, { PureComponent } from 'react';
|
||||
import translate from 'translations';
|
||||
import './SwapInfoHeader.scss';
|
||||
|
@ -29,11 +29,11 @@ export default class SwapInfoHeaderTitle extends PureComponent<SwapInfoHeaderTit
|
|||
<div className="col-xs-3">
|
||||
<a
|
||||
className="SwapInfo-top-logo"
|
||||
href={bityReferralURL}
|
||||
href={provider === 'shapeshift' ? shapeshiftReferralURL : bitboxReferralURL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img className="SwapInfo-top-logo-img" src={logoToRender} />
|
||||
<img className="SwapInfo-top-logo-img" src={logoToRender} alt={provider + ' logo'} />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -39,3 +39,8 @@
|
|||
[data-whatintent='mouse'] *:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// This is fine because the outline effect is reproduced with border and box-shadow styles on input elements
|
||||
input {
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,19 @@
|
|||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
&-1 {
|
||||
flex-grow: 1;
|
||||
}
|
||||
&-2 {
|
||||
flex-grow: 2;
|
||||
}
|
||||
&-3 {
|
||||
flex-grow: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-spacer {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
|
|
@ -8,9 +8,3 @@
|
|||
.btn-group > .btn-group {
|
||||
float: left;
|
||||
}
|
||||
|
||||
// On active and open, don't show outline
|
||||
.btn-group .dropdown-toggle:active,
|
||||
.btn-group.open .dropdown-toggle {
|
||||
outline: 0;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ input[readonly] {
|
|||
|
||||
&:focus {
|
||||
border-color: $input-border-focus;
|
||||
outline: 0;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba($brand-primary, 0.5);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,9 @@
|
|||
border-bottom-right-radius: 2px;
|
||||
border-color: inherit;
|
||||
}
|
||||
&-menu {
|
||||
max-height: 8.625rem;
|
||||
}
|
||||
&.invalid.has-blurred {
|
||||
border-color: $brand-danger;
|
||||
box-shadow: inset 0px 0px 0px 1px $brand-danger;
|
||||
|
|
|
@ -56,12 +56,7 @@ const nonStandardTransaction = (state: AppState): boolean => {
|
|||
const getGasCost = (state: AppState) => {
|
||||
const gasPrice = getGasPrice(state);
|
||||
const gasLimit = getGasLimit(state);
|
||||
if (!gasLimit.value) {
|
||||
return Wei('0');
|
||||
}
|
||||
const cost = gasLimit.value.mul(gasPrice.value);
|
||||
|
||||
return cost;
|
||||
return gasLimit.value ? gasPrice.value.mul(gasLimit.value) : Wei('0');
|
||||
};
|
||||
|
||||
const serializedAndTransactionFieldsMatch = (state: AppState, isLocallySigned: boolean) => {
|
||||
|
|
44
package.json
44
package.json
|
@ -18,7 +18,7 @@
|
|||
"electron-updater": "2.21.0",
|
||||
"ethereum-blockies": "git+https://github.com/MyCryptoHQ/blockies.git",
|
||||
"ethereumjs-abi": "0.6.5",
|
||||
"ethereumjs-tx": "1.3.3",
|
||||
"ethereumjs-tx": "1.3.4",
|
||||
"ethereumjs-util": "5.1.5",
|
||||
"ethereumjs-wallet": "0.6.0",
|
||||
"font-awesome": "4.7.0",
|
||||
|
@ -37,7 +37,7 @@
|
|||
"react": "16.2.0",
|
||||
"react-copy-to-clipboard": "5.0.1",
|
||||
"react-dom": "16.2.0",
|
||||
"react-markdown": "3.2.2",
|
||||
"react-markdown": "3.3.0",
|
||||
"react-redux": "5.0.7",
|
||||
"react-router-dom": "4.2.2",
|
||||
"react-router-redux": "4.0.8",
|
||||
|
@ -49,7 +49,7 @@
|
|||
"redux-saga": "0.16.0",
|
||||
"scryptsy": "2.0.0",
|
||||
"uuid": "3.2.1",
|
||||
"wallet-address-validator": "0.1.2",
|
||||
"wallet-address-validator": "0.1.3",
|
||||
"whatwg-fetch": "2.0.3",
|
||||
"zxcvbn": "4.4.2"
|
||||
},
|
||||
|
@ -70,7 +70,7 @@
|
|||
"@types/react-redux": "5.0.15",
|
||||
"@types/react-router-dom": "4.2.4",
|
||||
"@types/react-router-redux": "5.0.12",
|
||||
"@types/react-select": "1.2.3",
|
||||
"@types/react-select": "1.2.4",
|
||||
"@types/react-transition-group": "2.0.7",
|
||||
"@types/redux-logger": "3.0.5",
|
||||
"@types/uuid": "3.4.3",
|
||||
|
@ -83,10 +83,10 @@
|
|||
"cache-loader": "1.2.2",
|
||||
"check-node-version": "3.2.0",
|
||||
"concurrently": "3.5.1",
|
||||
"copy-webpack-plugin": "4.5.0",
|
||||
"copy-webpack-plugin": "4.5.1",
|
||||
"css-loader": "0.28.10",
|
||||
"electron": "1.8.2",
|
||||
"electron-builder": "20.2.1",
|
||||
"electron": "1.8.3",
|
||||
"electron-builder": "20.4.0",
|
||||
"empty": "0.10.1",
|
||||
"enzyme": "3.3.0",
|
||||
"enzyme-adapter-react-16": "1.1.1",
|
||||
|
@ -104,7 +104,7 @@
|
|||
"jest": "22.1.4",
|
||||
"klaw-sync": "3.0.2",
|
||||
"less": "2.7.3",
|
||||
"less-loader": "4.0.6",
|
||||
"less-loader": "4.1.0",
|
||||
"lint-staged": "7.0.0",
|
||||
"minimist": "1.2.0",
|
||||
"node-sass": "4.7.2",
|
||||
|
@ -119,7 +119,7 @@
|
|||
"resolve-url-loader": "2.3.0",
|
||||
"rimraf": "2.6.2",
|
||||
"sass-loader": "6.0.7",
|
||||
"style-loader": "0.20.2",
|
||||
"style-loader": "0.20.3",
|
||||
"thread-loader": "1.1.5",
|
||||
"ts-jest": "22.4.1",
|
||||
"ts-loader": "3.3.1",
|
||||
|
@ -130,7 +130,7 @@
|
|||
"types-rlp": "0.0.1",
|
||||
"typescript": "2.6.2",
|
||||
"url-loader": "1.0.1",
|
||||
"url-search-params-polyfill": "2.0.3",
|
||||
"url-search-params-polyfill": "3.0.0",
|
||||
"webpack": "3.11.0",
|
||||
"webpack-dev-middleware": "2.0.6",
|
||||
"webpack-hot-middleware": "2.21.0",
|
||||
|
@ -147,14 +147,10 @@
|
|||
"prebuild": "check-node-version --package",
|
||||
"build:downloadable": "webpack --config webpack_config/webpack.html.js",
|
||||
"prebuild:downloadable": "check-node-version --package",
|
||||
"build:electron":
|
||||
"webpack --config webpack_config/webpack.electron-prod.js && node webpack_config/buildElectron.js",
|
||||
"build:electron:osx":
|
||||
"webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=osx node webpack_config/buildElectron.js",
|
||||
"build:electron:windows":
|
||||
"webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=windows node webpack_config/buildElectron.js",
|
||||
"build:electron:linux":
|
||||
"webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=linux node webpack_config/buildElectron.js",
|
||||
"build:electron": "webpack --config webpack_config/webpack.electron-prod.js && node webpack_config/buildElectron.js",
|
||||
"build:electron:osx": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=osx node webpack_config/buildElectron.js",
|
||||
"build:electron:windows": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=windows node webpack_config/buildElectron.js",
|
||||
"build:electron:linux": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=linux node webpack_config/buildElectron.js",
|
||||
"prebuild:electron": "check-node-version --package",
|
||||
"test:coverage": "jest --config=jest_config/jest.config.json --coverage",
|
||||
"test": "jest --config=jest_config/jest.config.json",
|
||||
|
@ -166,18 +162,14 @@
|
|||
"predev": "check-node-version --package",
|
||||
"dev:https": "HTTPS=true node webpack_config/devServer.js",
|
||||
"predev:https": "check-node-version --package",
|
||||
"dev:electron":
|
||||
"concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true node webpack_config/devServer.js' 'webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
|
||||
"dev:electron:https":
|
||||
"concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true HTTPS=true node webpack_config/devServer.js' 'HTTPS=true webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
|
||||
"dev:electron": "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true node webpack_config/devServer.js' 'webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
|
||||
"dev:electron:https": "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true HTTPS=true node webpack_config/devServer.js' 'HTTPS=true webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
|
||||
"tslint": "tslint --project . --exclude common/vendor/**/*",
|
||||
"tscheck": "tsc --noEmit",
|
||||
"start": "npm run dev",
|
||||
"precommit": "lint-staged",
|
||||
"formatAll":
|
||||
"find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override",
|
||||
"prettier:diff":
|
||||
"prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"",
|
||||
"formatAll": "find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override",
|
||||
"prettier:diff": "prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"",
|
||||
"prepush": "npm run tslint && npm run tscheck"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import { INITIAL_STATE } from 'reducers/transaction';
|
||||
import { broadcast, ITransactionStatus } from 'reducers/transaction/broadcast';
|
||||
import * as txActions from 'actions/transaction';
|
||||
|
||||
const indexingHash = 'testingHash';
|
||||
|
||||
describe('broadcast reducer', () => {
|
||||
const serializedTransaction = new Buffer('testSerialized');
|
||||
const nextTxStatus: ITransactionStatus = {
|
||||
broadcastedHash: null,
|
||||
broadcastSuccessful: false,
|
||||
isBroadcasting: true,
|
||||
serializedTransaction
|
||||
};
|
||||
const nextState: any = {
|
||||
...INITIAL_STATE,
|
||||
[indexingHash]: nextTxStatus
|
||||
};
|
||||
it('should handle BROADCAST_TRANSACTION_QUEUED', () => {
|
||||
expect(
|
||||
broadcast(
|
||||
INITIAL_STATE as any,
|
||||
txActions.broadcastTransactionQueued({ indexingHash, serializedTransaction })
|
||||
)
|
||||
).toEqual(nextState);
|
||||
});
|
||||
|
||||
it('should handle BROADCAST_TRANSACTION_SUCCESS', () => {
|
||||
const broadcastedHash = 'testBroadcastHash';
|
||||
const broadcastedState = {
|
||||
...nextState,
|
||||
[indexingHash]: {
|
||||
...nextTxStatus,
|
||||
broadcastedHash,
|
||||
isBroadcasting: false,
|
||||
broadcastSuccessful: true
|
||||
}
|
||||
};
|
||||
expect(
|
||||
broadcast(
|
||||
nextState,
|
||||
txActions.broadcastTransactionSucceeded({ indexingHash, broadcastedHash })
|
||||
)
|
||||
).toEqual(broadcastedState);
|
||||
});
|
||||
|
||||
it('should handle BROADCAST_TRANSACTION_FAILURE', () => {
|
||||
const failedBroadcastState = {
|
||||
...nextState,
|
||||
[indexingHash]: { ...nextTxStatus, isBroadcasting: false, broadcastSuccessful: false }
|
||||
};
|
||||
expect(broadcast(nextState, txActions.broadcastTransactionFailed({ indexingHash }))).toEqual(
|
||||
failedBroadcastState
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,122 @@
|
|||
import { TypeKeys } from 'actions/transaction/constants';
|
||||
import { gasPricetoBase } from 'libs/units';
|
||||
import { fields, State } from 'reducers/transaction/fields';
|
||||
import * as txActions from 'actions/transaction';
|
||||
import BN from 'bn.js';
|
||||
|
||||
describe('fields reducer', () => {
|
||||
const INITIAL_STATE: State = {
|
||||
to: { raw: '', value: null },
|
||||
data: { raw: '', value: null },
|
||||
nonce: { raw: '', value: null },
|
||||
value: { raw: '', value: null },
|
||||
gasLimit: { raw: '21000', value: new BN(21000) },
|
||||
gasPrice: { raw: '20', value: gasPricetoBase(20) }
|
||||
};
|
||||
const testPayload = { raw: 'test', value: null };
|
||||
|
||||
it('should handle TO_FIELD_SET', () => {
|
||||
expect(fields(INITIAL_STATE, txActions.setToField(testPayload))).toEqual({
|
||||
...INITIAL_STATE,
|
||||
to: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle VALUE_FIELD_SET', () => {
|
||||
expect(fields(INITIAL_STATE, txActions.setValueField(testPayload))).toEqual({
|
||||
...INITIAL_STATE,
|
||||
value: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle DATA_FIELD_SET', () => {
|
||||
expect(fields(INITIAL_STATE, txActions.setDataField(testPayload))).toEqual({
|
||||
...INITIAL_STATE,
|
||||
data: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle GAS_LIMIT_FIELD_SET', () => {
|
||||
expect(fields(INITIAL_STATE, txActions.setGasLimitField(testPayload))).toEqual({
|
||||
...INITIAL_STATE,
|
||||
gasLimit: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle NONCE_SET', () => {
|
||||
expect(fields(INITIAL_STATE, txActions.setNonceField(testPayload))).toEqual({
|
||||
...INITIAL_STATE,
|
||||
nonce: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle GAS_PRICE_FIELD_SET', () => {
|
||||
expect(fields(INITIAL_STATE, txActions.setGasPriceField(testPayload))).toEqual({
|
||||
...INITIAL_STATE,
|
||||
gasPrice: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle TOKEN_TO_ETHER_SWAP', () => {
|
||||
const swapAction: txActions.SwapTokenToEtherAction = {
|
||||
type: TypeKeys.TOKEN_TO_ETHER_SWAP,
|
||||
payload: {
|
||||
to: testPayload,
|
||||
value: testPayload,
|
||||
decimal: 1
|
||||
}
|
||||
};
|
||||
expect(fields(INITIAL_STATE, swapAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
to: testPayload,
|
||||
value: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ETHER_TO_TOKEN_SWAP', () => {
|
||||
const swapAction: txActions.SwapEtherToTokenAction = {
|
||||
type: TypeKeys.ETHER_TO_TOKEN_SWAP,
|
||||
payload: {
|
||||
to: testPayload,
|
||||
data: testPayload,
|
||||
tokenTo: testPayload,
|
||||
tokenValue: testPayload,
|
||||
decimal: 1
|
||||
}
|
||||
};
|
||||
expect(fields(INITIAL_STATE, swapAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
to: testPayload,
|
||||
data: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle TOKEN_TO_TOKEN_SWAP', () => {
|
||||
const swapAction: txActions.SwapTokenToTokenAction = {
|
||||
type: TypeKeys.TOKEN_TO_TOKEN_SWAP,
|
||||
payload: {
|
||||
to: testPayload,
|
||||
data: testPayload,
|
||||
tokenValue: testPayload,
|
||||
decimal: 1
|
||||
}
|
||||
};
|
||||
expect(fields(INITIAL_STATE, swapAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
to: testPayload,
|
||||
data: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset', () => {
|
||||
const resetAction: txActions.ResetAction = {
|
||||
type: TypeKeys.RESET,
|
||||
payload: { include: {}, exclude: {} }
|
||||
};
|
||||
const modifiedState: State = {
|
||||
...INITIAL_STATE,
|
||||
data: { raw: 'modified', value: null }
|
||||
};
|
||||
expect(fields(modifiedState, resetAction)).toEqual(INITIAL_STATE);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
import { TypeKeys } from 'actions/transaction/constants';
|
||||
import { getDecimalFromEtherUnit } from 'libs/units';
|
||||
import { State, meta } from 'reducers/transaction/meta';
|
||||
import * as txActions from 'actions/transaction';
|
||||
|
||||
describe('meta reducer', () => {
|
||||
const INITIAL_STATE: State = {
|
||||
unit: '',
|
||||
previousUnit: '',
|
||||
decimal: getDecimalFromEtherUnit('ether'),
|
||||
tokenValue: { raw: '', value: null },
|
||||
tokenTo: { raw: '', value: null },
|
||||
from: null
|
||||
};
|
||||
|
||||
const testPayload = { raw: 'test', value: null };
|
||||
|
||||
it('should handle UNIT_META_SET', () => {
|
||||
const setUnitMetaAction: txActions.SetUnitMetaAction = {
|
||||
type: TypeKeys.UNIT_META_SET,
|
||||
payload: 'test'
|
||||
};
|
||||
expect(meta(INITIAL_STATE, setUnitMetaAction));
|
||||
});
|
||||
|
||||
it('should handle TOKEN_VALUE_META_SET', () => {
|
||||
expect(meta(INITIAL_STATE, txActions.setTokenValue(testPayload))).toEqual({
|
||||
...INITIAL_STATE,
|
||||
tokenValue: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle TOKEN_TO_META_SET', () => {
|
||||
expect(meta(INITIAL_STATE, txActions.setTokenTo(testPayload))).toEqual({
|
||||
...INITIAL_STATE,
|
||||
tokenTo: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle GET_FROM_SUCCEEDED', () => {
|
||||
expect(meta(INITIAL_STATE, txActions.getFromSucceeded('test'))).toEqual({
|
||||
...INITIAL_STATE,
|
||||
from: 'test'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle TOKEN_TO_ETHER_SWAP', () => {
|
||||
const swapAction: txActions.SwapTokenToEtherAction = {
|
||||
type: TypeKeys.TOKEN_TO_ETHER_SWAP,
|
||||
payload: {
|
||||
to: testPayload,
|
||||
value: testPayload,
|
||||
decimal: 1
|
||||
}
|
||||
};
|
||||
expect(meta(INITIAL_STATE, swapAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
decimal: swapAction.payload.decimal
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ETHER_TO_TOKEN_SWAP', () => {
|
||||
const swapAction: txActions.SwapEtherToTokenAction = {
|
||||
type: TypeKeys.ETHER_TO_TOKEN_SWAP,
|
||||
payload: {
|
||||
to: testPayload,
|
||||
data: testPayload,
|
||||
tokenTo: testPayload,
|
||||
tokenValue: testPayload,
|
||||
decimal: 1
|
||||
}
|
||||
};
|
||||
expect(meta(INITIAL_STATE, swapAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
decimal: swapAction.payload.decimal,
|
||||
tokenTo: testPayload,
|
||||
tokenValue: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle TOKEN_TO_TOKEN_SWAP', () => {
|
||||
const swapAction: txActions.SwapTokenToTokenAction = {
|
||||
type: TypeKeys.TOKEN_TO_TOKEN_SWAP,
|
||||
payload: {
|
||||
to: testPayload,
|
||||
data: testPayload,
|
||||
tokenValue: testPayload,
|
||||
decimal: 1
|
||||
}
|
||||
};
|
||||
expect(meta(INITIAL_STATE, swapAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
decimal: swapAction.payload.decimal,
|
||||
tokenValue: testPayload
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset', () => {
|
||||
const resetAction: txActions.ResetAction = {
|
||||
type: TypeKeys.RESET,
|
||||
payload: { include: {}, exclude: {} }
|
||||
};
|
||||
const modifiedState: State = {
|
||||
...INITIAL_STATE,
|
||||
unit: 'modified'
|
||||
};
|
||||
expect(meta(modifiedState, resetAction)).toEqual(INITIAL_STATE);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
import { State, network } from 'reducers/transaction/network';
|
||||
import * as txActions from 'actions/transaction';
|
||||
import { TypeKeys } from 'actions/transaction/constants';
|
||||
|
||||
describe('network reducer', () => {
|
||||
const INITIAL_STATE: State = {
|
||||
gasEstimationStatus: null,
|
||||
getFromStatus: null,
|
||||
getNonceStatus: null,
|
||||
gasPriceStatus: null
|
||||
};
|
||||
|
||||
it('should handle gas estimation status actions', () => {
|
||||
const gasEstimationAction: txActions.NetworkAction = {
|
||||
type: TypeKeys.ESTIMATE_GAS_SUCCEEDED
|
||||
};
|
||||
expect(network(INITIAL_STATE, gasEstimationAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
gasEstimationStatus: 'SUCCESS'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle get from status actions', () => {
|
||||
const getFromAction: txActions.NetworkAction = {
|
||||
type: TypeKeys.GET_FROM_SUCCEEDED,
|
||||
payload: 'test'
|
||||
};
|
||||
expect(network(INITIAL_STATE, getFromAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
getFromStatus: 'SUCCESS'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle get nonce status actions', () => {
|
||||
const getNonceAction: txActions.NetworkAction = {
|
||||
type: TypeKeys.GET_NONCE_SUCCEEDED,
|
||||
payload: 'test'
|
||||
};
|
||||
expect(network(INITIAL_STATE, getNonceAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
getNonceStatus: 'SUCCESS'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle gasPriceIntent', () => {
|
||||
const gasPriceAction: txActions.InputGasPriceAction = {
|
||||
type: TypeKeys.GAS_PRICE_INPUT,
|
||||
payload: 'test'
|
||||
};
|
||||
expect(network(INITIAL_STATE, gasPriceAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
gasPriceStatus: 'SUCCESS'
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
import EthTx from 'ethereumjs-tx';
|
||||
import * as txActions from 'actions/transaction';
|
||||
import { TypeKeys } from 'actions/transaction/constants';
|
||||
import { State, sign } from 'reducers/transaction/sign';
|
||||
|
||||
describe('sign reducer', () => {
|
||||
const INITIAL_STATE: State = {
|
||||
local: { signedTransaction: null },
|
||||
web3: { transaction: null },
|
||||
indexingHash: null,
|
||||
pending: false
|
||||
};
|
||||
it('should handle SIGN_TRANSACTION_REQUESTED', () => {
|
||||
const signTxRequestedAction: txActions.SignTransactionRequestedAction = {
|
||||
type: TypeKeys.SIGN_TRANSACTION_REQUESTED,
|
||||
payload: {} as EthTx
|
||||
};
|
||||
expect(sign(INITIAL_STATE, signTxRequestedAction)).toEqual({ ...INITIAL_STATE, pending: true });
|
||||
});
|
||||
|
||||
it('should handle SIGN_LOCAL_TRANSACTION_SUCCEEDED', () => {
|
||||
const signedTransaction = new Buffer('test');
|
||||
const indexingHash = 'test';
|
||||
const signLocalTxSucceededAction: txActions.SignLocalTransactionSucceededAction = {
|
||||
type: TypeKeys.SIGN_LOCAL_TRANSACTION_SUCCEEDED,
|
||||
payload: { signedTransaction, indexingHash }
|
||||
};
|
||||
expect(sign(INITIAL_STATE, signLocalTxSucceededAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
pending: false,
|
||||
indexingHash,
|
||||
local: { signedTransaction }
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle SIGN_WEB3_TRANSACTION_SUCCEEDED', () => {
|
||||
const transaction = new Buffer('test');
|
||||
const indexingHash = 'test';
|
||||
const signWeb3TxSucceededAction: txActions.SignWeb3TransactionSucceededAction = {
|
||||
type: TypeKeys.SIGN_WEB3_TRANSACTION_SUCCEEDED,
|
||||
payload: { transaction, indexingHash }
|
||||
};
|
||||
expect(sign(INITIAL_STATE, signWeb3TxSucceededAction)).toEqual({
|
||||
...INITIAL_STATE,
|
||||
pending: false,
|
||||
indexingHash,
|
||||
web3: { transaction }
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset', () => {
|
||||
const resetAction: txActions.ResetAction = {
|
||||
type: TypeKeys.RESET,
|
||||
payload: { include: {}, exclude: {} }
|
||||
};
|
||||
const modifiedState: State = {
|
||||
...INITIAL_STATE,
|
||||
pending: true
|
||||
};
|
||||
expect(sign(modifiedState, resetAction)).toEqual(INITIAL_STATE);
|
||||
});
|
||||
});
|
|
@ -1,46 +1,97 @@
|
|||
import { configuredStore } from 'store';
|
||||
import { getResolvedAddress } from 'selectors/ens';
|
||||
import { Address } from 'libs/units';
|
||||
import { call, select, put } from 'redux-saga/effects';
|
||||
import { call, select, put, take } from 'redux-saga/effects';
|
||||
import { isValidETHAddress, isValidENSAddress } from 'libs/validators';
|
||||
import { setCurrentTo, setField } from 'sagas/transaction/current/currentTo';
|
||||
import { isEtherTransaction } from 'selectors/transaction';
|
||||
import { cloneableGenerator } from 'redux-saga/utils';
|
||||
import { setToField, setTokenTo } from 'actions/transaction';
|
||||
configuredStore.getState();
|
||||
const raw = '0xa';
|
||||
|
||||
const payload = {
|
||||
raw,
|
||||
value: Address(raw)
|
||||
};
|
||||
import { resolveDomainRequested, TypeKeys as ENSTypekeys } from 'actions/ens';
|
||||
|
||||
describe('setCurrentTo*', () => {
|
||||
const action: any = {
|
||||
const data = {} as any;
|
||||
|
||||
describe('with valid Ethereum address', () => {
|
||||
const raw = '0xa';
|
||||
const ethAddrPayload = {
|
||||
raw,
|
||||
value: Address(raw)
|
||||
};
|
||||
const ethAddrAction: any = {
|
||||
payload: raw
|
||||
};
|
||||
const validAddress = true;
|
||||
const validEns = false;
|
||||
|
||||
const gen = setCurrentTo(action);
|
||||
|
||||
data.validEthGen = setCurrentTo(ethAddrAction);
|
||||
it('should call isValidETHAddress', () => {
|
||||
expect(gen.next().value).toEqual(call(isValidETHAddress, raw));
|
||||
expect(data.validEthGen.next().value).toEqual(call(isValidETHAddress, raw));
|
||||
});
|
||||
|
||||
it('should call isValidENSAddress', () => {
|
||||
expect(gen.next(validAddress).value).toEqual(call(isValidENSAddress, raw));
|
||||
expect(data.validEthGen.next(raw).value).toEqual(call(isValidENSAddress, raw));
|
||||
});
|
||||
|
||||
it('should call setField', () => {
|
||||
expect(gen.next(validEns).value).toEqual(call(setField, payload));
|
||||
expect(data.validEthGen.next(raw).value).toEqual(call(setField, ethAddrPayload));
|
||||
});
|
||||
});
|
||||
|
||||
it('should be done', () => {
|
||||
expect(gen.next().done).toEqual(true);
|
||||
describe('with invalid Ethereum address, valid ENS address', () => {
|
||||
const raw = 'testing.eth';
|
||||
const resolvedAddress = '0xa';
|
||||
const [domain] = raw.split('.');
|
||||
const ensAddrPayload = {
|
||||
raw,
|
||||
value: null
|
||||
};
|
||||
const ensAddrAction: any = {
|
||||
payload: raw
|
||||
};
|
||||
data.validEnsGen = setCurrentTo(ensAddrAction);
|
||||
|
||||
it('should call isValidETHAddress', () => {
|
||||
expect(data.validEnsGen.next().value).toEqual(call(isValidETHAddress, raw));
|
||||
});
|
||||
|
||||
it('should call isValidENSAddress', () => {
|
||||
expect(data.validEnsGen.next(false).value).toEqual(call(isValidENSAddress, raw));
|
||||
});
|
||||
|
||||
it('should call setField', () => {
|
||||
expect(data.validEnsGen.next(true).value).toEqual(call(setField, ensAddrPayload));
|
||||
});
|
||||
|
||||
it('should put resolveDomainRequested', () => {
|
||||
expect(data.validEnsGen.next().value).toEqual(put(resolveDomainRequested(domain)));
|
||||
});
|
||||
|
||||
it('should take ENS type keys', () => {
|
||||
expect(data.validEnsGen.next().value).toEqual(
|
||||
take([
|
||||
ENSTypekeys.ENS_RESOLVE_DOMAIN_FAILED,
|
||||
ENSTypekeys.ENS_RESOLVE_DOMAIN_SUCCEEDED,
|
||||
ENSTypekeys.ENS_RESOLVE_DOMAIN_CACHED
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should select getResolvedAddress', () => {
|
||||
expect(data.validEnsGen.next().value).toEqual(select(getResolvedAddress, true));
|
||||
});
|
||||
|
||||
it('should call setField', () => {
|
||||
expect(data.validEnsGen.next(resolvedAddress).value).toEqual(
|
||||
call(setField, { raw, value: Address(resolvedAddress) })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setField', () => {
|
||||
const raw = '0xa';
|
||||
const payload = {
|
||||
raw,
|
||||
value: Address(raw)
|
||||
};
|
||||
const etherTransaction = cloneableGenerator(setField)(payload);
|
||||
it('should select etherTransaction', () => {
|
||||
expect(etherTransaction.next().value).toEqual(select(isEtherTransaction));
|
||||
|
|
|
@ -46,7 +46,6 @@ describe('valueHandler', () => {
|
|||
});
|
||||
it('should select getUnit', () => {
|
||||
gen.invalidDecimal = gen.pass.clone();
|
||||
|
||||
expect(gen.pass.next(decimal).value).toEqual(select(getUnit));
|
||||
expect(gen.invalidNumber.next(decimal).value).toEqual(select(getUnit));
|
||||
expect(gen.invalidDecimal.next(failCases.invalidDecimal).value).toEqual(select(getUnit));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { configuredStore } from 'store';
|
||||
import BN from 'bn.js';
|
||||
import { SagaIterator, delay } from 'redux-saga';
|
||||
import { call, put } from 'redux-saga/effects';
|
||||
import { setDataField, setGasLimitField, setNonceField } from 'actions/transaction/actionCreators';
|
||||
import { isValidHex, isValidNonce, gasPriceValidator, gasLimitValidator } from 'libs/validators';
|
||||
|
@ -8,12 +8,11 @@ import {
|
|||
handleDataInput,
|
||||
handleGasLimitInput,
|
||||
handleNonceInput,
|
||||
handleGasPriceInput
|
||||
handleGasPriceInput,
|
||||
handleGasPriceInputIntent
|
||||
} from 'sagas/transaction/fields/fields';
|
||||
import { cloneableGenerator } from 'redux-saga/utils';
|
||||
import { setGasPriceField } from 'actions/transaction';
|
||||
import { SagaIterator } from 'redux-saga';
|
||||
configuredStore.getState();
|
||||
import { setGasPriceField, inputGasPrice } from 'actions/transaction';
|
||||
|
||||
const itShouldBeDone = (gen: SagaIterator) => {
|
||||
it('should be done', () => {
|
||||
|
@ -142,6 +141,19 @@ describe('handleGasPriceInput*', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('handleGasPriceInputIntent*', () => {
|
||||
const payload = '100.111';
|
||||
const action: any = { payload };
|
||||
const gen = handleGasPriceInputIntent(action);
|
||||
it('should call delay', () => {
|
||||
expect(gen.next().value).toEqual(call(delay, 300));
|
||||
});
|
||||
|
||||
it('should put inputGasPrice', () => {
|
||||
expect(gen.next().value).toEqual(put(inputGasPrice(payload)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleNonceInput*', () => {
|
||||
const payload = '42';
|
||||
const action: any = { payload };
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import { put, apply, call } from 'redux-saga/effects';
|
||||
import { signLocalTransactionSucceeded, signWeb3TransactionSucceeded } from 'actions/transaction';
|
||||
import { computeIndexingHash } from 'libs/transaction';
|
||||
import {
|
||||
signLocalTransactionHandler,
|
||||
signWeb3TransactionHandler
|
||||
} from 'sagas/transaction/signing/signing';
|
||||
|
||||
describe('signLocalTransactionHandler*', () => {
|
||||
const tx = 'tx';
|
||||
const wallet = {
|
||||
signRawTransaction: jest.fn()
|
||||
};
|
||||
const action: any = { tx, wallet };
|
||||
const signedTransaction = new Buffer('signedTransaction');
|
||||
const indexingHash = 'indexingHash';
|
||||
|
||||
const gen = signLocalTransactionHandler(action);
|
||||
|
||||
it('should apply wallet.signRawTransaction', () => {
|
||||
expect(gen.next().value).toEqual(apply(wallet, wallet.signRawTransaction, [tx]));
|
||||
});
|
||||
|
||||
it('should call computeIndexingHash', () => {
|
||||
expect(gen.next(signedTransaction).value).toEqual(call(computeIndexingHash, signedTransaction));
|
||||
});
|
||||
|
||||
it('should put signLocalTransactionSucceeded', () => {
|
||||
expect(gen.next(indexingHash).value).toEqual(
|
||||
put(
|
||||
signLocalTransactionSucceeded({
|
||||
signedTransaction,
|
||||
indexingHash,
|
||||
noVerify: false
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should be done', () => {
|
||||
expect(gen.next().done).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('signWeb3TransactionHandler*', () => {
|
||||
const tx = {
|
||||
serialize: jest.fn
|
||||
};
|
||||
const action: any = { tx };
|
||||
const serializedTransaction = new Buffer('tx');
|
||||
const indexingHash = 'indexingHash';
|
||||
|
||||
const gen = signWeb3TransactionHandler(action);
|
||||
|
||||
it('should apply tx.serialize', () => {
|
||||
expect(gen.next().value).toEqual(apply(tx, tx.serialize));
|
||||
});
|
||||
|
||||
it('should call computeIndexingHash', () => {
|
||||
expect(gen.next(serializedTransaction).value).toEqual(
|
||||
call(computeIndexingHash, serializedTransaction)
|
||||
);
|
||||
});
|
||||
|
||||
it('should put signWeb3TransactionSucceeded', () => {
|
||||
expect(gen.next(indexingHash).value).toEqual(
|
||||
put(
|
||||
signWeb3TransactionSucceeded({
|
||||
transaction: serializedTransaction,
|
||||
indexingHash
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should be done', () => {
|
||||
expect(gen.next().done).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import { configuredStore } from '../../common/store';
|
||||
|
||||
export function getInitialState() {
|
||||
return { ...configuredStore.getState() };
|
||||
}
|
||||
|
||||
export function testShallowlyEqual(oldValue: any, newValue: any) {
|
||||
it('should be shallowly equal when called again with the same state', () => {
|
||||
expect(oldValue === newValue).toBeTruthy();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import {
|
||||
getTransactionStatus,
|
||||
currentTransactionFailed,
|
||||
currentTransactionBroadcasting,
|
||||
currentTransactionBroadcasted,
|
||||
getCurrentTransactionStatus
|
||||
} from 'selectors/transaction';
|
||||
import { getInitialState } from '../helpers';
|
||||
|
||||
describe('broadcast selector', () => {
|
||||
const state = getInitialState();
|
||||
state.transaction = {
|
||||
...state.transaction,
|
||||
broadcast: {
|
||||
...state.transaction.broadcast,
|
||||
testIndexingHash1: {
|
||||
broadcastedHash: 'testBroadcastedHash',
|
||||
broadcastSuccessful: true,
|
||||
isBroadcasting: false,
|
||||
serializedTransaction: new Buffer([1, 2, 3])
|
||||
},
|
||||
testIndexingHash2: {
|
||||
broadcastedHash: 'testBroadcastedHash',
|
||||
broadcastSuccessful: true,
|
||||
isBroadcasting: false,
|
||||
serializedTransaction: new Buffer([1, 2, 3])
|
||||
}
|
||||
},
|
||||
sign: {
|
||||
...state.transaction.sign,
|
||||
indexingHash: 'testIndexingHash1',
|
||||
pending: false
|
||||
}
|
||||
};
|
||||
it('should check getTransactionState with an indexing hash', () => {
|
||||
expect(getTransactionStatus(state, 'testIndexingHash1')).toEqual(
|
||||
state.transaction.broadcast.testIndexingHash1
|
||||
);
|
||||
});
|
||||
|
||||
it('should check getCurrentTransactionStatus', () => {
|
||||
expect(getCurrentTransactionStatus(state)).toEqual(
|
||||
state.transaction.broadcast.testIndexingHash2
|
||||
);
|
||||
});
|
||||
|
||||
it('should check currentTransactionFailed', () => {
|
||||
expect(currentTransactionFailed(state)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should check currentTransactionBroadcasting', () => {
|
||||
expect(currentTransactionBroadcasting(state)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should check currentTransactionBroadcasted', () => {
|
||||
expect(currentTransactionBroadcasted(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false on getCurrentTransactionStatus if no index hash present', () => {
|
||||
state.transaction.sign.indexingHash = null;
|
||||
expect(getCurrentTransactionStatus(state)).toEqual(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import { Wei } from 'libs/units';
|
||||
import {
|
||||
getCurrentValue,
|
||||
getCurrentTo,
|
||||
isEtherTransaction,
|
||||
isValidCurrentTo,
|
||||
isValidGasPrice,
|
||||
isValidGasLimit,
|
||||
getCurrentToAddressMessage
|
||||
} from 'selectors/transaction';
|
||||
import { getInitialState } from '../helpers';
|
||||
|
||||
describe('current selector', () => {
|
||||
const state = getInitialState();
|
||||
state.transaction = {
|
||||
...state.transaction,
|
||||
fields: {
|
||||
...state.transaction.fields,
|
||||
to: {
|
||||
raw: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520',
|
||||
value: new Buffer([0, 1, 2, 3])
|
||||
},
|
||||
gasLimit: {
|
||||
raw: '21000',
|
||||
value: Wei('21000')
|
||||
},
|
||||
gasPrice: {
|
||||
raw: '1500',
|
||||
value: Wei('1500')
|
||||
}
|
||||
},
|
||||
meta: {
|
||||
...state.transaction.meta,
|
||||
unit: 'ETH',
|
||||
previousUnit: 'ETH'
|
||||
}
|
||||
};
|
||||
|
||||
it('should get stored receiver address on getCurrentTo', () => {
|
||||
expect(getCurrentTo(state)).toEqual(state.transaction.fields.to);
|
||||
});
|
||||
|
||||
it('should get stored value on getCurrentValue', () => {
|
||||
expect(getCurrentValue(state)).toEqual(state.transaction.fields.value);
|
||||
});
|
||||
|
||||
it('should get message to the receiver', () => {
|
||||
expect(getCurrentToAddressMessage(state)).toEqual({
|
||||
msg: 'Thank you for donating to MyCrypto. TO THE MOON!'
|
||||
});
|
||||
});
|
||||
|
||||
it('should check isValidGasPrice', () => {
|
||||
expect(isValidGasPrice(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should check isEtherTransaction', () => {
|
||||
expect(isEtherTransaction(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should check isValidGasLimit', () => {
|
||||
expect(isValidGasLimit(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should check isValidCurrentTo', () => {
|
||||
expect(isValidCurrentTo(state)).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
import BN from 'bn.js';
|
||||
import { Wei } from 'libs/units';
|
||||
import {
|
||||
getData,
|
||||
getFields,
|
||||
getGasLimit,
|
||||
getValue,
|
||||
getTo,
|
||||
getNonce,
|
||||
getGasPrice,
|
||||
getDataExists,
|
||||
getValidGasCost
|
||||
} from 'selectors/transaction';
|
||||
import { getInitialState } from '../helpers';
|
||||
|
||||
describe('fields selector', () => {
|
||||
const state = getInitialState();
|
||||
state.transaction.fields = {
|
||||
to: {
|
||||
raw: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520',
|
||||
value: new Buffer([0, 1, 2, 3])
|
||||
},
|
||||
data: {
|
||||
raw: '',
|
||||
value: null
|
||||
},
|
||||
nonce: {
|
||||
raw: '0',
|
||||
value: new BN('0')
|
||||
},
|
||||
value: {
|
||||
raw: '1000000000',
|
||||
value: Wei('1000000000')
|
||||
},
|
||||
gasLimit: {
|
||||
raw: '21000',
|
||||
value: Wei('21000')
|
||||
},
|
||||
gasPrice: {
|
||||
raw: '1500',
|
||||
value: Wei('1500')
|
||||
}
|
||||
};
|
||||
|
||||
it('should get fields from fields store', () => {
|
||||
expect(getFields(state)).toEqual(state.transaction.fields);
|
||||
});
|
||||
|
||||
it('should get data from fields store', () => {
|
||||
expect(getData(state)).toEqual(state.transaction.fields.data);
|
||||
});
|
||||
|
||||
it('should get gas limit from fields store', () => {
|
||||
expect(getGasLimit(state)).toEqual(state.transaction.fields.gasLimit);
|
||||
});
|
||||
|
||||
it('should get value from fields store', () => {
|
||||
expect(getValue(state)).toEqual(state.transaction.fields.value);
|
||||
});
|
||||
|
||||
it('sould get receiver address from fields store', () => {
|
||||
expect(getTo(state)).toEqual(state.transaction.fields.to);
|
||||
});
|
||||
|
||||
it('should get nonce from fields store', () => {
|
||||
expect(getNonce(state)).toEqual(state.transaction.fields.nonce);
|
||||
});
|
||||
|
||||
it('should get gas price from fields store', () => {
|
||||
expect(getGasPrice(state)).toEqual(state.transaction.fields.gasPrice);
|
||||
});
|
||||
|
||||
it('should check getDataExists', () => {
|
||||
expect(getDataExists(state)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should check when gas cost is valid', () => {
|
||||
expect(getValidGasCost(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should check when gas cost is invalid', () => {
|
||||
state.wallet.balance = {
|
||||
wei: Wei('0'),
|
||||
isPending: false
|
||||
};
|
||||
expect(getValidGasCost(state)).toEqual(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
import BN from 'bn.js';
|
||||
import { Wei } from 'libs/units';
|
||||
import { reduceToValues, isFullTx } from 'selectors/transaction/helpers';
|
||||
import {
|
||||
getCurrentTo,
|
||||
getCurrentValue,
|
||||
getFields,
|
||||
getUnit,
|
||||
getDataExists,
|
||||
getValidGasCost
|
||||
} from 'selectors/transaction';
|
||||
import { getInitialState } from '../helpers';
|
||||
|
||||
describe('helpers selector', () => {
|
||||
const state = getInitialState();
|
||||
state.transaction = {
|
||||
...state.transaction,
|
||||
meta: {
|
||||
...state.transaction.meta,
|
||||
unit: 'ETH'
|
||||
},
|
||||
fields: {
|
||||
to: {
|
||||
raw: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520',
|
||||
value: new Buffer([0, 1, 2, 3])
|
||||
},
|
||||
data: {
|
||||
raw: '',
|
||||
value: null
|
||||
},
|
||||
nonce: {
|
||||
raw: '0',
|
||||
value: new BN('0')
|
||||
},
|
||||
value: {
|
||||
raw: '1000000000',
|
||||
value: Wei('1000000000')
|
||||
},
|
||||
gasLimit: {
|
||||
raw: '21000',
|
||||
value: Wei('21000')
|
||||
},
|
||||
gasPrice: {
|
||||
raw: '1500',
|
||||
value: Wei('1500')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it('should reduce the fields state to its base values', () => {
|
||||
const values = {
|
||||
data: null,
|
||||
gasLimit: Wei('21000'),
|
||||
gasPrice: Wei('1500'),
|
||||
nonce: new BN('0'),
|
||||
to: new Buffer([0, 1, 2, 3]),
|
||||
value: Wei('1000000000')
|
||||
};
|
||||
expect(reduceToValues(state.transaction.fields)).toEqual(values);
|
||||
});
|
||||
|
||||
it('should check isFullTransaction with full transaction arguments', () => {
|
||||
const currentTo = getCurrentTo(state);
|
||||
const currentValue = getCurrentValue(state);
|
||||
const transactionFields = getFields(state);
|
||||
const unit = getUnit(state);
|
||||
const dataExists = getDataExists(state);
|
||||
const validGasCost = getValidGasCost(state);
|
||||
const isFullTransaction = isFullTx(
|
||||
state,
|
||||
transactionFields,
|
||||
currentTo,
|
||||
currentValue,
|
||||
dataExists,
|
||||
validGasCost,
|
||||
unit
|
||||
);
|
||||
expect(isFullTransaction).toEqual(true);
|
||||
});
|
||||
|
||||
it('should check isFullTransaction without full transaction arguments', () => {
|
||||
const currentTo = { raw: '', value: null };
|
||||
const currentValue = getCurrentValue(state);
|
||||
const transactionFields = getFields(state);
|
||||
const unit = getUnit(state);
|
||||
const dataExists = getDataExists(state);
|
||||
const validGasCost = getValidGasCost(state);
|
||||
const isFullTransaction = isFullTx(
|
||||
state,
|
||||
transactionFields,
|
||||
currentTo,
|
||||
currentValue,
|
||||
dataExists,
|
||||
validGasCost,
|
||||
unit
|
||||
);
|
||||
expect(isFullTransaction).toEqual(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
getFrom,
|
||||
getDecimal,
|
||||
getTokenValue,
|
||||
getTokenTo,
|
||||
getUnit,
|
||||
getPreviousUnit,
|
||||
getDecimalFromUnit
|
||||
} from 'selectors/transaction/meta';
|
||||
import { getInitialState } from '../helpers';
|
||||
|
||||
describe('meta tests', () => {
|
||||
const state = getInitialState();
|
||||
(state.transaction.meta = {
|
||||
unit: 'ETH',
|
||||
previousUnit: 'ETH',
|
||||
decimal: 18,
|
||||
tokenValue: {
|
||||
raw: '',
|
||||
value: null
|
||||
},
|
||||
tokenTo: {
|
||||
raw: '',
|
||||
value: null
|
||||
},
|
||||
from: 'fromAddress'
|
||||
}),
|
||||
(state.customTokens = [
|
||||
{
|
||||
address: '0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7',
|
||||
symbol: 'UNI',
|
||||
decimal: 0
|
||||
}
|
||||
]);
|
||||
it('should get the stored sender address', () => {
|
||||
expect(getFrom(state)).toEqual(state.transaction.meta.from);
|
||||
});
|
||||
|
||||
it('should get the stored decimal', () => {
|
||||
expect(getDecimal(state)).toEqual(state.transaction.meta.decimal);
|
||||
});
|
||||
|
||||
it('should get the token value', () => {
|
||||
expect(getTokenValue(state)).toEqual(state.transaction.meta.tokenValue);
|
||||
});
|
||||
|
||||
it('should get the token receiver address', () => {
|
||||
expect(getTokenTo(state)).toEqual(state.transaction.meta.tokenTo);
|
||||
});
|
||||
|
||||
it('should get the stored unit', () => {
|
||||
expect(getUnit(state)).toEqual(state.transaction.meta.unit);
|
||||
});
|
||||
|
||||
it('should get the stored previous unit', () => {
|
||||
expect(getPreviousUnit(state)).toEqual(state.transaction.meta.previousUnit);
|
||||
});
|
||||
|
||||
it('should get the decimal for ether', () => {
|
||||
expect(getDecimalFromUnit(state, getUnit(state))).toEqual(18);
|
||||
});
|
||||
|
||||
it('should get the decimal for a token', () => {
|
||||
expect(getDecimalFromUnit(state, 'UNI')).toEqual(0);
|
||||
});
|
||||
|
||||
it('should throw error if the token is not found', () => {
|
||||
expect(() => getDecimalFromUnit(state, 'ABC')).toThrowError(`Token ABC not found`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
import { RequestStatus } from 'reducers/transaction/network';
|
||||
import {
|
||||
getNetworkStatus,
|
||||
nonceRequestPending,
|
||||
nonceRequestFailed,
|
||||
isNetworkRequestPending,
|
||||
getGasEstimationPending,
|
||||
getGasLimitEstimationTimedOut
|
||||
} from 'selectors/transaction';
|
||||
import { getInitialState } from '../helpers';
|
||||
|
||||
describe('current selector', () => {
|
||||
const state = getInitialState();
|
||||
state.transaction.network = {
|
||||
...state.transaction.network,
|
||||
gasEstimationStatus: RequestStatus.REQUESTED,
|
||||
getFromStatus: RequestStatus.SUCCEEDED,
|
||||
getNonceStatus: RequestStatus.REQUESTED,
|
||||
gasPriceStatus: RequestStatus.SUCCEEDED
|
||||
};
|
||||
|
||||
it('should get network status', () => {
|
||||
expect(getNetworkStatus(state)).toEqual(state.transaction.network);
|
||||
});
|
||||
|
||||
it('should check with the store if the nonce request is pending', () => {
|
||||
expect(nonceRequestPending(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should check with the store if the nonce request failed', () => {
|
||||
state.transaction.network.getNonceStatus = RequestStatus.FAILED;
|
||||
expect(nonceRequestFailed(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should check with the store if the gas estimation is pending', () => {
|
||||
expect(getGasEstimationPending(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should check with the store if gas limit estimation timed out', () => {
|
||||
state.transaction.network.gasEstimationStatus = RequestStatus.TIMEDOUT;
|
||||
expect(getGasLimitEstimationTimedOut(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should check with the store if network request is pending', () => {
|
||||
state.transaction.network.gasEstimationStatus = RequestStatus.REQUESTED;
|
||||
expect(isNetworkRequestPending(state)).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
import {
|
||||
signaturePending,
|
||||
getSignedTx,
|
||||
getWeb3Tx,
|
||||
getSignState,
|
||||
getSerializedTransaction
|
||||
} from 'selectors/transaction/sign';
|
||||
import { getInitialState } from '../helpers';
|
||||
|
||||
describe('sign tests', () => {
|
||||
const state = getInitialState();
|
||||
(state.transaction.sign = {
|
||||
indexingHash: 'testIndexingHash',
|
||||
pending: false,
|
||||
local: {
|
||||
signedTransaction: new Buffer([4, 5, 6, 7])
|
||||
},
|
||||
web3: {
|
||||
transaction: null
|
||||
}
|
||||
}),
|
||||
it('should return whether the current signature is pending', () => {
|
||||
expect(signaturePending(state)).toEqual({
|
||||
isHardwareWallet: false,
|
||||
isSignaturePending: false
|
||||
});
|
||||
});
|
||||
|
||||
it('should should get the stored sign state', () => {
|
||||
expect(getSignState(state)).toEqual(state.transaction.sign);
|
||||
});
|
||||
|
||||
it('should get the signed local transaction state', () => {
|
||||
expect(getSignedTx(state)).toEqual(state.transaction.sign.local.signedTransaction);
|
||||
});
|
||||
|
||||
it('should get the signed web3 transaction state', () => {
|
||||
expect(getWeb3Tx(state)).toEqual(state.transaction.sign.web3.transaction);
|
||||
});
|
||||
|
||||
it('should get the serialized transaction state', () => {
|
||||
expect(getSerializedTransaction(state)).toEqual(new Buffer([4, 5, 6, 7]));
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue