[FEATURE] Add Window Size field.

This commit is contained in:
Daniel Kmak 2018-03-29 15:04:04 +02:00 committed by Bagaric
parent 09b1062db1
commit 68cc3182ca
33 changed files with 291 additions and 18 deletions

View File

@ -12,6 +12,7 @@ import {
ResetAction,
SetGasPriceFieldAction,
SetTimeBountyFieldAction,
SetWindowSizeFieldAction,
SetWindowStartFieldAction,
SetScheduleTimestampFieldAction,
SetScheduleTypeAction
@ -92,6 +93,14 @@ const setGasPriceField = (payload: SetGasPriceFieldAction['payload']): SetGasPri
payload
});
type TSetWindowSizeField = typeof setWindowSizeField;
const setWindowSizeField = (
payload: SetWindowSizeFieldAction['payload']
): SetWindowSizeFieldAction => ({
type: TypeKeys.WINDOW_SIZE_FIELD_SET,
payload
});
type TSetWindowStartField = typeof setWindowStartField;
const setWindowStartField = (
payload: SetWindowStartFieldAction['payload']
@ -132,6 +141,7 @@ export {
TSetNonceField,
TSetValueField,
TSetGasPriceField,
TSetWindowSizeField,
TSetWindowStartField,
TSetTimeBountyField,
TSetScheduleTimestampField,
@ -149,6 +159,7 @@ export {
setNonceField,
setValueField,
setGasPriceField,
setWindowSizeField,
setWindowStartField,
setScheduleTimestampField,
setScheduleType,

View File

@ -5,6 +5,7 @@ export * from './sign';
export * from './broadcast';
export * from './current';
export * from './timeBounty';
export * from './windowSize';
export * from './windowStart';
export * from './scheduleTimestamp';
export * from './scheduleType';

View File

@ -0,0 +1,12 @@
import { SetCurrentWindowSizeAction } from '../actionTypes/windowSize';
import { TypeKeys } from '../';
type TSetCurrentWindowSize = typeof setCurrentWindowSize;
const setCurrentWindowSize = (
payload: SetCurrentWindowSizeAction['payload']
): SetCurrentWindowSizeAction => ({
type: TypeKeys.CURRENT_WINDOW_SIZE_SET,
payload
});
export { setCurrentWindowSize, TSetCurrentWindowSize };

View File

@ -91,6 +91,14 @@ interface SetValueFieldAction {
};
}
interface SetWindowSizeFieldAction {
type: TypeKeys.WINDOW_SIZE_FIELD_SET;
payload: {
raw: string;
value: number | null;
};
}
interface SetWindowStartFieldAction {
type: TypeKeys.WINDOW_START_FIELD_SET;
payload: {
@ -125,6 +133,7 @@ type FieldAction =
| SetValueFieldAction
| SetGasPriceFieldAction
| SetTimeBountyFieldAction
| SetWindowSizeFieldAction
| SetWindowStartFieldAction
| SetScheduleTimestampFieldAction
| SetScheduleTypeAction;
@ -146,6 +155,7 @@ export {
InputFieldAction,
SetGasPriceFieldAction,
SetTimeBountyFieldAction,
SetWindowSizeFieldAction,
SetWindowStartFieldAction,
SetScheduleTimestampFieldAction,
SetScheduleTypeAction

View File

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

View File

@ -26,6 +26,7 @@ export enum TypeKeys {
CURRENT_VALUE_SET = 'CURRENT_VALUE_SET',
CURRENT_TO_SET = 'CURRENT_TO_SET',
CURRENT_TIME_BOUNTY_SET = 'CURRENT_TIME_BOUNTY_SET',
CURRENT_WINDOW_SIZE_SET = 'CURRENT_WINDOW_SIZE_SET',
CURRENT_WINDOW_START_SET = 'CURRENT_WINDOW_START_SET',
CURRENT_SCHEDULE_TIMESTAMP_SET = 'CURRENT_SCHEDULE_TIMESTAMP_SET',
CURRENT_SCHEDULE_TYPE = 'CURRENT_SCHEDULE_TYPE',
@ -45,6 +46,7 @@ export enum TypeKeys {
NONCE_FIELD_SET = 'NONCE_FIELD_SET',
GAS_PRICE_FIELD_SET = 'GAS_PRICE_FIELD_SET',
TIME_BOUNTY_FIELD_SET = 'TIME_BOUNTY_FIELD_SET',
WINDOW_SIZE_FIELD_SET = 'WINDOW_SIZE_FIELD_SET',
WINDOW_START_FIELD_SET = 'WINDOW_START_FIELD_SET',
SCHEDULE_TIMESTAMP_FIELD_SET = 'SCHEDULE_TIMESTAMP_FIELD_SET',
SCHEDULE_TIMEZONE_SET = 'SCHEDULE_TIMEZONE_SET',

View File

@ -25,7 +25,7 @@ const TransactionSucceeded = ({ txHash, blockExplorer, scheduling }: Transaction
if (scheduling) {
scheduleDetailsBtn = (
<a href={getTXDetailsCheckURL(txHash)} className="btn btn-xs">
{translate('SCHEDULE_check')}
{translate('SCHEDULE_CHECK')}
</a>
);
}

View File

@ -20,7 +20,7 @@ class ScheduleTimezoneDropDownClass extends Component<DispatchProps> {
return (
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">{translate('SCHEDULE_timezone')}</div>
<div className="input-group-header">{translate('SCHEDULE_TIMEZONE')}</div>
<Query
params={['readOnly']}
withQuery={({ readOnly }) => (

View File

@ -33,7 +33,7 @@ class ScheduleTypeClass extends Component<Props> {
onChange={this.handleOnChange}
checked={currentScheduleType.value === 'time'}
/>
{translate('SCHEDULE_type_time')}
{translate('SCHEDULE_TYPE_TIME')}
</label>
</div>
</div>
@ -47,7 +47,7 @@ class ScheduleTypeClass extends Component<Props> {
onChange={this.handleOnChange}
checked={currentScheduleType.value === 'block'}
/>
{translate('SCHEDULE_type_block')}
{translate('SCHEDULE_TYPE_BLOCK')}
</label>
</div>
</div>

View File

@ -21,6 +21,7 @@ export type Param =
| 'value'
| 'gaslimit'
| 'limit'
| 'windowSize'
| 'windowStart'
| 'scheduleTimestamp'
| 'timeBounty';

View File

@ -12,12 +12,12 @@ export const TimeBountyField: React.SFC<Props> = ({ isReadOnly }) => (
withProps={({ currentTimeBounty, isValid, onChange, readOnly }) => (
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">{translate('SCHEDULE_timebounty')}</div>
<div className="input-group-header">{translate('SCHEDULE_TIMEBOUNTY')}</div>
<Input
className={`input-group-input ${isValid ? '' : 'invalid'}`}
type="text"
value={currentTimeBounty.raw}
placeholder={translateRaw('SCHEDULE_timebounty_placeholder')}
placeholder={translateRaw('SCHEDULE_TIMEBOUNTY_PLACEHOLDER')}
readOnly={!!(isReadOnly || readOnly)}
spellCheck={false}
onChange={onChange}

View File

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

View File

@ -0,0 +1,61 @@
import { Query } from 'components/renderCbs';
import { setCurrentWindowSize, TSetCurrentWindowSize } from 'actions/transaction';
import { WindowSizeInputFactory } from './WindowSizeInputFactory';
import React from 'react';
import { connect } from 'react-redux';
import { ICurrentWindowSize } from 'selectors/transaction';
interface DispatchProps {
setCurrentWindowSize: TSetCurrentWindowSize;
}
interface OwnProps {
windowSize: string | null;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
export interface CallbackProps {
isValid: boolean;
readOnly: boolean;
currentWindowSize: ICurrentWindowSize;
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
type Props = DispatchProps & OwnProps;
class WindowSizeFieldFactoryClass extends React.Component<Props> {
public componentDidMount() {
const { windowSize } = this.props;
if (windowSize) {
this.props.setCurrentWindowSize(windowSize);
}
}
public render() {
return (
<WindowSizeInputFactory onChange={this.setWindowSize} withProps={this.props.withProps} />
);
}
private setWindowSize = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.setCurrentWindowSize(value);
};
}
const WindowSizeFieldFactory = connect(null, { setCurrentWindowSize })(WindowSizeFieldFactoryClass);
interface DefaultWindowSizeFieldProps {
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
const DefaultWindowSizeField: React.SFC<DefaultWindowSizeFieldProps> = ({ withProps }) => (
<Query
params={['windowSize']}
withQuery={({ windowSize }) => (
<WindowSizeFieldFactory windowSize={windowSize} withProps={withProps} />
)}
/>
);
export { DefaultWindowSizeField as WindowSizeFieldFactory };

View File

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

View File

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

View File

@ -12,12 +12,12 @@ export const WindowStartField: React.SFC<Props> = ({ isReadOnly }) => (
withProps={({ currentWindowStart, isValid, onChange, readOnly }) => (
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">{translate('SCHEDULE_block')}</div>
<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)}
placeholder={translateRaw('SCHEDULE_BLOCK_PLACEHOLDER')}
readOnly={!!(isReadOnly || readOnly)}
spellCheck={false}
onChange={onChange}

View File

@ -8,7 +8,7 @@ import {
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getResolvingDomain } from 'selectors/ens';
import { CallbackProps } from 'containers/Tabs/ScheduleTransaction/components/Fields/WindowStart/WindowStartFieldFactory';
import { CallbackProps } from './WindowStartFieldFactory';
interface StateProps {
currentWindowStart: ICurrentWindowStart;

View File

@ -7,7 +7,7 @@ export const GenerateScheduleTransactionButton: 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')}
{isWeb3Wallet ? translate('SCHEDULE_SCHEDULE') : translate('DEP_signtx')}
</button>
)}
/>

View File

@ -22,7 +22,7 @@ export const SendScheduleTransactionButton: React.SFC<{
!!signing ? (signTx(), openModal()) : openModal();
}}
>
{translate('SCHEDULE_schedule')}
{translate('SCHEDULE_SCHEDULE')}
</button>
</React.Fragment>
)}

View File

@ -1,2 +1,3 @@
export * from './Fields/WindowStart/WindowStartField';
export * from './Fields/TimeBounty/TimeBountyField';
export * from './Fields/WindowSize/WindowSizeField';

View File

@ -0,0 +1,23 @@
import { setWindowSizeField } 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 { SetWindowSizeFieldAction } from 'actions/transaction';
import { SetCurrentWindowSizeAction } from 'actions/transaction/actionTypes/windowSize';
export function* setCurrentWindowSize({ payload: raw }: SetCurrentWindowSizeAction): SagaIterator {
let value: number | null = null;
value = parseInt(raw, 10);
yield call(setField, { value, raw });
}
export function* setField(payload: SetWindowSizeFieldAction['payload']) {
yield put(setWindowSizeField(payload));
}
export const currentWindowSize = takeLatest(
[TypeKeys.CURRENT_WINDOW_SIZE_SET],
setCurrentWindowSize
);

View File

@ -1,8 +1,10 @@
import { currentWindowSize } from './currentWindowSize';
import { currentWindowStart } from './currentWindowStart';
import { currentScheduleTimestamp } from './currentScheduleTimestamp';
import { currentTimeBounty } from './currentTimeBounty';
export const schedulingCurrentSagas = [
currentWindowSize,
currentWindowStart,
currentScheduleTimestamp,
currentTimeBounty

View File

@ -14,7 +14,6 @@ export const EAC_SCHEDULING_CONFIG = {
TIME_BOUNTY_MIN,
TIME_BOUNTY_DEFAULT: TIME_BOUNTY_MIN,
TIME_BOUNTY_MAX: toWei('900', Units.ether.length - 1), // 900 ETH
WINDOW_SIZE_IN_BLOCKS: 90,
SCHEDULE_TIMESTAMP_FORMAT: 'YYYY-MM-DD HH:mm:ss',
DEFAULT_SCHEDULING_METHOD: 'time'
};
@ -54,13 +53,20 @@ export const getScheduleData = (
callData = '',
callGas: number,
callValue: BN | null,
windowSize: number,
windowSize: number | null,
windowStart: any,
gasPrice: BN | null,
timeBounty: BN | null,
requiredDeposit: any
) => {
if (!callValue || !gasPrice || !windowStart || !timeBounty || timeBounty.lt(new BN(0))) {
if (
!callValue ||
!gasPrice ||
!windowStart ||
!windowSize ||
!timeBounty ||
timeBounty.lt(new BN(0))
) {
return;
}

View File

@ -19,6 +19,7 @@ const INITIAL_STATE: State = {
data: { raw: '', value: null },
nonce: { raw: '', value: null },
value: { raw: '', value: null },
windowSize: { raw: '', value: null },
windowStart: { raw: '', value: null },
scheduleTimestamp: { raw: '', value: null },
gasLimit: { raw: '21000', value: new BN(21000) },
@ -82,6 +83,8 @@ export const fields = (
return updateField('gasPrice')(state, action);
case TK.TIME_BOUNTY_FIELD_SET:
return updateField('timeBounty')(state, action);
case TK.WINDOW_SIZE_FIELD_SET:
return updateField('windowSize')(state, action);
case TK.WINDOW_START_FIELD_SET:
return updateField('windowStart')(state, action);
case TK.SCHEDULE_TIMESTAMP_FIELD_SET:

View File

@ -3,6 +3,7 @@ import {
SetDataFieldAction,
SetNonceFieldAction,
SetGasLimitFieldAction,
SetWindowSizeFieldAction,
SetWindowStartFieldAction,
SetScheduleTimestampFieldAction,
SetScheduleTypeAction
@ -17,6 +18,7 @@ export interface State {
gasLimit: SetGasLimitFieldAction['payload'];
gasPrice: { raw: string; value: Wei };
timeBounty: { raw: string; value: Wei };
windowSize: SetWindowSizeFieldAction['payload'];
windowStart: SetWindowStartFieldAction['payload'];
scheduleTimestamp: SetScheduleTimestampFieldAction['payload'];
scheduleType: SetScheduleTypeAction['payload'];

View File

@ -11,6 +11,7 @@ const getGasPrice = (state: AppState) => getFields(state).gasPrice;
const getValue = (state: AppState) => getFields(state).value;
const getNonce = (state: AppState) => getFields(state).nonce;
const getTimeBounty = (state: AppState) => getFields(state).timeBounty;
const getWindowSize = (state: AppState) => getFields(state).windowSize;
const getWindowStart = (state: AppState) => getFields(state).windowStart;
const getScheduleTimestamp = (state: AppState) => getFields(state).scheduleTimestamp;
const getScheduleType = (state: AppState) => getFields(state).scheduleType;
@ -41,6 +42,7 @@ export {
getDataExists,
getValidGasCost,
getTimeBounty,
getWindowSize,
getWindowStart,
getScheduleTimestamp,
getScheduleType

View File

@ -30,6 +30,7 @@ export const isFullTx = (
const partialParamsToCheck = { ...rest };
delete partialParamsToCheck.windowStart;
delete partialParamsToCheck.windowSize;
delete partialParamsToCheck.scheduleTimestamp;
const validPartialParams = Object.values(partialParamsToCheck).reduce<boolean>(
@ -61,6 +62,12 @@ export const isFullTx = (
}
};
export const isWindowSizeValid = (transactionFields: AppState['transaction']['fields']) => {
const { windowSize } = transactionFields;
return Boolean(windowSize && windowSize.value);
};
export const isWindowStartValid = (
transactionFields: AppState['transaction']['fields'],
latestBlock: string

View File

@ -4,6 +4,7 @@ export * from './fields';
export * from './meta';
export * from './sign';
export * from './current';
export * from './windowSize';
export * from './windowStart';
export * from './timeBounty';
export * from './scheduleTimestamp';

View File

@ -6,7 +6,8 @@ import {
getWindowStart,
getNonce,
getTimeBounty,
getScheduleType
getScheduleType,
getWindowSize
} from './fields';
import { makeTransaction, IHexStrTransaction } from 'libs/transaction';
import EthTx from 'ethereumjs-tx';
@ -15,7 +16,8 @@ import {
reduceToValues,
isFullTx,
isWindowStartValid,
isScheduleTimestampValid
isScheduleTimestampValid,
isWindowSizeValid
} from 'selectors/transaction/helpers';
import {
getGasPrice,
@ -75,23 +77,26 @@ const getSchedulingTransaction = (state: AppState): IGetTransaction => {
const validGasCost = getValidGasCost(state);
const scheduleType = getScheduleType(state);
const windowStart = getWindowStart(state);
const windowSize = getWindowSize(state);
const gasLimit = getGasLimit(state);
const nonce = getNonce(state);
const gasPrice = getGasPrice(state);
const timeBounty = getTimeBounty(state);
const windowSizeValid = isWindowSizeValid(transactionFields);
const windowStartValid = isWindowStartValid(transactionFields, getLatestBlock(state));
const scheduleTimestampValid = isScheduleTimestampValid(transactionFields);
const isFullTransaction =
isFullTx(state, transactionFields, currentTo, currentValue, dataExists, validGasCost, unit) &&
(windowStartValid || scheduleTimestampValid);
(windowStartValid || scheduleTimestampValid) &&
windowSizeValid;
const transactionData = getScheduleData(
currentTo.raw,
callData.raw,
parseInt(gasLimit.raw, 10),
currentValue.value,
EAC_SCHEDULING_CONFIG.WINDOW_SIZE_IN_BLOCKS,
windowSize.value,
windowStart.value,
gasPrice.value,
timeBounty.value,

View File

@ -0,0 +1,17 @@
import { AppState } from 'reducers';
import { getWindowSize } from './fields';
interface ICurrentWindowSize {
raw: string;
value: number | null;
}
const isValidCurrentWindowSize = (state: AppState) => {
const currentWindowSize = getWindowSize(state);
return currentWindowSize && currentWindowSize.value && currentWindowSize.value > 0;
};
const getCurrentWindowSize = (state: AppState): ICurrentWindowSize => getWindowSize(state);
export { getCurrentWindowSize, ICurrentWindowSize, isValidCurrentWindowSize };

View File

@ -17,6 +17,7 @@ describe('fields reducer', () => {
raw: fromWei(EAC_SCHEDULING_CONFIG.TIME_BOUNTY_DEFAULT, 'ether'),
value: EAC_SCHEDULING_CONFIG.TIME_BOUNTY_DEFAULT
},
windowSize: { raw: '', value: null },
windowStart: { raw: '', value: null },
scheduleTimestamp: { raw: '', value: null },
scheduleType: {

View File

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

View File

@ -48,6 +48,10 @@ describe('helpers selector', () => {
raw: '1500',
value: Wei('1500')
},
windowSize: {
raw: '',
value: null
},
windowStart: {
raw: '',
value: null
@ -72,6 +76,7 @@ describe('helpers selector', () => {
to: new Buffer([0, 1, 2, 3]),
value: Wei('1000000000'),
timeBounty: Wei('1500'),
windowSize: null,
windowStart: null,
scheduleTimestamp: null,
scheduleType: 'time'