[FEATURE] Initial EAC integration.

This commit is contained in:
Daniel Kmak 2018-03-13 15:04:47 +01:00 committed by Bagaric
parent ab2b559dd2
commit f2afd78f4e
44 changed files with 869 additions and 40 deletions

View File

@ -10,7 +10,8 @@ import {
InputDataAction,
InputNonceAction,
ResetAction,
SetGasPriceFieldAction
SetGasPriceFieldAction,
SetWindowStartFieldAction
} from '../actionTypes';
import { TypeKeys } from 'actions/transaction/constants';
@ -80,6 +81,14 @@ const setGasPriceField = (payload: SetGasPriceFieldAction['payload']): SetGasPri
payload
});
type TSetWindowStartField = typeof setWindowStartField;
const setWindowStartField = (
payload: SetWindowStartFieldAction['payload']
): SetWindowStartFieldAction => ({
type: TypeKeys.WINDOW_START_FIELD_SET,
payload
});
type TReset = typeof reset;
const reset = (payload: ResetAction['payload'] = { include: {}, exclude: {} }): ResetAction => ({
type: TypeKeys.RESET,
@ -98,6 +107,7 @@ export {
TSetNonceField,
TSetValueField,
TSetGasPriceField,
TSetWindowStartField,
TReset,
inputGasLimit,
inputGasPrice,
@ -110,5 +120,6 @@ export {
setNonceField,
setValueField,
setGasPriceField,
setWindowStartField,
reset
};

View File

@ -4,4 +4,5 @@ export * from './network';
export * from './sign';
export * from './broadcast';
export * from './current';
export * from './windowStart';
export * from './sendEverything';

View File

@ -0,0 +1,12 @@
import { SetCurrentWindowStartAction } from '../actionTypes/windowStart';
import { TypeKeys } from '../';
type TSetCurrentWindowStart = typeof setCurrentWindowStart;
const setCurrentWindowStart = (
payload: SetCurrentWindowStartAction['payload']
): SetCurrentWindowStartAction => ({
type: TypeKeys.CURRENT_WINDOW_START_SET,
payload
});
export { setCurrentWindowStart, TSetCurrentWindowStart };

View File

@ -75,6 +75,14 @@ interface SetValueFieldAction {
};
}
interface SetWindowStartFieldAction {
type: TypeKeys.WINDOW_START_FIELD_SET;
payload: {
raw: string;
value: number | null;
};
}
type InputFieldAction = InputNonceAction | InputGasLimitAction | InputDataAction;
type FieldAction =
@ -83,7 +91,8 @@ type FieldAction =
| SetToFieldAction
| SetNonceFieldAction
| SetValueFieldAction
| SetGasPriceFieldAction;
| SetGasPriceFieldAction
| SetWindowStartFieldAction;
export {
InputGasLimitAction,
@ -98,5 +107,6 @@ export {
SetValueFieldAction,
FieldAction,
InputFieldAction,
SetGasPriceFieldAction
SetGasPriceFieldAction,
SetWindowStartFieldAction
};

View File

@ -0,0 +1,12 @@
import { TypeKeys } from '../constants';
/* user input */
interface SetCurrentWindowStartAction {
type: TypeKeys.CURRENT_WINDOW_START_SET;
payload: string;
}
type CurrentAction = SetCurrentWindowStartAction;
export { SetCurrentWindowStartAction, CurrentAction };

View File

@ -25,6 +25,7 @@ export enum TypeKeys {
CURRENT_VALUE_SET = 'CURRENT_VALUE_SET',
CURRENT_TO_SET = 'CURRENT_TO_SET',
CURRENT_WINDOW_START_SET = 'CURRENT_WINDOW_START_SET',
DATA_FIELD_INPUT = 'DATA_FIELD_INPUT',
GAS_LIMIT_INPUT = 'GAS_LIMIT_INPUT',
@ -38,6 +39,7 @@ export enum TypeKeys {
VALUE_FIELD_SET = 'VALUE_FIELD_SET',
NONCE_FIELD_SET = 'NONCE_FIELD_SET',
GAS_PRICE_FIELD_SET = 'GAS_PRICE_FIELD_SET',
WINDOW_START_FIELD_SET = 'WINDOW_START_FIELD_SET',
TOKEN_TO_META_SET = 'TOKEN_TO_META_SET',
UNIT_META_SET = 'UNIT_META_SET',

View File

@ -61,6 +61,7 @@ class GenerateTransactionFactoryClass extends Component<Props> {
const isButtonDisabled =
!isFullTransaction || networkRequestPending || !validGasPrice || !validGasLimit;
return (
<React.Fragment>
<WithSigner

View File

@ -0,0 +1,13 @@
import { ScheduleTransactionFactory } from './ScheduleTransactionFactory';
import React from 'react';
import translate from 'translations';
export const ScheduleTransaction: React.SFC<{}> = () => (
<ScheduleTransactionFactory
withProps={({ disabled, isWeb3Wallet, onClick }) => (
<button disabled={disabled} className="btn btn-info btn-block" onClick={onClick}>
{isWeb3Wallet ? translate('SCHEDULE_schedule') : translate('DEP_signtx')}
</button>
)}
/>
);

View File

@ -0,0 +1,20 @@
import { signTransactionRequested, TSignTransactionRequested } from 'actions/transaction';
import React, { Component } from 'react';
import { connect } from 'react-redux';
interface DispatchProps {
signTransactionRequested: TSignTransactionRequested;
}
interface OwnProps {
isWeb3: boolean;
withSigner(signer: TSignTransactionRequested): React.ReactElement<any> | null;
}
class Container extends Component<DispatchProps & OwnProps, {}> {
public render() {
return this.props.withSigner(this.props.signTransactionRequested);
}
}
export const WithSigner = connect(null, { signTransactionRequested })(Container);

View File

@ -0,0 +1,88 @@
import { WithSigner } from './Container';
import EthTx from 'ethereumjs-tx';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import {
getSchedulingTransaction,
isNetworkRequestPending,
isValidGasPrice,
isValidGasLimit,
getGasPrice,
getCurrentTo,
getCurrentValue
} from 'selectors/transaction';
import { getWalletType } from 'selectors/wallet';
import { getWindowStart } from '../../selectors/transaction/fields';
export interface CallbackProps {
disabled: boolean;
isWeb3Wallet: boolean;
onClick(): void;
}
interface StateProps {
transaction: EthTx;
networkRequestPending: boolean;
isFullTransaction: boolean;
isWeb3Wallet: boolean;
validGasPrice: boolean;
validGasLimit: boolean;
isWindowStartValid: boolean;
windowStart: any;
gasPrice: any;
currentTo: any;
currentValue: any;
}
interface OwnProps {
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
type Props = OwnProps & StateProps;
class ScheduleTransactionFactoryClass extends Component<Props> {
public render() {
const {
isFullTransaction,
isWeb3Wallet,
networkRequestPending,
validGasPrice,
validGasLimit,
isWindowStartValid,
transaction
} = this.props;
const isButtonDisabled =
!isWindowStartValid ||
!isFullTransaction ||
networkRequestPending ||
!validGasPrice ||
!validGasLimit;
return (
<WithSigner
isWeb3={isWeb3Wallet}
withSigner={signer =>
this.props.withProps({
disabled: isButtonDisabled,
isWeb3Wallet,
onClick: () => signer(transaction)
})
}
/>
);
}
}
export const ScheduleTransactionFactory = connect((state: AppState) => ({
...getSchedulingTransaction(state),
networkRequestPending: isNetworkRequestPending(state),
isWeb3Wallet: getWalletType(state).isWeb3Wallet,
validGasPrice: isValidGasPrice(state),
validGasLimit: isValidGasLimit(state),
windowStart: getWindowStart(state),
gasPrice: getGasPrice(state),
currentTo: getCurrentTo(state),
currentValue: getCurrentValue(state)
}))(ScheduleTransactionFactoryClass);

View File

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

View File

@ -50,6 +50,7 @@ interface OwnProps {
disableToggle?: boolean;
advancedGasOptions?: AdvancedOptions;
className?: string;
scheduling?: boolean;
}
type Props = DispatchProps & OwnProps & StateProps;
@ -90,7 +91,7 @@ class TXMetaDataPanel extends React.Component<Props, State> {
}
public render() {
const { offline, disableToggle, advancedGasOptions, className = '' } = this.props;
const { offline, disableToggle, advancedGasOptions, className = '', scheduling } = this.props;
const { gasPrice } = this.state;
const showAdvanced = this.state.sliderState === 'advanced' || offline;
return (
@ -100,12 +101,14 @@ class TXMetaDataPanel extends React.Component<Props, State> {
gasPrice={gasPrice}
inputGasPrice={this.props.inputGasPrice}
options={advancedGasOptions}
scheduling={scheduling}
/>
) : (
<SimpleGas
gasPrice={gasPrice}
inputGasPrice={this.handleGasPriceInput}
setGasPrice={this.props.inputGasPrice}
scheduling={scheduling}
/>
)}

View File

@ -11,6 +11,8 @@ import { getAutoGasLimitEnabled } from 'selectors/config';
import { isValidGasPrice } from 'selectors/transaction';
import { sanitizeNumericalInput } from 'libs/values';
import { Input } from 'components/ui';
import SchedulingFeeSummary from './SchedulingFeeSummary';
import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling';
export interface AdvancedOptions {
gasPriceField?: boolean;
@ -24,6 +26,7 @@ interface OwnProps {
inputGasPrice: TInputGasPrice;
gasPrice: AppState['transaction']['fields']['gasPrice'];
options?: AdvancedOptions;
scheduling?: boolean;
}
interface StateProps {
@ -54,8 +57,9 @@ class AdvancedGas extends React.Component<Props, State> {
};
public render() {
const { autoGasLimitEnabled, gasPrice, validGasPrice } = this.props;
const { autoGasLimitEnabled, gasPrice, validGasPrice, scheduling } = this.props;
const { gasPriceField, gasLimitField, nonceField, dataField, feeSummary } = this.state.options;
return (
<div className="AdvancedGas row form-group">
<div className="AdvancedGas-calculate-limit">
@ -107,7 +111,8 @@ class AdvancedGas extends React.Component<Props, State> {
</div>
)}
{feeSummary && (
{!scheduling &&
feeSummary && (
<div className="AdvancedGas-fee-summary">
<FeeSummary
gasPrice={gasPrice}
@ -119,6 +124,26 @@ class AdvancedGas extends React.Component<Props, State> {
/>
</div>
)}
{scheduling &&
feeSummary && (
<div className="AdvancedGas-fee-summary">
<SchedulingFeeSummary
gasPrice={gasPrice}
render={({ gasPriceWei, gasLimit, fee, usd }) => (
<div>
<span>
{EAC_SCHEDULING_CONFIG.PAYMENT} + {gasPriceWei} * ({
EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT
}{' '}
+ {EAC_SCHEDULING_CONFIG.FUTURE_EXECUTION_COST} + {gasLimit}) = {fee}{' '}
{usd && <span>~= ${usd} USD</span>}
</span>
</div>
)}
/>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,106 @@
import React from 'react';
import BN from 'bn.js';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getNetworkConfig, getOffline } from 'selectors/config';
import { getIsEstimating } from 'selectors/gas';
import { getGasLimit } from 'selectors/transaction';
import { UnitDisplay, Spinner } from 'components/ui';
import { NetworkConfig } from 'types/network';
import './FeeSummary.scss';
import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling';
interface RenderData {
gasPriceWei: string;
gasPriceGwei: string;
gasLimit: string;
fee: React.ReactElement<string>;
usd: React.ReactElement<string> | null;
}
interface ReduxStateProps {
gasLimit: AppState['transaction']['fields']['gasLimit'];
rates: AppState['rates']['rates'];
network: NetworkConfig;
isOffline: AppState['config']['meta']['offline'];
isGasEstimating: AppState['gas']['isEstimating'];
}
interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
render(data: RenderData): React.ReactElement<string> | string;
}
type Props = OwnProps & ReduxStateProps;
class SchedulingFeeSummary extends React.Component<Props> {
public render() {
const { gasPrice, gasLimit, rates, network, isOffline, isGasEstimating } = this.props;
if (isGasEstimating) {
return (
<div className="FeeSummary is-loading">
<Spinner />
</div>
);
}
const feeBig =
gasPrice.value &&
gasLimit.value &&
gasPrice.value
.mul(
gasLimit.value
.add(new BN(EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT))
.add(new BN(EAC_SCHEDULING_CONFIG.FUTURE_EXECUTION_COST))
)
.add(new BN(EAC_SCHEDULING_CONFIG.PAYMENT));
const fee = (
<UnitDisplay
value={feeBig}
unit="ether"
symbol={network.unit}
displayShortBalance={6}
checkOffline={false}
/>
);
const usdBig = network.isTestnet
? new BN(0)
: feeBig && rates[network.unit] && feeBig.muln(rates[network.unit].USD);
const usd = isOffline ? null : (
<UnitDisplay
value={usdBig}
unit="ether"
displayShortBalance={2}
displayTrailingZeroes={true}
checkOffline={true}
/>
);
return (
<div className="FeeSummary">
{this.props.render({
gasPriceWei: gasPrice.value.toString(),
gasPriceGwei: gasPrice.raw,
fee,
usd,
gasLimit: gasLimit.raw
})}
</div>
);
}
}
function mapStateToProps(state: AppState): ReduxStateProps {
return {
gasLimit: getGasLimit(state),
rates: state.rates.rates,
network: getNetworkConfig(state),
isOffline: getOffline(state),
isGasEstimating: getIsEstimating(state)
};
}
export default connect(mapStateToProps)(SchedulingFeeSummary);

View File

@ -1,7 +1,6 @@
import React from 'react';
import Slider, { createSliderWithTooltip } from 'rc-slider';
import translate from 'translations';
import FeeSummary from './FeeSummary';
import translate, { translateRaw } from 'translations';
import './SimpleGas.scss';
import { AppState } from 'reducers';
import {
@ -17,11 +16,14 @@ import { Wei, fromWei } from 'libs/units';
import { gasPriceDefaults } from 'config';
import { InlineSpinner } from 'components/ui/InlineSpinner';
import { TInputGasPrice } from 'actions/transaction';
import SchedulingFeeSummary from './SchedulingFeeSummary';
const SliderWithTooltip = createSliderWithTooltip(Slider);
interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
setGasPrice: TInputGasPrice;
scheduling?: boolean;
inputGasPrice(rawGas: string): void;
}
@ -112,7 +114,7 @@ class SimpleGas extends React.Component<Props> {
<span>{translate('TX_FEE_SCALE_RIGHT')}</span>
</div>
</div>
<FeeSummary
<SchedulingFeeSummary
gasPrice={gasPrice}
render={({ fee, usd }) => (
<span>

View File

@ -0,0 +1,29 @@
import React from 'react';
import translate from 'translations';
import { Input } from 'components/ui';
import { WindowStartFieldFactory } from './WindowStartFieldFactory';
interface Props {
isReadOnly?: boolean;
}
export const WindowStartField: React.SFC<Props> = ({ isReadOnly }) => (
<WindowStartFieldFactory
withProps={({ currentWindowStart, isValid, onChange, readOnly }) => (
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">{translate('SCHEDULE_block')}</div>
<Input
className={`input-group-input ${isValid ? '' : 'invalid'}`}
type="text"
value={currentWindowStart.raw}
placeholder={translate('SCHEDULE_block_placeholder', true)}
readOnly={!!(isReadOnly || readOnly)}
spellCheck={false}
onChange={onChange}
/>
</label>
</div>
)}
/>
);

View File

@ -0,0 +1,63 @@
import { Query } from 'components/renderCbs';
import { setCurrentWindowStart, TSetCurrentWindowStart } from 'actions/transaction';
import { WindowStartInputFactory } from './WindowStartInputFactory';
import React from 'react';
import { connect } from 'react-redux';
import { ICurrentWindowStart } from 'selectors/transaction';
interface DispatchProps {
setCurrentWindowStart: TSetCurrentWindowStart;
}
interface OwnProps {
windowStart: string | null;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
export interface CallbackProps {
isValid: boolean;
readOnly: boolean;
currentWindowStart: ICurrentWindowStart;
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
type Props = DispatchProps & OwnProps;
class WindowStartFieldFactoryClass extends React.Component<Props> {
public componentDidMount() {
const { windowStart } = this.props;
if (windowStart) {
this.props.setCurrentWindowStart(windowStart);
}
}
public render() {
return (
<WindowStartInputFactory onChange={this.setWindowStart} withProps={this.props.withProps} />
);
}
private setWindowStart = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.setCurrentWindowStart(value);
};
}
const WindowStartFieldFactory = connect(null, { setCurrentWindowStart })(
WindowStartFieldFactoryClass
);
interface DefaultWindowStartFieldProps {
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
const DefaultWindowStartField: React.SFC<DefaultWindowStartFieldProps> = ({ withProps }) => (
<Query
params={['windowStart']}
withQuery={({ windowStart }) => (
<WindowStartFieldFactory windowStart={windowStart} withProps={withProps} />
)}
/>
);
export { DefaultWindowStartField as WindowStartFieldFactory };

View File

@ -0,0 +1,54 @@
import React, { Component } from 'react';
import { Query } from 'components/renderCbs';
import {
getCurrentWindowStart,
isValidCurrentWindowStart,
ICurrentWindowStart
} from 'selectors/transaction';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { CallbackProps } from 'components/WindowStartFieldFactory';
import { getResolvingDomain } from 'selectors/ens';
interface StateProps {
currentWindowStart: ICurrentWindowStart;
isValid: boolean;
isResolving: boolean;
}
interface OwnProps {
onChange(ev: React.FormEvent<HTMLInputElement>): void;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
type Props = OwnProps & StateProps;
class WindowStartInputFactoryClass extends Component<Props> {
public render() {
const { currentWindowStart, onChange, isValid, withProps } = this.props;
return (
<div className="row form-group">
<div className="col-xs-11">
<Query
params={['readOnly']}
withQuery={({ readOnly }) =>
withProps({
currentWindowStart,
isValid,
onChange,
readOnly: !!readOnly || this.props.isResolving
})
}
/>
</div>
</div>
);
}
}
export const WindowStartInputFactory = connect((state: AppState) => ({
currentWindowStart: getCurrentWindowStart(state),
isResolving: getResolvingDomain(state),
isValid: isValidCurrentWindowStart(state)
}))(WindowStartInputFactoryClass);

View File

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

View File

@ -8,6 +8,8 @@ export * from './CurrentCustomMessage';
export * from './GenerateTransaction';
export * from './SendButton';
export * from './SigningStatus';
export * from './WindowStartField';
export * from './ScheduleTransaction';
export { default as NonceField } from './NonceField';
export { default as Header } from './Header';
export { default as Footer } from './Footer';

View File

@ -13,7 +13,15 @@ interface IQueryResults {
[key: string]: string | null;
}
export type Param = 'to' | 'data' | 'readOnly' | 'tokenSymbol' | 'value' | 'gaslimit' | 'limit';
export type Param =
| 'to'
| 'data'
| 'readOnly'
| 'tokenSymbol'
| 'value'
| 'gaslimit'
| 'limit'
| 'windowStart';
interface Props extends RouteComponentProps<{}> {
params: Param[];

View File

@ -0,0 +1,90 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { isAnyOfflineWithWeb3 } from 'selectors/derived';
import {
AddressField,
AmountField,
TXMetaDataPanel,
SendEverything,
CurrentCustomMessage,
ScheduleTransaction,
SendButton,
SigningStatus,
WindowStartField
} from 'components';
import { OnlyUnlocked, WhenQueryExists } from 'components/renderCbs';
import translate from 'translations';
import { AppState } from 'reducers';
import { NonStandardTransaction } from './components';
const content = (
<div className="Tab-content-pane">
<AddressField />
<div className="row form-group">
<div className="col-xs-12">
<AmountField hasUnitDropdown={true} />
<SendEverything />
</div>
</div>
<div className="row form-group">
<div className="col-xs-12">
<WindowStartField />
</div>
</div>
<div className="row form-group">
<div className="col-xs-12">
<TXMetaDataPanel scheduling={true} />
</div>
</div>
<CurrentCustomMessage />
<NonStandardTransaction />
<div className="row form-group">
<div className="col-xs-12 clearfix">
<ScheduleTransaction />
</div>
</div>
<SigningStatus />
<div className="row form-group">
<SendButton />
</div>
</div>
);
const QueryWarning: React.SFC<{}> = () => (
<WhenQueryExists
whenQueryExists={
<div className="alert alert-info">
<p>{translate('WARN_Send_Link')}</p>
</div>
}
/>
);
interface StateProps {
shouldDisplay: boolean;
}
class SchedulingFieldsClass extends Component<StateProps> {
public render() {
const { shouldDisplay } = this.props;
return (
<OnlyUnlocked
whenUnlocked={
<React.Fragment>
<QueryWarning />
{shouldDisplay ? content : null}
</React.Fragment>
}
/>
);
}
}
export const SchedulingFields = connect((state: AppState) => ({
shouldDisplay: !isAnyOfflineWithWeb3(state)
}))(SchedulingFieldsClass);

View File

@ -1 +1,2 @@
export * from './Fields';
export * from './SchedulingFields';

View File

@ -0,0 +1,11 @@
import React from 'react';
import { SchedulingFields, UnavailableWallets } from 'containers/Tabs/SendTransaction/components';
export default function() {
return (
<React.Fragment>
<SchedulingFields />
<UnavailableWallets />
</React.Fragment>
);
}

View File

@ -3,4 +3,4 @@ export * from './UnavailableWallets';
export * from './SideBar';
export { default as WalletInfo } from './WalletInfo';
export { default as RequestPayment } from './RequestPayment';
export { default as RecentTransactions } from './RecentTransactions';
export { default as SchedulePayment } from './SchedulePayment';

View File

@ -13,11 +13,12 @@ import {
RecentTransactions,
Fields,
UnavailableWallets,
SideBar
} from './components';
SchedulePayment
} from 'containers/Tabs/SendTransaction/components';
import SubTabs, { Tab } from 'components/SubTabs';
import { RouteNotFound } from 'components/RouteNotFound';
import { isNetworkUnit } from 'selectors/config/wallet';
import { getNetworkConfig } from 'selectors/config/networks';
const Send = () => (
<React.Fragment>
@ -29,6 +30,7 @@ const Send = () => (
interface StateProps {
wallet: AppState['wallet']['inst'];
requestDisabled: boolean;
scheduleDisabled: boolean;
}
type Props = StateProps & RouteComponentProps<{}>;
@ -43,9 +45,14 @@ class SendTransaction extends React.Component<Props> {
name: translate('NAV_SENDETHER'),
disabled: !!wallet && !!wallet.isReadOnly
},
{
path: 'schedule',
name: translate('NAV_SchedulePayment'),
disabled: (!!wallet && !!wallet.isReadOnly) || this.props.scheduleDisabled
},
{
path: 'request',
name: translate('NAV_REQUESTPAYMENT'),
name: translate('Request Payment'),
disabled: this.props.requestDisabled
},
{
@ -97,9 +104,15 @@ class SendTransaction extends React.Component<Props> {
render={() => <RequestPayment wallet={wallet} />}
/>
<Route
path={`${currentPath}/recent-txs`}
path={`${currentPath}/schedule`}
exact={true}
render={() => <RecentTransactions wallet={wallet} />}
render={() => {
return wallet.isReadOnly || this.props.scheduleDisabled ? (
<Redirect to={`${currentPath}/info`} />
) : (
<SchedulePayment />
);
}}
/>
<RouteNotFound />
</Switch>
@ -115,5 +128,6 @@ class SendTransaction extends React.Component<Props> {
export default connect((state: AppState) => ({
wallet: getWalletInst(state),
requestDisabled: !isNetworkUnit(state, 'ETH')
requestDisabled: !isNetworkUnit(state, 'ETH'),
scheduleDisabled: getNetworkConfig(state).name !== 'Kovan'
}))(SendTransaction);

View File

@ -0,0 +1,7 @@
export const EAC_SCHEDULING_CONFIG = {
SCHEDULING_GAS_LIMIT: 1500000,
FUTURE_EXECUTION_COST: 180000,
FEE_MULTIPLIER: 2,
PAYMENT: 10000000,
WINDOW_SIZE_IN_BLOCKS: 90
};

View File

@ -18,6 +18,7 @@ const INITIAL_STATE: State = {
data: { raw: '', value: null },
nonce: { raw: '', value: null },
value: { raw: '', value: null },
windowStart: { raw: '', value: null },
gasLimit: { raw: '21000', value: new BN(21000) },
gasPrice: { raw: '20', value: gasPricetoBase(20) }
};
@ -69,6 +70,8 @@ export const fields = (
return updateField('nonce')(state, action);
case TK.GAS_PRICE_FIELD_SET:
return updateField('gasPrice')(state, action);
case TK.WINDOW_START_FIELD_SET:
return updateField('windowStart')(state, action);
case TK.TOKEN_TO_ETHER_SWAP:
return tokenToEther(state, action);
case TK.ETHER_TO_TOKEN_SWAP:

View File

@ -2,7 +2,8 @@ import {
SetToFieldAction,
SetDataFieldAction,
SetNonceFieldAction,
SetGasLimitFieldAction
SetGasLimitFieldAction,
SetWindowStartFieldAction
} from 'actions/transaction';
import { Wei } from 'libs/units';
@ -10,6 +11,7 @@ export interface State {
to: SetToFieldAction['payload'];
data: SetDataFieldAction['payload'];
nonce: SetNonceFieldAction['payload'];
windowStart: SetWindowStartFieldAction['payload'];
value: { raw: string; value: Wei | null }; // TODO: fix this workaround since some of the payload is optional
gasLimit: SetGasLimitFieldAction['payload'];
gasPrice: { raw: string; value: Wei };

View File

@ -0,0 +1,25 @@
import { setWindowStartField } from 'actions/transaction/actionCreators/fields';
import { call, put, takeLatest } from 'redux-saga/effects';
import { SagaIterator } from 'redux-saga';
import { TypeKeys } from 'actions/transaction/constants';
import { SetWindowStartFieldAction } from 'actions/transaction';
import { SetCurrentWindowStartAction } from 'actions/transaction/actionTypes/windowStart';
export function* setCurrentWindowStart({
payload: raw
}: SetCurrentWindowStartAction): SagaIterator {
let value: number | null = null;
value = parseInt(raw, 10);
yield call(setField, { value, raw });
}
export function* setField(payload: SetWindowStartFieldAction['payload']) {
yield put(setWindowStartField(payload));
}
export const currentWindowStart = takeLatest(
[TypeKeys.CURRENT_WINDOW_START_SET],
setCurrentWindowStart
);

View File

@ -1,3 +1,5 @@
import { currentTo } from './currentTo';
import { currentValue } from './currentValue';
export const current = [currentTo, ...currentValue];
import { currentWindowStart } from './currentWindowStart';
export const current = [currentTo, ...currentValue, currentWindowStart];

View File

@ -10,6 +10,7 @@ const getGasLimit = (state: AppState) => getFields(state).gasLimit;
const getGasPrice = (state: AppState) => getFields(state).gasPrice;
const getValue = (state: AppState) => getFields(state).value;
const getNonce = (state: AppState) => getFields(state).nonce;
const getWindowStart = (state: AppState) => getFields(state).windowStart;
const getDataExists = (state: AppState) => {
const { value } = getData(state);
@ -35,5 +36,6 @@ export {
getNonce,
getGasPrice,
getDataExists,
getValidGasCost
getValidGasCost,
getWindowStart
};

View File

@ -28,11 +28,15 @@ export const isFullTx = (
) => {
const { data, value, to, ...rest } = transactionFields;
const partialParamsToCheck = { ...rest };
delete partialParamsToCheck.windowStart;
const validPartialParams = Object.values(partialParamsToCheck).reduce<boolean>(
(isValid, v: AppState['transaction']['fields'] & ICurrentTo & ICurrentValue) =>
isValid && !!v.value,
true
);
if (isNetworkUnit(state, unit)) {
// if theres data we can have no current value, and we dont have to check for a to address
if (dataExists && validGasCost && !currentValue.value && currentValue.raw === '') {
@ -55,3 +59,12 @@ export const isFullTx = (
);
}
};
export const isWindowStartValid = (
transactionFields: AppState['transaction']['fields'],
latestBlock: string
) => {
const { windowStart } = transactionFields;
return Boolean(windowStart && windowStart.value && windowStart.value > parseInt(latestBlock, 10));
};

View File

@ -4,4 +4,5 @@ export * from './fields';
export * from './meta';
export * from './sign';
export * from './current';
export * from './windowStart';
export * from './network';

View File

@ -1,10 +1,10 @@
import { AppState } from 'reducers';
import { getCurrentTo, getCurrentValue } from './current';
import { getFields } from './fields';
import { getFields, getData, getWindowStart, getNonce } from './fields';
import { makeTransaction, IHexStrTransaction } from 'libs/transaction';
import EthTx from 'ethereumjs-tx';
import { getUnit } from 'selectors/transaction/meta';
import { reduceToValues, isFullTx } from 'selectors/transaction/helpers';
import { reduceToValues, isFullTx, isWindowStartValid } from 'selectors/transaction/helpers';
import {
getGasPrice,
getGasLimit,
@ -13,9 +13,12 @@ import {
getValidGasCost,
isEtherTransaction
} from 'selectors/transaction';
import { Wei } from 'libs/units';
import { Wei, Address } from 'libs/units';
import { getTransactionFields } from 'libs/transaction/utils/ether';
import { getNetworkConfig } from 'selectors/config';
import { getNetworkConfig, getLatestBlock } from 'selectors/config';
import BN from 'bn.js';
import abi from 'ethereumjs-abi';
import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling';
const getTransactionState = (state: AppState) => state.transaction;
@ -24,6 +27,12 @@ export interface IGetTransaction {
isFullTransaction: boolean; //if the user has filled all the fields
}
export interface IGetSchedulingTransaction {
transaction: EthTx;
isFullTransaction: boolean;
isWindowStartValid: boolean;
}
const getTransaction = (state: AppState): IGetTransaction => {
const currentTo = getCurrentTo(state);
const currentValue = getCurrentValue(state);
@ -46,6 +55,132 @@ const getTransaction = (state: AppState): IGetTransaction => {
return { transaction, isFullTransaction };
};
const getScheduleData = (
toAddress: string,
callData = '',
callGas: number,
callValue: BN | null,
windowSize: number,
windowStart: any,
gasPrice: BN | null,
fee: number,
payment: any,
requiredDeposit: any
) => {
if (!callValue || !gasPrice || !windowStart) {
return;
}
return abi.simpleEncode('schedule(address,bytes,uint[8]):(address)', toAddress, callData, [
callGas,
callValue,
windowSize,
windowStart,
gasPrice,
fee,
payment,
requiredDeposit
]);
};
const calcEACEndowment = (
callGas: number | string | BN,
callValue: number | string | BN,
gasPrice: number | string | BN,
fee: number | string | BN,
payment: number | string | BN
) => {
const callGasBN = new BN(callGas);
const callValueBN = new BN(callValue);
const gasPriceBN = new BN(gasPrice);
const feeBN = new BN(fee);
const paymentBN = new BN(payment);
return paymentBN
.add(feeBN.mul(new BN(2)))
.add(callGasBN.mul(gasPriceBN))
.add(gasPriceBN.mul(new BN(EAC_SCHEDULING_CONFIG.FUTURE_EXECUTION_COST)))
.add(callValueBN);
};
const getSchedulingTransaction = (state: AppState): IGetSchedulingTransaction => {
const currentTo = getCurrentTo(state);
const currentValue = getCurrentValue(state);
const transactionFields = getFields(state);
const unit = getUnit(state);
const dataExists = getDataExists(state);
const callData = getData(state);
const validGasCost = getValidGasCost(state);
const windowStart = getWindowStart(state);
const gasLimit = getGasLimit(state);
const nonce = getNonce(state);
const gasPrice = getGasPrice(state);
const isFullTransaction = isFullTx(
state,
transactionFields,
currentTo,
currentValue,
dataExists,
validGasCost,
unit
);
const WINDOW_SIZE_IN_BLOCKS = EAC_SCHEDULING_CONFIG.WINDOW_SIZE_IN_BLOCKS;
const SCHEDULING_GAS_LIMIT = new BN(EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT);
const EAC_FEE = 0;
const PAYMENT = EAC_SCHEDULING_CONFIG.PAYMENT;
const REQUIRED_DEPOSIT = 0;
const EAC_ADDRESSES = {
KOVAN: {
blockScheduler: '0x1afc19a7e642761ba2b55d2a45b32c7ef08269d1'
}
};
const transactionData = getScheduleData(
currentTo.raw,
callData.raw,
parseInt(gasLimit.raw, 10),
currentValue.value,
WINDOW_SIZE_IN_BLOCKS,
windowStart.value,
gasPrice.value,
EAC_FEE,
PAYMENT,
REQUIRED_DEPOSIT
);
const endowment = calcEACEndowment(
gasLimit.value || 21000,
currentValue.value || 0,
gasPrice.value,
EAC_FEE,
PAYMENT
);
const transactionOptions = {
to: Address(EAC_ADDRESSES.KOVAN.blockScheduler),
data: transactionData,
gasLimit: SCHEDULING_GAS_LIMIT,
gasPrice: gasPrice.value,
nonce: new BN(0),
value: endowment
};
if (nonce) {
transactionOptions.nonce = new BN(nonce.raw);
}
const transaction: EthTx = makeTransaction(transactionOptions);
return {
transaction,
isFullTransaction,
isWindowStartValid: isWindowStartValid(transactionFields, getLatestBlock(state))
};
};
const nonStandardTransaction = (state: AppState): boolean => {
const etherTransaction = isEtherTransaction(state);
const { isFullTransaction } = getTransaction(state);
@ -89,9 +224,11 @@ const serializedAndTransactionFieldsMatch = (state: AppState, isLocallySigned: b
};
export {
getSchedulingTransaction,
getTransaction,
getTransactionState,
getGasCost,
nonStandardTransaction,
serializedAndTransactionFieldsMatch
serializedAndTransactionFieldsMatch,
getScheduleData
};

View File

@ -0,0 +1,22 @@
import { AppState } from 'reducers';
import { getWindowStart } from './fields';
import { getLatestBlock } from '../config';
interface ICurrentWindowStart {
raw: string;
value: number | null;
}
const isValidCurrentWindowStart = (state: AppState) => {
const currentWindowStart = getWindowStart(state);
if (!currentWindowStart.value) {
return false;
}
return currentWindowStart.value > parseInt(getLatestBlock(state), 10);
};
const getCurrentWindowStart = (state: AppState): ICurrentWindowStart => getWindowStart(state);
export { getCurrentWindowStart, ICurrentWindowStart, isValidCurrentWindowStart };

View File

@ -105,6 +105,27 @@
"SCAN_TOKENS_FAIL": "Failed to fetch token values",
"SCAN_TOKENS_FAIL_NO_TOKENS": "No tokens found",
"SCAN_TOKENS_OFFLINE": "Token balances are unavailable offline",
"SCHEDULING_TOGGLE": "Send Later ",
"SCHEDULING_TITLE": "Scheduled Transaction Settings",
"SCHEDULING_DESCRIPTION": "This allows you to schedule a transaction for sending at a later time. Due to unforeseen circumstances (like the state of the network), we cannot 100% guarantee that your transaction is sent in the time period you specify.",
"SCHEDULE_BLOCK": "Block Number ",
"SCHEDULE_BLOCK_PLACEHOLDER": "Enter Block Number ",
"SCHEDULE_DEPOSIT": "Require Deposit (optional) ",
"SCHEDULE_DEPOSIT_PLACEHOLDER": "0.00001 ",
"SCHEDULE_DEPOSIT_TOOLTIP": "Require TimeNode to deposit a given amount of ETH in order to gain an exclusive time window for execution.",
"SCHEDULE_TIMESTAMP": "Date & Time ",
"SCHEDULE_TIMEZONE": "Timezone ",
"SCHEDULE_TIMEBOUNTY": "Time Bounty ",
"SCHEDULE_TIMEBOUNTY_PLACEHOLDER": "Enter Time Bounty ",
"SCHEDULE_TIMEBOUNTY_TOOLTIP": "The amount of ETH you wish to offer to TimeNodes in exchange for execution. The higher the Time Bounty, the likelier your transaction will get executed. ",
"SCHEDULE_CHECK": "Check on Chronos ",
"SCHEDULE_SCHEDULE": "Schedule transaction ",
"SCHEDULE_TYPE_TIME": "Minutes ",
"SCHEDULE_TYPE_BLOCK": "Blocks ",
"SCHEDULE_GAS_PRICE": "Future Gas Price",
"SCHEDULE_GAS_LIMIT": "Future Gas Limit",
"SCHEDULE_WINDOW_SIZE": "Window ",
"SCHEDULE_WINDOW_SIZE_TOOLTIP": "Window ",
"SEND_GAS": "Gas ",
"SEND_TRANSFERTOTAL": "Send Entire Balance ",
"SEND_GENERATE": "Generate Transaction ",

View File

@ -1,6 +1,6 @@
declare module 'ethereumjs-abi' {
import BN from 'bn.js';
type Values = (string | number | BN)[];
type Values = (string | number | BN | Array<any>)[];
type Types = string[];
export function eventID(name: string, types: Types): Buffer;
export function methodID(name: string, types: Types): Buffer;
@ -9,7 +9,7 @@ declare module 'ethereumjs-abi' {
types: Types,
data: string | Buffer
): (Buffer | boolean | number | BN | string)[];
export function simpleEncode(method: string, values: Values): Buffer;
export function simpleEncode(method: string, ...values: Values): Buffer;
export function simpleDecode(
method: string,
data: string | Buffer

3
shared/types/eac.js-lib.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
declare module 'eac.js-lib' {
export default function(web3: any): any;
}

View File

@ -42,5 +42,6 @@ exports[`render snapshot 1`] = `
}
}
requestDisabled={false}
scheduleDisabled={true}
/>
`;

View File

@ -11,7 +11,8 @@ describe('fields reducer', () => {
nonce: { raw: '', value: null },
value: { raw: '', value: null },
gasLimit: { raw: '21000', value: new BN(21000) },
gasPrice: { raw: '20', value: gasPricetoBase(20) }
gasPrice: { raw: '20', value: gasPricetoBase(20) },
windowStart: { raw: '', value: null }
};
const testPayload = { raw: 'test', value: null };

View File

@ -39,6 +39,10 @@ describe('fields selector', () => {
gasPrice: {
raw: '1500',
value: Wei('1500')
},
windowStart: {
raw: '',
value: null
}
};

View File

@ -43,6 +43,10 @@ describe('helpers selector', () => {
gasPrice: {
raw: '1500',
value: Wei('1500')
},
windowStart: {
raw: '',
value: null
}
}
};
@ -54,7 +58,8 @@ describe('helpers selector', () => {
gasPrice: Wei('1500'),
nonce: new BN('0'),
to: new Buffer([0, 1, 2, 3]),
value: Wei('1000000000')
value: Wei('1000000000'),
windowStart: null
};
expect(reduceToValues(state.transaction.fields)).toEqual(values);
});