Offline Send (#276)
* offline-send mvp * cleanup unneeded imports * - create pollOfflineStatus action, action creator, interface * expand UnlockHeader when collapse-button is clicked, instead of div * kick-off pollOfflineStatus upon SendTransaction mount. * Create sagas for polling offline status * remove comment * - create CONFIG_FORCE_OFFLINE action, action creator, interface * Adjust OfflineToggle terms to "Force Online/Offline", and understand when forced offline and when really offline. * - Assume offline in SendTransaction when either offline or forcedOffline * - handle forceOffline action in reducer - adjust state type / provide default state for forceOffline in config reducer * adjust test to pass with different key name * fix incorrect import * - allow size to be specified in offline toggle * - Decode and display nonce in confirmation modal * - set default nonces when forced offline and have online connectivity based on transaction count - pass nonce to generateCompleteTransaction - refactor componentDidUpdate * Allow optional nonce to be passed to generateCompleteTransaction * - create stripHexPrefix function * - cleanup sagas * move getParam into helper util * update address on component update * - show spinner while transaction is being signed - reset state when wallet instance changes (new wallet instantiated via UnlockHeader) * center-align offline message * Adjust force offline/online button text * - validate nonces when offline - only estimate gas when online - don't show send tx button when offline * - break generateCompleteTransactionFromRawTransaction into multiple functions. - support offline generation in generateCompleteTransaction (and generateCompleteTransactionFromRawTransaction). Balance checking is now only done when not offline to support offline generation. * Create Help component (to be used as a tooltip) * Disable hardware wallets when offline. * Hide Send Entire Balance when balance is falsy * Show help icon in nonce field. * - show helper instructions on how to broadcast when user is offline after generating a tx - hardcoded gas limits when offline - refactors * create isPositiveInteger helper function * fix nonce validation * really fix nonce validation (specifically the input highlighting) * remove stray // @flow's * remove offline tab nav * remove unused action arg * address PR comments
This commit is contained in:
parent
4858f96520
commit
b493a0c968
|
@ -1,11 +1,25 @@
|
|||
import * as interfaces from './actionTypes';
|
||||
import { TypeKeys } from './constants';
|
||||
|
||||
export type TForceOfflineConfig = typeof forceOfflineConfig;
|
||||
export function forceOfflineConfig(): interfaces.ForceOfflineAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_FORCE_OFFLINE
|
||||
};
|
||||
}
|
||||
|
||||
export type TToggleOfflineConfig = typeof toggleOfflineConfig;
|
||||
export function toggleOfflineConfig(): interfaces.ToggleOfflineAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_TOGGLE_OFFLINE
|
||||
};
|
||||
}
|
||||
|
||||
export type TChangeLanguage = typeof changeLanguage;
|
||||
export function changeLanguage(sign: string): interfaces.ChangeLanguageAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_LANGUAGE_CHANGE,
|
||||
value: sign
|
||||
payload: sign
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -13,7 +27,7 @@ export type TChangeNode = typeof changeNode;
|
|||
export function changeNode(value: string): interfaces.ChangeNodeAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_NODE_CHANGE,
|
||||
value
|
||||
payload: value
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -21,7 +35,14 @@ export type TChangeGasPrice = typeof changeGasPrice;
|
|||
export function changeGasPrice(value: number): interfaces.ChangeGasPriceAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_GAS_PRICE,
|
||||
value
|
||||
payload: value
|
||||
};
|
||||
}
|
||||
|
||||
export type TPollOfflineStatus = typeof pollOfflineStatus;
|
||||
export function pollOfflineStatus(): interfaces.PollOfflineStatus {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_POLL_OFFLINE_STATUS
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,37 @@
|
|||
import { TypeKeys } from './constants';
|
||||
|
||||
/*** Toggle Offline ***/
|
||||
export interface ToggleOfflineAction {
|
||||
type: TypeKeys.CONFIG_TOGGLE_OFFLINE;
|
||||
}
|
||||
|
||||
/*** Force Offline ***/
|
||||
export interface ForceOfflineAction {
|
||||
type: TypeKeys.CONFIG_FORCE_OFFLINE;
|
||||
}
|
||||
|
||||
/*** Change Language ***/
|
||||
export interface ChangeLanguageAction {
|
||||
type: TypeKeys.CONFIG_LANGUAGE_CHANGE;
|
||||
value: string;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
/*** Change Node ***/
|
||||
export interface ChangeNodeAction {
|
||||
type: TypeKeys.CONFIG_NODE_CHANGE;
|
||||
// FIXME $keyof?
|
||||
value: string;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
/*** Change gas price ***/
|
||||
export interface ChangeGasPriceAction {
|
||||
type: TypeKeys.CONFIG_GAS_PRICE;
|
||||
value: number;
|
||||
payload: number;
|
||||
}
|
||||
|
||||
/*** Poll offline status ***/
|
||||
export interface PollOfflineStatus {
|
||||
type: TypeKeys.CONFIG_POLL_OFFLINE_STATUS;
|
||||
}
|
||||
|
||||
/*** Change Node ***/
|
||||
|
@ -30,4 +45,7 @@ export type ConfigAction =
|
|||
| ChangeNodeAction
|
||||
| ChangeLanguageAction
|
||||
| ChangeGasPriceAction
|
||||
| ToggleOfflineAction
|
||||
| PollOfflineStatus
|
||||
| ForceOfflineAction
|
||||
| ChangeNodeIntentAction;
|
||||
|
|
|
@ -2,5 +2,8 @@ export enum TypeKeys {
|
|||
CONFIG_LANGUAGE_CHANGE = 'CONFIG_LANGUAGE_CHANGE',
|
||||
CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE',
|
||||
CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT',
|
||||
CONFIG_GAS_PRICE = 'CONFIG_GAS_PRICE'
|
||||
CONFIG_GAS_PRICE = 'CONFIG_GAS_PRICE',
|
||||
CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE',
|
||||
CONFIG_FORCE_OFFLINE = 'CONFIG_FORCE_OFFLINE',
|
||||
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS'
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496.16 496.16"><defs><style>.cls-1{fill:#04778d}.cls-2{fill:#fff}.cls-3{fill:#f34002}</style></defs><title>Offline Icon</title><path class="cls-1" d="M496.16 248.09C496.16 111.06 385.09 0 248.08 0S0 111.06 0 248.09s111.07 248.07 248.08 248.07 248.08-111.07 248.08-248.07z"/><circle class="cls-2" cx="248.08" cy="332.81" r="49.15"/><path class="cls-2" d="M172.13 289.13c20.29-23.3 47.26-36.13 76-36.13s55.66 12.83 76 36.13l16-18.34c-24.55-28.2-57.2-43.73-91.93-43.73s-67.37 15.53-91.92 43.73l16 18.34z"/><path class="cls-2" d="M138.56 248.41c29.26-33.6 68.15-52.1 109.52-52.1s80.27 18.5 109.52 52.1l16-18.34c-33.52-38.49-78.09-59.7-125.49-59.7s-92 21.2-125.49 59.7l16 18.34z"/><path class="cls-2" d="M105.31 208.06c38.14-43.8 88.84-67.92 142.77-67.92s104.64 24.12 142.77 67.92l16-18.34C364.42 141 308 114.21 248.08 114.21S131.74 141 89.34 189.72l16 18.34zM248.08 376.37h.01v.01h-.01z"/><circle class="cls-3" cx="340.29" cy="343.74" r="63.78"/><path class="cls-2" d="M315 365.51l16.82-21.59-14.12-19.21a36.92 36.92 0 0 1-3-4.8 8.64 8.64 0 0 1-1-3.86 4.31 4.31 0 0 1 1.92-3.4 7.36 7.36 0 0 1 4.69-1.51 7 7 0 0 1 4.95 1.65 42.15 42.15 0 0 1 4.9 6.11l11.28 16 12.05-16 2.54-3.47a18.66 18.66 0 0 1 2-2.39 6.53 6.53 0 0 1 2.18-1.42 7.61 7.61 0 0 1 2.79-.47 7.11 7.11 0 0 1 4.69 1.51 4.53 4.53 0 0 1 1.82 3.58q0 3-3.95 8.21l-14.82 19.48 16 21.59a35.31 35.31 0 0 1 3.13 4.71 7.7 7.7 0 0 1 1 3.54 5.09 5.09 0 0 1-.87 2.89 6 6 0 0 1-2.46 2.07 8.17 8.17 0 0 1-3.59.77 7.7 7.7 0 0 1-3.64-.79 7.44 7.44 0 0 1-2.41-2q-.92-1.17-3.44-4.55l-13.23-18.3-14.05 18.84q-1.64 2.26-2.33 3.16a12.43 12.43 0 0 1-1.67 1.76 7.32 7.32 0 0 1-2.31 1.35 9 9 0 0 1-3.13.5 7 7 0 0 1-4.59-1.49 5.27 5.27 0 0 1-1.82-4.33q-.04-3.32 3.67-8.14z"/></svg>
|
After Width: | Height: | Size: 1.8 KiB |
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496.16 496.16"><defs><style>.cls-1{fill:#04778d}.cls-2{fill:#fff}.cls-3{fill:#00e65f}</style></defs><title>Online Icon</title><path class="cls-1" d="M496.16 248.09C496.16 111.06 385.09 0 248.08 0S0 111.06 0 248.09s111.07 248.07 248.08 248.07 248.08-111.07 248.08-248.07z"/><circle class="cls-2" cx="248.08" cy="332.81" r="49.15"/><path class="cls-2" d="M172.13 289.13c20.29-23.3 47.26-36.13 76-36.13s55.66 12.83 76 36.13l16-18.34c-24.55-28.2-57.2-43.73-91.93-43.73s-67.37 15.53-91.92 43.73l16 18.34z"/><path class="cls-2" d="M138.56 248.41c29.26-33.6 68.15-52.1 109.52-52.1s80.27 18.5 109.52 52.1l16-18.34c-33.52-38.49-78.09-59.7-125.49-59.7s-92 21.2-125.49 59.7l16 18.34z"/><path class="cls-2" d="M105.31 208.06c38.14-43.8 88.84-67.92 142.77-67.92s104.64 24.12 142.77 67.92l16-18.34C364.42 141 308 114.21 248.08 114.21S131.74 141 89.34 189.72l16 18.34zM248.08 376.37h.01v.01h-.01z"/><circle class="cls-3" cx="340.29" cy="343.74" r="63.78"/><path class="cls-2" d="M383.53 317.43c-1.85-4.77-5.61-4-9.7-3.21-2.44.51-13.28 3.68-30.44 21.77a144.85 144.85 0 0 0-14.91 18.06c-1.89-2.32-4.05-4.8-6.33-7.08a91.5 91.5 0 0 0-15.11-12 6.95 6.95 0 0 0-7.26 11.85 78.31 78.31 0 0 1 12.54 10 107.89 107.89 0 0 1 11.29 14 6.95 6.95 0 0 0 12.52-2.17c0-.06 2.77-7.68 17.32-23 11.72-12.36 19.54-16.29 22.25-17.38h.08l.25-.12a7.69 7.69 0 0 1 .73-.24h-.2l3.65-1.58c3.55-1.54 4.7-5.33 3.32-8.9z"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -25,11 +25,20 @@ export default class AccountInfo extends React.Component<Props, State> {
|
|||
address: ''
|
||||
};
|
||||
|
||||
public async setAddressFromWallet() {
|
||||
const address = await this.props.wallet.getAddress();
|
||||
if (address !== this.state.address) {
|
||||
this.setState({ address });
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.fetchCCRates();
|
||||
this.props.wallet.getAddress().then(address => {
|
||||
this.setState({ address });
|
||||
});
|
||||
this.setAddressFromWallet();
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.setAddressFromWallet();
|
||||
}
|
||||
|
||||
// TODO: don't use any;
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
forceOfflineConfig as dForceOfflineConfig,
|
||||
TForceOfflineConfig
|
||||
} from 'actions/config';
|
||||
import OfflineSymbol from 'components/ui/OfflineSymbol';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
type sizeType = 'small' | 'medium' | 'large';
|
||||
|
||||
interface OfflineToggleProps {
|
||||
offline: boolean;
|
||||
forceOffline: boolean;
|
||||
forceOfflineConfig: TForceOfflineConfig;
|
||||
size?: sizeType;
|
||||
}
|
||||
|
||||
class OfflineToggle extends React.Component<OfflineToggleProps, {}> {
|
||||
public render() {
|
||||
const { forceOfflineConfig, offline, forceOffline, size } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!offline ? (
|
||||
<div className="row text-center">
|
||||
<div className="col-md-3">
|
||||
<OfflineSymbol offline={offline || forceOffline} size={size} />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<button className="btn-xs btn-info" onClick={forceOfflineConfig}>
|
||||
{forceOffline ? 'Go Online' : 'Go Offline'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<h5>You are currently offline.</h5>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
return {
|
||||
offline: state.config.offline,
|
||||
forceOffline: state.config.forceOffline
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
forceOfflineConfig: dForceOfflineConfig
|
||||
})(OfflineToggle);
|
|
@ -23,6 +23,7 @@ import EquivalentValues from './EquivalentValues';
|
|||
import Promos from './Promos';
|
||||
import TokenBalances from './TokenBalances';
|
||||
import { State } from 'reducers/rates';
|
||||
import OfflineToggle from './OfflineToggle';
|
||||
|
||||
interface Props {
|
||||
wallet: IWallet;
|
||||
|
@ -59,6 +60,10 @@ export class BalanceSidebar extends React.Component<Props, {}> {
|
|||
}
|
||||
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
name: 'Go Offline',
|
||||
content: <OfflineToggle />
|
||||
},
|
||||
{
|
||||
name: 'Account Info',
|
||||
content: (
|
||||
|
|
|
@ -17,9 +17,6 @@ const tabs = [
|
|||
name: 'NAV_Swap',
|
||||
to: 'swap'
|
||||
},
|
||||
{
|
||||
name: 'NAV_Offline'
|
||||
},
|
||||
{
|
||||
name: 'NAV_ViewWallet'
|
||||
// to: 'view-wallet'
|
||||
|
|
|
@ -19,6 +19,7 @@ import MnemonicDecrypt from './Mnemonic';
|
|||
import PrivateKeyDecrypt, { PrivateKeyValue } from './PrivateKey';
|
||||
import TrezorDecrypt from './Trezor';
|
||||
import ViewOnlyDecrypt from './ViewOnly';
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
const WALLETS = {
|
||||
'keystore-file': {
|
||||
|
@ -76,6 +77,7 @@ interface Props {
|
|||
dispatch: Dispatch<
|
||||
UnlockKeystoreAction | UnlockMnemonicAction | UnlockPrivateKeyAction
|
||||
>;
|
||||
offline: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -106,6 +108,13 @@ export class WalletDecrypt extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
public isOnlineRequiredWalletAndOffline(selectedWalletKey) {
|
||||
const onlineRequiredWallets = ['trezor', 'ledger-nano-s'];
|
||||
return (
|
||||
this.props.offline && onlineRequiredWallets.includes(selectedWalletKey)
|
||||
);
|
||||
}
|
||||
|
||||
public buildWalletOptions() {
|
||||
return map(WALLETS, (wallet, key) => {
|
||||
const isSelected = this.state.selectedWalletKey === key;
|
||||
|
@ -120,7 +129,9 @@ export class WalletDecrypt extends Component<Props, State> {
|
|||
value={key}
|
||||
checked={isSelected}
|
||||
onChange={this.handleDecryptionChoiceChange}
|
||||
disabled={wallet.disabled}
|
||||
disabled={
|
||||
wallet.disabled || this.isOnlineRequiredWalletAndOffline(key)
|
||||
}
|
||||
/>
|
||||
<span id={`${key}-label`}>{translate(wallet.lid)}</span>
|
||||
</label>
|
||||
|
@ -191,4 +202,10 @@ export class WalletDecrypt extends Component<Props, State> {
|
|||
};
|
||||
}
|
||||
|
||||
export default connect()(WalletDecrypt);
|
||||
function mapStateToProps(state: AppState) {
|
||||
return {
|
||||
offline: state.config.offline
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(WalletDecrypt);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import DropdownShell from './DropdownShell';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import helpIcon from 'assets/images/icon-help.svg';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
|
||||
type sizeType = 'small' | 'medium' | 'large';
|
||||
|
||||
interface HelpProps {
|
||||
link: string;
|
||||
size?: sizeType;
|
||||
helpText?: string;
|
||||
}
|
||||
|
||||
const Help = ({ size, link, helpText }: HelpProps) => {
|
||||
let width = 30;
|
||||
let height = 12;
|
||||
|
||||
switch (size) {
|
||||
case 'medium':
|
||||
width = width * 3;
|
||||
height = height * 3;
|
||||
break;
|
||||
case 'large':
|
||||
width = width * 4;
|
||||
height = height * 4;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={link} className={'account-help-icon'} target={'_blank'}>
|
||||
<img src={helpIcon} width={width} height={height} />
|
||||
|
||||
{helpText && <p className="account-help-text">{helpText}</p>}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default Help;
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import wifiOn from 'assets/images/wifi-on.svg';
|
||||
import wifiOff from 'assets/images/wifi-off.svg';
|
||||
|
||||
type sizeType = 'small' | 'medium' | 'large';
|
||||
|
||||
interface OfflineSymbolProps {
|
||||
offline: boolean;
|
||||
size?: sizeType;
|
||||
}
|
||||
|
||||
const OfflineSymbol = ({ offline, size }: OfflineSymbolProps) => {
|
||||
let width = 30;
|
||||
let height = 12;
|
||||
|
||||
switch (size) {
|
||||
case 'medium':
|
||||
width = width * 3;
|
||||
height = height * 3;
|
||||
break;
|
||||
case 'large':
|
||||
width = width * 4;
|
||||
height = height * 4;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return <img src={offline ? wifiOff : wifiOn} width={width} height={height} />;
|
||||
};
|
||||
|
||||
export default OfflineSymbol;
|
|
@ -3,10 +3,9 @@ import { IWallet } from 'libs/wallet/IWallet';
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import translate from 'translations';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
title: React.ReactElement<any>;
|
||||
wallet: IWallet;
|
||||
}
|
||||
interface State {
|
||||
|
@ -29,17 +28,14 @@ export class UnlockHeader extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { title } = this.props;
|
||||
return (
|
||||
<article className="collapse-container">
|
||||
<div onClick={this.toggleExpanded}>
|
||||
<a className="collapse-button">
|
||||
<span>
|
||||
{this.state.expanded ? '-' : '+'}
|
||||
</span>
|
||||
<div>
|
||||
<a className="collapse-button" onClick={this.toggleExpanded}>
|
||||
<span>{this.state.expanded ? '-' : '+'}</span>
|
||||
</a>
|
||||
<h1>
|
||||
{translate(this.props.title)}
|
||||
</h1>
|
||||
<h1>{title}</h1>
|
||||
</div>
|
||||
{this.state.expanded && <WalletDecrypt />}
|
||||
{this.state.expanded && <hr />}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import React from 'react';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import UnitDropdown from './UnitDropdown';
|
||||
import { Ether } from 'libs/units';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
unit: string;
|
||||
tokens: string[];
|
||||
balance: number | null | Ether;
|
||||
onChange?(value: string, unit: string): void;
|
||||
}
|
||||
|
||||
|
@ -13,14 +15,12 @@ export default class AmountField extends React.Component {
|
|||
public props: Props;
|
||||
|
||||
public render() {
|
||||
const { value, unit, onChange } = this.props;
|
||||
const { value, unit, onChange, balance } = this.props;
|
||||
const isReadonly = !onChange;
|
||||
return (
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-11">
|
||||
<label>
|
||||
{translate('SEND_amount')}
|
||||
</label>
|
||||
<label>{translate('SEND_amount')}</label>
|
||||
<div className="input-group">
|
||||
<input
|
||||
className={`form-control ${isFinite(Number(value)) &&
|
||||
|
@ -40,13 +40,15 @@ export default class AmountField extends React.Component {
|
|||
/>
|
||||
</div>
|
||||
{!isReadonly &&
|
||||
<span className="help-block">
|
||||
<a onClick={this.onSendEverything}>
|
||||
<span className="strong">
|
||||
{translate('SEND_TransferTotal')}
|
||||
</span>
|
||||
</a>
|
||||
</span>}
|
||||
balance && (
|
||||
<span className="help-block">
|
||||
<a onClick={this.onSendEverything}>
|
||||
<span className="strong">
|
||||
{translate('SEND_TransferTotal')}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -78,7 +78,13 @@ class ConfirmationModal extends React.Component<Props, State> {
|
|||
public render() {
|
||||
const { node, token, network, onClose, broadCastTxStatus } = this.props;
|
||||
const { fromAddress, timeToRead } = this.state;
|
||||
const { toAddress, value, gasPrice, data } = this.decodeTransaction();
|
||||
const {
|
||||
toAddress,
|
||||
value,
|
||||
gasPrice,
|
||||
data,
|
||||
nonce
|
||||
} = this.decodeTransaction();
|
||||
|
||||
const buttonPrefix = timeToRead > 0 ? `(${timeToRead}) ` : '';
|
||||
const buttons: IButton[] = [
|
||||
|
@ -138,6 +144,9 @@ class ConfirmationModal extends React.Component<Props, State> {
|
|||
<li className="ConfModal-details-detail">
|
||||
You are sending to <code>{toAddress}</code>
|
||||
</li>
|
||||
<li className="ConfModal-details-detail">
|
||||
You are sending with a nonce of <code>{nonce}</code>
|
||||
</li>
|
||||
<li className="ConfModal-details-detail">
|
||||
You are sending{' '}
|
||||
<strong>
|
||||
|
@ -191,7 +200,9 @@ class ConfirmationModal extends React.Component<Props, State> {
|
|||
|
||||
private decodeTransaction() {
|
||||
const { transaction, token } = this.props;
|
||||
const { to, value, data, gasPrice } = getTransactionFields(transaction);
|
||||
const { to, value, data, gasPrice, nonce } = getTransactionFields(
|
||||
transaction
|
||||
);
|
||||
let fixedValue;
|
||||
let toAddress;
|
||||
|
||||
|
@ -208,7 +219,8 @@ class ConfirmationModal extends React.Component<Props, State> {
|
|||
value: fixedValue,
|
||||
gasPrice: toUnit(new Big(gasPrice, 16), 'wei', 'gwei').toString(),
|
||||
data,
|
||||
toAddress
|
||||
toAddress,
|
||||
nonce
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react';
|
||||
import Help from 'components/ui/Help';
|
||||
import { isPositiveInteger } from 'utils/helpers';
|
||||
|
||||
interface PublicProps {
|
||||
placeholder: string;
|
||||
value: number | null | undefined;
|
||||
onChange(value: number): void;
|
||||
}
|
||||
|
||||
const isValidNonce = (value: string | null | undefined) => {
|
||||
let valid;
|
||||
if (value === '0') {
|
||||
valid = true;
|
||||
} else if (!value) {
|
||||
valid = false;
|
||||
} else {
|
||||
valid = isPositiveInteger(parseInt(value, 10));
|
||||
}
|
||||
return valid;
|
||||
};
|
||||
|
||||
export default class NonceField extends React.Component<PublicProps, {}> {
|
||||
public render() {
|
||||
const { placeholder, value } = this.props;
|
||||
const strValue = value ? value.toString() : '';
|
||||
return (
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-11">
|
||||
<Help
|
||||
size={'small'}
|
||||
link={
|
||||
'https://myetherwallet.github.io/knowledge-base/transactions/what-is-nonce.html'
|
||||
}
|
||||
/>
|
||||
<label>Nonce</label>
|
||||
<input
|
||||
className={`form-control ${isValidNonce(strValue)
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
type="number"
|
||||
value={strValue}
|
||||
placeholder={placeholder}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onChange = (e: any) => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
}
|
|
@ -5,3 +5,4 @@ export { default as CustomMessage } from './CustomMessage';
|
|||
export { default as AmountField } from './AmountField';
|
||||
export { default as AddressField } from './AddressField';
|
||||
export { default as ConfirmationModal } from './ConfirmationModal';
|
||||
export { default as NonceField } from './NonceField';
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import { showNotification, TShowNotification } from 'actions/notifications';
|
||||
import {
|
||||
broadcastTx,
|
||||
TBroadcastTx,
|
||||
resetWallet,
|
||||
TResetWallet
|
||||
} from 'actions/wallet';
|
||||
import Big from 'bignumber.js';
|
||||
import { BalanceSidebar } from 'components';
|
||||
// COMPONENTS
|
||||
import { UnlockHeader } from 'components/ui';
|
||||
import { donationAddressMap, NetworkConfig, NodeConfig } from 'config/data';
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
import TabSection from 'containers/TabSection';
|
||||
import { BalanceSidebar } from 'components';
|
||||
import { UnlockHeader } from 'components/ui';
|
||||
import {
|
||||
NonceField,
|
||||
AddressField,
|
||||
AmountField,
|
||||
ConfirmationModal,
|
||||
CustomMessage,
|
||||
DataField,
|
||||
GasField
|
||||
} from './components';
|
||||
import NavigationPrompt from './components/NavigationPrompt';
|
||||
// CONFIG
|
||||
import { donationAddressMap, NetworkConfig, NodeConfig } from 'config/data';
|
||||
// LIBS
|
||||
import { stripHexPrefix } from 'libs/values';
|
||||
import { TransactionWithoutGas } from 'libs/messages';
|
||||
import { RPCNode } from 'libs/nodes';
|
||||
import {
|
||||
|
@ -25,20 +30,30 @@ import {
|
|||
} from 'libs/transaction';
|
||||
import { Ether, GWei, UnitKey, Wei } from 'libs/units';
|
||||
import { isValidETHAddress } from 'libs/validators';
|
||||
// LIBS
|
||||
import { IWallet } from 'libs/wallet/IWallet';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import React from 'react';
|
||||
// REDUX
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import { showNotification, TShowNotification } from 'actions/notifications';
|
||||
import {
|
||||
broadcastTx,
|
||||
TBroadcastTx,
|
||||
resetWallet,
|
||||
TResetWallet
|
||||
} from 'actions/wallet';
|
||||
import {
|
||||
pollOfflineStatus as dPollOfflineStatus,
|
||||
TPollOfflineStatus
|
||||
} from 'actions/config';
|
||||
// SELECTORS
|
||||
import {
|
||||
getGasPriceGwei,
|
||||
getNetworkConfig,
|
||||
getNodeConfig,
|
||||
getNodeLib
|
||||
} from 'selectors/config';
|
||||
// SELECTORS
|
||||
import {
|
||||
getTokenBalances,
|
||||
getTokens,
|
||||
|
@ -49,27 +64,11 @@ import {
|
|||
import translate from 'translations';
|
||||
// UTILS
|
||||
import { formatGasLimit } from 'utils/formatters';
|
||||
import {
|
||||
AddressField,
|
||||
AmountField,
|
||||
ConfirmationModal,
|
||||
CustomMessage,
|
||||
DataField,
|
||||
GasField
|
||||
} from './components';
|
||||
import { getParam } from 'utils/helpers';
|
||||
import queryString from 'query-string';
|
||||
// MISC
|
||||
import customMessages from './messages';
|
||||
|
||||
function getParam(query: { [key: string]: string }, key: string) {
|
||||
const keys = Object.keys(query);
|
||||
const index = keys.findIndex(k => k.toLowerCase() === key.toLowerCase());
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
return query[keys[index]];
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasQueryString: boolean;
|
||||
readOnly: boolean;
|
||||
|
@ -84,6 +83,9 @@ interface State {
|
|||
transaction: CompleteTransaction | null;
|
||||
showTxConfirm: boolean;
|
||||
generateDisabled: boolean;
|
||||
nonce: number | null | undefined;
|
||||
hasSetDefaultNonce: boolean;
|
||||
generateTxProcessing: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -99,6 +101,9 @@ interface Props {
|
|||
showNotification: TShowNotification;
|
||||
broadcastTx: TBroadcastTx;
|
||||
resetWallet: TResetWallet;
|
||||
offline: boolean;
|
||||
forceOffline: boolean;
|
||||
pollOfflineStatus: TPollOfflineStatus;
|
||||
location: { search: string };
|
||||
}
|
||||
|
||||
|
@ -114,40 +119,66 @@ const initialState: State = {
|
|||
gasChanged: false,
|
||||
showTxConfirm: false,
|
||||
transaction: null,
|
||||
generateDisabled: true
|
||||
generateDisabled: true,
|
||||
nonce: null,
|
||||
hasSetDefaultNonce: false,
|
||||
generateTxProcessing: false
|
||||
};
|
||||
|
||||
export class SendTransaction extends React.Component<Props, State> {
|
||||
public state: State = initialState;
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.pollOfflineStatus();
|
||||
const queryPresets = pickBy(this.parseQuery());
|
||||
if (Object.keys(queryPresets).length) {
|
||||
this.setState({ ...queryPresets, hasQueryString: true });
|
||||
this.setState(state => {
|
||||
return {
|
||||
...state,
|
||||
...queryPresets,
|
||||
hasQueryString: true
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
public haveFieldsChanged(prevState) {
|
||||
return (
|
||||
this.state.to !== prevState.to ||
|
||||
this.state.value !== prevState.value ||
|
||||
this.state.unit !== prevState.unit ||
|
||||
this.state.data !== prevState.data
|
||||
);
|
||||
}
|
||||
|
||||
public shouldReEstimateGas(prevState) {
|
||||
// TODO listen to gas price changes here
|
||||
// TODO debounce the call
|
||||
if (
|
||||
// handle gas estimation
|
||||
// if any relevant fields changed
|
||||
return (
|
||||
this.haveFieldsChanged(prevState) &&
|
||||
// if gas has not changed
|
||||
!this.state.gasChanged &&
|
||||
// if we have valid tx
|
||||
this.isValid() &&
|
||||
// if any relevant fields changed
|
||||
(this.state.to !== prevState.to ||
|
||||
this.state.value !== prevState.value ||
|
||||
this.state.unit !== prevState.unit ||
|
||||
this.state.data !== prevState.data)
|
||||
) {
|
||||
if (!isNaN(parseInt(this.state.value, 10))) {
|
||||
this.estimateGas();
|
||||
}
|
||||
(this.isValid() || (this.props.offline || this.props.forceOffline))
|
||||
);
|
||||
}
|
||||
|
||||
public handleGasEstimationOnUpdate(prevState) {
|
||||
if (this.shouldReEstimateGas(prevState)) {
|
||||
this.estimateGas();
|
||||
}
|
||||
}
|
||||
|
||||
public handleGenerateDisabledOnUpdate() {
|
||||
if (this.state.generateDisabled === this.isValid()) {
|
||||
this.setState({ generateDisabled: !this.isValid() });
|
||||
}
|
||||
}
|
||||
|
||||
public handleBroadcastTransactionOnUpdate() {
|
||||
// handle clearing the form once broadcast transaction promise resolves and compontent updates
|
||||
const componentStateTransaction = this.state.transaction;
|
||||
if (componentStateTransaction) {
|
||||
// lives in redux state
|
||||
|
@ -165,6 +196,42 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
public async handleSetNonceWhenOfflineOnUpdate() {
|
||||
const { offline, forceOffline, wallet, nodeLib } = this.props;
|
||||
const { hasSetDefaultNonce, nonce } = this.state;
|
||||
const unlocked = !!wallet;
|
||||
if (unlocked) {
|
||||
const from = await wallet.getAddress();
|
||||
if (forceOffline && !offline && !hasSetDefaultNonce) {
|
||||
const nonceHex = await nodeLib.getTransactionCount(from);
|
||||
const newNonce = parseInt(stripHexPrefix(nonceHex), 10);
|
||||
this.setState({ nonce: newNonce, hasSetDefaultNonce: true });
|
||||
}
|
||||
if (!forceOffline && !offline && nonce) {
|
||||
// set hasSetDefaultNonce back to false in case user toggles force offline several times
|
||||
this.setState({ nonce: null, hasSetDefaultNonce: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public handleWalletStateOnUpdate(prevProps) {
|
||||
if (this.props.wallet !== prevProps.wallet) {
|
||||
this.setState(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
this.handleGasEstimationOnUpdate(prevState);
|
||||
this.handleGenerateDisabledOnUpdate();
|
||||
this.handleBroadcastTransactionOnUpdate();
|
||||
this.handleSetNonceWhenOfflineOnUpdate();
|
||||
this.handleWalletStateOnUpdate(prevProps);
|
||||
}
|
||||
|
||||
public onNonceChange = (value: number) => {
|
||||
this.setState({ nonce: value });
|
||||
};
|
||||
|
||||
public render() {
|
||||
const unlocked = !!this.props.wallet;
|
||||
const {
|
||||
|
@ -176,13 +243,25 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
readOnly,
|
||||
hasQueryString,
|
||||
showTxConfirm,
|
||||
transaction
|
||||
transaction,
|
||||
nonce,
|
||||
generateTxProcessing
|
||||
} = this.state;
|
||||
const { offline, forceOffline, balance } = this.props;
|
||||
const customMessage = customMessages.find(m => m.to === to);
|
||||
return (
|
||||
<TabSection>
|
||||
<section className="Tab-content">
|
||||
<UnlockHeader title={'NAV_SendEther'} />
|
||||
<UnlockHeader
|
||||
title={
|
||||
<div>
|
||||
{translate('NAV_SendEther')}
|
||||
{offline || forceOffline ? (
|
||||
<span style={{ color: 'red' }}> (Offline)</span>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<NavigationPrompt
|
||||
when={unlocked}
|
||||
onConfirm={this.props.resetWallet}
|
||||
|
@ -206,6 +285,7 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
<AmountField
|
||||
value={value}
|
||||
unit={unit}
|
||||
balance={balance}
|
||||
tokens={this.props.tokenBalances
|
||||
.filter(token => !token.balance.eq(0))
|
||||
.map(token => token.symbol)
|
||||
|
@ -216,6 +296,15 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
value={gasLimit}
|
||||
onChange={readOnly ? void 0 : this.onGasChange}
|
||||
/>
|
||||
{(offline || forceOffline) && (
|
||||
<div>
|
||||
<NonceField
|
||||
value={nonce}
|
||||
onChange={this.onNonceChange}
|
||||
placeholder={'0'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{unit === 'ether' && (
|
||||
<DataField
|
||||
value={data}
|
||||
|
@ -236,6 +325,14 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{generateTxProcessing && (
|
||||
<div className="container">
|
||||
<div className="row form-group text-center">
|
||||
<Spinner size="5x" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{transaction && (
|
||||
<div>
|
||||
<div className="row form-group">
|
||||
|
@ -256,18 +353,35 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
rows={4}
|
||||
readOnly={true}
|
||||
/>
|
||||
{offline && (
|
||||
<p>
|
||||
To broadcast this transaction, paste the above
|
||||
into{' '}
|
||||
<a href="https://myetherwallet.com/pushTx">
|
||||
{' '}
|
||||
myetherwallet.com/pushTx
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a href="https://etherscan.io/pushTx">
|
||||
{' '}
|
||||
etherscan.io/pushTx
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<button
|
||||
className="btn btn-primary btn-block col-sm-11"
|
||||
disabled={!this.state.transaction}
|
||||
onClick={this.openTxModal}
|
||||
>
|
||||
{translate('SEND_trans')}
|
||||
</button>
|
||||
</div>
|
||||
{!offline && (
|
||||
<div className="form-group">
|
||||
<button
|
||||
className="btn btn-primary btn-block col-sm-11"
|
||||
disabled={!this.state.transaction}
|
||||
onClick={this.openTxModal}
|
||||
>
|
||||
{translate('SEND_trans')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -312,6 +426,18 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
return { to, data, value, unit, gasLimit, readOnly };
|
||||
}
|
||||
|
||||
public isValidNonce() {
|
||||
const { offline, forceOffline } = this.props;
|
||||
const { nonce } = this.state;
|
||||
let valid = true;
|
||||
if (offline || forceOffline) {
|
||||
if (!nonce || nonce < 0) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
public isValid() {
|
||||
const { to, value, gasLimit } = this.state;
|
||||
return (
|
||||
|
@ -321,7 +447,8 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
!isNaN(Number(value)) &&
|
||||
isFinite(Number(value)) &&
|
||||
!isNaN(parseInt(gasLimit, 10)) &&
|
||||
isFinite(parseInt(gasLimit, 10))
|
||||
isFinite(parseInt(gasLimit, 10)) &&
|
||||
this.isValidNonce()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -338,16 +465,35 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
return await formatTxInput(wallet, transactionInput);
|
||||
}
|
||||
|
||||
public isValidValue() {
|
||||
return !isNaN(parseInt(this.state.value, 10));
|
||||
}
|
||||
|
||||
public async estimateGas() {
|
||||
if (isNaN(parseInt(this.state.value, 10))) {
|
||||
const { offline, forceOffline, nodeLib } = this.props;
|
||||
let gasLimit;
|
||||
|
||||
if (offline || forceOffline) {
|
||||
const { unit } = this.state;
|
||||
if (unit === 'ether') {
|
||||
gasLimit = 21000;
|
||||
} else {
|
||||
gasLimit = 150000;
|
||||
}
|
||||
this.setState({ gasLimit });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValidValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const cachedFormattedTx = await this.getFormattedTxFromState();
|
||||
// Grab a reference to state. If it has changed by the time the estimateGas
|
||||
// call comes back, we don't want to replace the gasLimit in state.
|
||||
const state = this.state;
|
||||
const gasLimit = await this.props.nodeLib.estimateGas(cachedFormattedTx);
|
||||
gasLimit = await nodeLib.estimateGas(cachedFormattedTx);
|
||||
if (this.state === state) {
|
||||
this.setState({ gasLimit: formatGasLimit(gasLimit, state.unit) });
|
||||
} else {
|
||||
|
@ -434,10 +580,10 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
);
|
||||
|
||||
public generateTxFromState = async () => {
|
||||
this.setState({ generateTxProcessing: true });
|
||||
await this.resetJustTx();
|
||||
const { nodeLib, wallet, gasPrice, network } = this.props;
|
||||
|
||||
const { token, unit, value, to, data, gasLimit } = this.state;
|
||||
const { nodeLib, wallet, gasPrice, network, offline } = this.props;
|
||||
const { token, unit, value, to, data, gasLimit, nonce } = this.state;
|
||||
const chainId = network.chainId;
|
||||
const transactionInput = {
|
||||
token,
|
||||
|
@ -454,10 +600,13 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
gasPrice,
|
||||
bigGasLimit,
|
||||
chainId,
|
||||
transactionInput
|
||||
transactionInput,
|
||||
nonce,
|
||||
offline
|
||||
);
|
||||
this.setState({ transaction: signedTx });
|
||||
this.setState({ transaction: signedTx, generateTxProcessing: false });
|
||||
} catch (err) {
|
||||
this.setState({ generateTxProcessing: false });
|
||||
this.props.showNotification('danger', err.message, 5000);
|
||||
}
|
||||
};
|
||||
|
@ -493,12 +642,15 @@ function mapStateToProps(state: AppState) {
|
|||
network: getNetworkConfig(state),
|
||||
tokens: getTokens(state),
|
||||
gasPrice: new GWei(getGasPriceGwei(state)).toWei(),
|
||||
transactions: state.wallet.transactions
|
||||
transactions: state.wallet.transactions,
|
||||
offline: state.config.offline,
|
||||
forceOffline: state.config.forceOffline
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
showNotification,
|
||||
broadcastTx,
|
||||
resetWallet
|
||||
resetWallet,
|
||||
pollOfflineStatus: dPollOfflineStatus
|
||||
})(SendTransaction);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import Big, { BigNumber } from 'bignumber.js';
|
||||
import { Token } from 'config/data';
|
||||
import EthTx from 'ethereumjs-tx';
|
||||
import { addHexPrefix, padToEven, toChecksumAddress } from 'ethereumjs-util';
|
||||
|
@ -11,6 +10,7 @@ import { isValidETHAddress } from 'libs/validators';
|
|||
import { stripHexPrefixAndLower, valueToHex } from 'libs/values';
|
||||
import { IWallet } from 'libs/wallet';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import Big, { BigNumber } from 'bignumber.js';
|
||||
|
||||
export interface TransactionInput {
|
||||
token?: Token | null;
|
||||
|
@ -71,13 +71,66 @@ export function getTransactionFields(tx: EthTx) {
|
|||
};
|
||||
}
|
||||
|
||||
export async function generateCompleteTransactionFromRawTransaction(
|
||||
function getValue(
|
||||
token: Token | null | undefined,
|
||||
tx: ExtendedRawTransaction
|
||||
): BigNumber {
|
||||
let value;
|
||||
if (token) {
|
||||
value = new Big(ERC20.$transfer(tx.data).value);
|
||||
} else {
|
||||
value = new Big(tx.value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function getBalance(
|
||||
node: INode,
|
||||
tx: ExtendedRawTransaction,
|
||||
wallet: IWallet,
|
||||
token: Token | null | undefined
|
||||
): Promise<CompleteTransaction> {
|
||||
const { to, data, gasLimit, gasPrice, from, chainId, nonce } = tx;
|
||||
) {
|
||||
const { from } = tx;
|
||||
const ETHBalance = await node.getBalance(from);
|
||||
let balance;
|
||||
if (token) {
|
||||
balance = toTokenUnit(await node.getTokenBalance(tx.from, token), token);
|
||||
} else {
|
||||
balance = ETHBalance.amount;
|
||||
}
|
||||
return {
|
||||
balance,
|
||||
ETHBalance
|
||||
};
|
||||
}
|
||||
|
||||
async function balanceCheck(
|
||||
node: INode,
|
||||
tx: ExtendedRawTransaction,
|
||||
token: Token | null | undefined,
|
||||
value: BigNumber,
|
||||
gasCost: Wei
|
||||
) {
|
||||
// Ensure their balance exceeds the amount they're sending
|
||||
const { balance, ETHBalance } = await getBalance(node, tx, token);
|
||||
if (value.gt(balance)) {
|
||||
throw new Error(translateRaw('GETH_Balance'));
|
||||
}
|
||||
// ensure gas cost is not greaterThan current eth balance
|
||||
// TODO check that eth balance is not lesser than txAmount + gasCost
|
||||
if (gasCost.amount.gt(ETHBalance.amount)) {
|
||||
throw new Error(
|
||||
`gasCost: ${gasCost.amount} greaterThan ETHBalance: ${ETHBalance.amount}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function generateTxValidation(
|
||||
to: string,
|
||||
token: Token | null | undefined,
|
||||
data: string,
|
||||
gasLimit: BigNumber | string,
|
||||
gasPrice: Wei | string
|
||||
) {
|
||||
// Reject bad addresses
|
||||
if (!isValidETHAddress(to)) {
|
||||
throw new Error(translateRaw('ERROR_5'));
|
||||
|
@ -106,28 +159,29 @@ export async function generateCompleteTransactionFromRawTransaction(
|
|||
'Gas price too high. Please contact support if this was not a mistake.'
|
||||
);
|
||||
}
|
||||
// build gasCost by multiplying gasPrice * gasLimit
|
||||
}
|
||||
|
||||
export async function generateCompleteTransactionFromRawTransaction(
|
||||
node: INode,
|
||||
tx: ExtendedRawTransaction,
|
||||
wallet: IWallet,
|
||||
token: Token | null | undefined,
|
||||
offline?: boolean
|
||||
): Promise<CompleteTransaction> {
|
||||
const { to, data, gasLimit, gasPrice, chainId, nonce } = tx;
|
||||
// validation
|
||||
generateTxValidation(to, token, data, gasLimit, gasPrice);
|
||||
// duplicated from generateTxValidation -- typescript bug
|
||||
if (typeof gasLimit === 'string' || typeof gasPrice === 'string') {
|
||||
throw Error('Gas Limit and Gas Price should be of type bignumber');
|
||||
}
|
||||
// computed gas cost (gasprice * gaslimit)
|
||||
const gasCost: Wei = new Wei(gasPrice.amount.times(gasLimit));
|
||||
// Ensure their balance exceeds the amount they're sending
|
||||
let value;
|
||||
let balance;
|
||||
const ETHBalance: Wei = await node.getBalance(from);
|
||||
if (token) {
|
||||
value = new Big(ERC20.$transfer(tx.data).value);
|
||||
balance = toTokenUnit(await node.getTokenBalance(tx.from, token), token);
|
||||
} else {
|
||||
value = new Big(tx.value);
|
||||
balance = ETHBalance.amount;
|
||||
}
|
||||
if (value.gt(balance)) {
|
||||
throw new Error(translateRaw('GETH_Balance'));
|
||||
}
|
||||
// ensure gas cost is not greaterThan current eth balance
|
||||
// TODO check that eth balance is not lesser than txAmount + gasCost
|
||||
if (gasCost.amount.gt(ETHBalance.amount)) {
|
||||
throw new Error(
|
||||
`gasCost: ${gasCost.amount} greaterThan ETHBalance: ${ETHBalance.amount}`
|
||||
);
|
||||
// get amount value (either in ETH or in Token)
|
||||
const value = getValue(token, tx);
|
||||
// if not offline, ensure that balance exceeds costs
|
||||
if (!offline) {
|
||||
await balanceCheck(node, tx, token, value, gasCost);
|
||||
}
|
||||
// Taken from v3's `sanitizeHex`, ensures that the value is a %2 === 0
|
||||
// prefix'd hex value.
|
||||
|
@ -141,19 +195,13 @@ export async function generateCompleteTransactionFromRawTransaction(
|
|||
data: data ? cleanHex(data) : '',
|
||||
chainId: chainId || 1
|
||||
};
|
||||
|
||||
// Sign the transaction
|
||||
const rawTxJson = JSON.stringify(cleanedRawTx);
|
||||
const signedTx = await wallet.signRawTransaction(cleanedRawTx);
|
||||
// Repeat all of this shit for Flow typechecking. Sealed objects don't
|
||||
// like spreads, so we have to be explicit.
|
||||
|
||||
return {
|
||||
nonce: cleanedRawTx.nonce,
|
||||
gasPrice: cleanedRawTx.gasPrice,
|
||||
gasLimit: cleanedRawTx.gasLimit,
|
||||
to: cleanedRawTx.to,
|
||||
value: cleanedRawTx.value,
|
||||
data: cleanedRawTx.data,
|
||||
chainId: cleanedRawTx.chainId,
|
||||
...cleanedRawTx,
|
||||
rawTx: rawTxJson,
|
||||
signedTx
|
||||
};
|
||||
|
@ -191,7 +239,9 @@ export async function generateCompleteTransaction(
|
|||
gasPrice: Wei,
|
||||
gasLimit: BigNumber,
|
||||
chainId: number,
|
||||
transactionInput: TransactionInput
|
||||
transactionInput: TransactionInput,
|
||||
nonce?: number | null,
|
||||
offline?: boolean
|
||||
): Promise<CompleteTransaction> {
|
||||
const { token } = transactionInput;
|
||||
const { from, to, value, data } = await formatTxInput(
|
||||
|
@ -199,7 +249,7 @@ export async function generateCompleteTransaction(
|
|||
transactionInput
|
||||
);
|
||||
const transaction: ExtendedRawTransaction = {
|
||||
nonce: await nodeLib.getTransactionCount(from),
|
||||
nonce: nonce ? `0x${nonce}` : await nodeLib.getTransactionCount(from),
|
||||
from,
|
||||
to,
|
||||
gasLimit,
|
||||
|
@ -212,7 +262,8 @@ export async function generateCompleteTransaction(
|
|||
nodeLib,
|
||||
transaction,
|
||||
wallet,
|
||||
token
|
||||
token,
|
||||
offline
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { Ether } from 'libs/units';
|
||||
|
||||
export function stripHexPrefixAndLower(address: string): string {
|
||||
return address.replace('0x', '').toLowerCase();
|
||||
export function stripHexPrefix(value: string) {
|
||||
return value.replace('0x', '');
|
||||
}
|
||||
|
||||
export function stripHexPrefixAndLower(value: string): string {
|
||||
return stripHexPrefix(value).toLowerCase();
|
||||
}
|
||||
|
||||
export function valueToHex(value: Ether): string {
|
||||
|
|
|
@ -2,42 +2,62 @@ import {
|
|||
ChangeGasPriceAction,
|
||||
ChangeLanguageAction,
|
||||
ChangeNodeAction,
|
||||
ConfigAction
|
||||
ConfigAction,
|
||||
ToggleOfflineAction,
|
||||
ForceOfflineAction
|
||||
} from 'actions/config';
|
||||
import { TypeKeys } from 'actions/config/constants';
|
||||
import { languages, NODES } from '../config/data';
|
||||
import { NODES } from '../config/data';
|
||||
|
||||
export interface State {
|
||||
// FIXME
|
||||
languageSelection: string;
|
||||
nodeSelection: string;
|
||||
gasPriceGwei: number;
|
||||
offline: boolean;
|
||||
forceOffline: boolean;
|
||||
}
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
languageSelection: 'en',
|
||||
nodeSelection: Object.keys(NODES)[0],
|
||||
gasPriceGwei: 21
|
||||
gasPriceGwei: 21,
|
||||
offline: false,
|
||||
forceOffline: false
|
||||
};
|
||||
|
||||
function changeLanguage(state: State, action: ChangeLanguageAction): State {
|
||||
return {
|
||||
...state,
|
||||
languageSelection: action.value
|
||||
languageSelection: action.payload
|
||||
};
|
||||
}
|
||||
|
||||
function changeNode(state: State, action: ChangeNodeAction): State {
|
||||
return {
|
||||
...state,
|
||||
nodeSelection: action.value
|
||||
nodeSelection: action.payload
|
||||
};
|
||||
}
|
||||
|
||||
function changeGasPrice(state: State, action: ChangeGasPriceAction): State {
|
||||
return {
|
||||
...state,
|
||||
gasPriceGwei: action.value
|
||||
gasPriceGwei: action.payload
|
||||
};
|
||||
}
|
||||
|
||||
function toggleOffline(state: State, action: ToggleOfflineAction): State {
|
||||
return {
|
||||
...state,
|
||||
offline: !state.offline
|
||||
};
|
||||
}
|
||||
|
||||
function forceOffline(state: State, action: ForceOfflineAction): State {
|
||||
return {
|
||||
...state,
|
||||
forceOffline: !state.forceOffline
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -52,6 +72,10 @@ export function config(
|
|||
return changeNode(state, action);
|
||||
case TypeKeys.CONFIG_GAS_PRICE:
|
||||
return changeGasPrice(state, action);
|
||||
case TypeKeys.CONFIG_TOGGLE_OFFLINE:
|
||||
return toggleOffline(state, action);
|
||||
case TypeKeys.CONFIG_FORCE_OFFLINE:
|
||||
return forceOffline(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,46 @@
|
|||
import { SagaIterator } from 'redux-saga';
|
||||
import { call, put, select, takeEvery } from 'redux-saga/effects';
|
||||
import { changeNode } from 'actions/config';
|
||||
import { delay, SagaIterator } from 'redux-saga';
|
||||
import {
|
||||
call,
|
||||
cancel,
|
||||
fork,
|
||||
put,
|
||||
take,
|
||||
takeLatest,
|
||||
takeEvery,
|
||||
select
|
||||
} from 'redux-saga/effects';
|
||||
import { NODES } from 'config/data';
|
||||
|
||||
import { getNodeConfig } from 'selectors/config';
|
||||
import { AppState } from 'reducers';
|
||||
import { TypeKeys } from 'actions/config/constants';
|
||||
import {
|
||||
toggleOfflineConfig,
|
||||
TToggleOfflineConfig,
|
||||
changeNode
|
||||
} from 'actions/config';
|
||||
import { State as ConfigState } from 'reducers/config';
|
||||
|
||||
export const getConfig = (state: AppState): ConfigState => state.config;
|
||||
|
||||
export function* pollOfflineStatus(): SagaIterator {
|
||||
while (true) {
|
||||
const offline = !navigator.onLine;
|
||||
const config = yield select(getConfig);
|
||||
const offlineState = config.offline;
|
||||
if (offline !== offlineState) {
|
||||
yield put(toggleOfflineConfig());
|
||||
}
|
||||
yield call(delay, 250);
|
||||
}
|
||||
}
|
||||
|
||||
// Fork our recurring API call, watch for the need to cancel.
|
||||
function* handlePollOfflineStatus(): SagaIterator {
|
||||
const pollOfflineStatusTask = yield fork(pollOfflineStatus);
|
||||
yield take('CONFIG_STOP_POLL_OFFLINE_STATE');
|
||||
yield cancel(pollOfflineStatusTask);
|
||||
}
|
||||
|
||||
// @HACK For now we reload the app when doing a language swap to force non-connected
|
||||
// data to reload. Also the use of timeout to avoid using additional actions for now.
|
||||
function* reload(): SagaIterator {
|
||||
|
@ -20,7 +57,11 @@ function* handleNodeChangeIntent(action): SagaIterator {
|
|||
}
|
||||
}
|
||||
|
||||
export default function* handleConfigChanges(): SagaIterator {
|
||||
yield takeEvery('CONFIG_NODE_CHANGE_INTENT', handleNodeChangeIntent);
|
||||
yield takeEvery('CONFIG_LANGUAGE_CHANGE', reload);
|
||||
export default function* configSaga(): SagaIterator {
|
||||
yield takeLatest(
|
||||
TypeKeys.CONFIG_POLL_OFFLINE_STATUS,
|
||||
handlePollOfflineStatus
|
||||
);
|
||||
yield takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent);
|
||||
yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import handleConfigChanges from './config';
|
||||
import configSaga from './config';
|
||||
import contracts from './contracts';
|
||||
import deterministicWallets from './deterministicWallets';
|
||||
import notifications from './notifications';
|
||||
|
@ -12,7 +12,7 @@ import wallet from './wallet';
|
|||
|
||||
export default {
|
||||
bityTimeRemaining,
|
||||
handleConfigChanges,
|
||||
configSaga,
|
||||
postBityOrderSaga,
|
||||
pollBityOrderStatusSaga,
|
||||
getBityRatesSaga,
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
export function getKeyByValue(object, value) {
|
||||
return Object.keys(object).find(key => object[key] === value);
|
||||
}
|
||||
|
||||
export function getParam(query: { [key: string]: string }, key: string) {
|
||||
const keys = Object.keys(query);
|
||||
const index = keys.findIndex(k => k.toLowerCase() === key.toLowerCase());
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
return query[keys[index]];
|
||||
}
|
||||
|
||||
export function isPositiveInteger(n: number) {
|
||||
return Number.isInteger(n) && n > 0;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ describe('actions', () => {
|
|||
const value = 'en';
|
||||
const expectedAction = {
|
||||
type: 'CONFIG_LANGUAGE_CHANGE',
|
||||
value
|
||||
payload: value
|
||||
};
|
||||
expect(changeLanguage(value)).toEqual(expectedAction);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue