[FEATURE] TimeBounty slider.

This commit is contained in:
Daniel Kmak 2018-03-21 11:14:08 +03:00 committed by Bagaric
parent eb19dd9f0b
commit d2b3d9c47f
21 changed files with 344 additions and 114 deletions

View File

@ -7,13 +7,16 @@ import {
InputGasLimitAction, InputGasLimitAction,
InputGasPriceAction, InputGasPriceAction,
InputGasPriceIntentAction, InputGasPriceIntentAction,
InputTimeBountyAction,
InputDataAction, InputDataAction,
InputNonceAction, InputNonceAction,
ResetAction, ResetAction,
SetGasPriceFieldAction, SetGasPriceFieldAction,
SetTimeBountyFieldAction,
SetWindowStartFieldAction SetWindowStartFieldAction
} from '../actionTypes'; } from '../actionTypes';
import { TypeKeys } from 'actions/transaction/constants'; import { TypeKeys } from 'actions/transaction/constants';
import { InputTimeBountyIntentAction } from 'actions/transaction';
type TInputGasLimit = typeof inputGasLimit; type TInputGasLimit = typeof inputGasLimit;
const inputGasLimit = (payload: InputGasLimitAction['payload']) => ({ const inputGasLimit = (payload: InputGasLimitAction['payload']) => ({
@ -33,6 +36,26 @@ const inputGasPriceIntent = (payload: InputGasPriceIntentAction['payload']) => (
payload payload
}); });
type TInputTimeBounty = typeof inputTimeBounty;
const inputTimeBounty = (payload: InputTimeBountyAction['payload']) => ({
type: TypeKeys.TIME_BOUNTY_INPUT,
payload
});
type TInputTimeBountyIntent = typeof inputTimeBounty;
const inputTimeBountyIntent = (payload: InputTimeBountyIntentAction['payload']) => ({
type: TypeKeys.TIME_BOUNTY_INPUT_INTENT,
payload
});
type TSetTimeBountyField = typeof setTimeBountyField;
const setTimeBountyField = (
payload: SetTimeBountyFieldAction['payload']
): SetTimeBountyFieldAction => ({
type: TypeKeys.TIME_BOUNTY_FIELD_SET,
payload
});
type TInputNonce = typeof inputNonce; type TInputNonce = typeof inputNonce;
const inputNonce = (payload: InputNonceAction['payload']) => ({ const inputNonce = (payload: InputNonceAction['payload']) => ({
type: TypeKeys.NONCE_INPUT, type: TypeKeys.NONCE_INPUT,
@ -99,6 +122,8 @@ export {
TInputGasLimit, TInputGasLimit,
TInputGasPrice, TInputGasPrice,
TInputGasPriceIntent, TInputGasPriceIntent,
TInputTimeBounty,
TInputTimeBountyIntent,
TInputNonce, TInputNonce,
TInputData, TInputData,
TSetGasLimitField, TSetGasLimitField,
@ -108,10 +133,14 @@ export {
TSetValueField, TSetValueField,
TSetGasPriceField, TSetGasPriceField,
TSetWindowStartField, TSetWindowStartField,
TSetTimeBountyField,
TReset, TReset,
inputGasLimit, inputGasLimit,
inputGasPrice, inputGasPrice,
inputGasPriceIntent, inputGasPriceIntent,
inputTimeBounty,
inputTimeBountyIntent,
setTimeBountyField,
inputNonce, inputNonce,
inputData, inputData,
setGasLimitField, setGasLimitField,

View File

@ -14,6 +14,14 @@ interface InputGasPriceIntentAction {
type: TypeKeys.GAS_PRICE_INPUT_INTENT; type: TypeKeys.GAS_PRICE_INPUT_INTENT;
payload: string; payload: string;
} }
interface InputTimeBountyAction {
type: TypeKeys.TIME_BOUNTY_INPUT;
payload: string;
}
interface InputTimeBountyIntentAction {
type: TypeKeys.TIME_BOUNTY_INPUT_INTENT;
payload: string;
}
interface InputDataAction { interface InputDataAction {
type: TypeKeys.DATA_FIELD_INPUT; type: TypeKeys.DATA_FIELD_INPUT;
payload: string; payload: string;
@ -43,6 +51,14 @@ interface SetGasPriceFieldAction {
}; };
} }
interface SetTimeBountyFieldAction {
type: TypeKeys.TIME_BOUNTY_FIELD_SET;
payload: {
raw: string;
value: Wei | null;
};
}
interface SetDataFieldAction { interface SetDataFieldAction {
type: TypeKeys.DATA_FIELD_SET; type: TypeKeys.DATA_FIELD_SET;
payload: { payload: {
@ -92,12 +108,15 @@ type FieldAction =
| SetNonceFieldAction | SetNonceFieldAction
| SetValueFieldAction | SetValueFieldAction
| SetGasPriceFieldAction | SetGasPriceFieldAction
| SetTimeBountyFieldAction
| SetWindowStartFieldAction; | SetWindowStartFieldAction;
export { export {
InputGasLimitAction, InputGasLimitAction,
InputGasPriceAction, InputGasPriceAction,
InputGasPriceIntentAction, InputGasPriceIntentAction,
InputTimeBountyAction,
InputTimeBountyIntentAction,
InputDataAction, InputDataAction,
InputNonceAction, InputNonceAction,
SetGasLimitFieldAction, SetGasLimitFieldAction,
@ -108,5 +127,6 @@ export {
FieldAction, FieldAction,
InputFieldAction, InputFieldAction,
SetGasPriceFieldAction, SetGasPriceFieldAction,
SetTimeBountyFieldAction,
SetWindowStartFieldAction SetWindowStartFieldAction
}; };

View File

@ -31,6 +31,8 @@ export enum TypeKeys {
GAS_LIMIT_INPUT = 'GAS_LIMIT_INPUT', GAS_LIMIT_INPUT = 'GAS_LIMIT_INPUT',
GAS_PRICE_INPUT = 'GAS_PRICE_INPUT', GAS_PRICE_INPUT = 'GAS_PRICE_INPUT',
GAS_PRICE_INPUT_INTENT = 'GAS_PRICE_INPUT_INTENT', GAS_PRICE_INPUT_INTENT = 'GAS_PRICE_INPUT_INTENT',
TIME_BOUNTY_INPUT = 'TIME_BOUNTY_INPUT',
TIME_BOUNTY_INPUT_INTENT = 'TIME_BOUNTY_INPUT_INTENT',
NONCE_INPUT = 'NONCE_INPUT', NONCE_INPUT = 'NONCE_INPUT',
DATA_FIELD_SET = 'DATA_FIELD_SET', DATA_FIELD_SET = 'DATA_FIELD_SET',
@ -39,6 +41,7 @@ export enum TypeKeys {
VALUE_FIELD_SET = 'VALUE_FIELD_SET', VALUE_FIELD_SET = 'VALUE_FIELD_SET',
NONCE_FIELD_SET = 'NONCE_FIELD_SET', NONCE_FIELD_SET = 'NONCE_FIELD_SET',
GAS_PRICE_FIELD_SET = 'GAS_PRICE_FIELD_SET', GAS_PRICE_FIELD_SET = 'GAS_PRICE_FIELD_SET',
TIME_BOUNTY_FIELD_SET = 'TIME_BOUNTY_FIELD_SET',
WINDOW_START_FIELD_SET = 'WINDOW_START_FIELD_SET', WINDOW_START_FIELD_SET = 'WINDOW_START_FIELD_SET',
TOKEN_TO_META_SET = 'TOKEN_TO_META_SET', TOKEN_TO_META_SET = 'TOKEN_TO_META_SET',

View File

@ -94,8 +94,10 @@ class TXMetaDataPanel extends React.Component<Props, State> {
const { offline, disableToggle, advancedGasOptions, className = '', scheduling } = this.props; const { offline, disableToggle, advancedGasOptions, className = '', scheduling } = this.props;
const { gasPrice } = this.state; const { gasPrice } = this.state;
const showAdvanced = this.state.sliderState === 'advanced' || offline; const showAdvanced = this.state.sliderState === 'advanced' || offline;
return ( return (
<div className={`Gas col-md-12 ${className}`}> <div className={`Gas col-md-12 ${className}`}>
<br />
{showAdvanced ? ( {showAdvanced ? (
<AdvancedGas <AdvancedGas
gasPrice={gasPrice} gasPrice={gasPrice}

View File

@ -10,7 +10,7 @@ import { connect } from 'react-redux';
import { getAutoGasLimitEnabled } from 'selectors/config'; import { getAutoGasLimitEnabled } from 'selectors/config';
import { isValidGasPrice } from 'selectors/transaction'; import { isValidGasPrice } from 'selectors/transaction';
import { sanitizeNumericalInput } from 'libs/values'; import { sanitizeNumericalInput } from 'libs/values';
import { Input } from 'components/ui'; import { Input, UnitDisplay } from 'components/ui';
import SchedulingFeeSummary from './SchedulingFeeSummary'; import SchedulingFeeSummary from './SchedulingFeeSummary';
import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling'; import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling';
@ -27,6 +27,7 @@ interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice']; gasPrice: AppState['transaction']['fields']['gasPrice'];
options?: AdvancedOptions; options?: AdvancedOptions;
scheduling?: boolean; scheduling?: boolean;
timeBounty?: AppState['transaction']['fields']['timeBounty'];
} }
interface StateProps { interface StateProps {
@ -117,7 +118,7 @@ class AdvancedGas extends React.Component<Props, State> {
} }
private renderFee() { private renderFee() {
const { gasPrice, scheduling } = this.props; const { gasPrice, scheduling, timeBounty } = this.props;
const { feeSummary } = this.state.options; const { feeSummary } = this.state.options;
if (!feeSummary) { if (!feeSummary) {
@ -132,11 +133,17 @@ class AdvancedGas extends React.Component<Props, State> {
render={({ gasPriceWei, gasLimit, fee, usd }) => ( render={({ gasPriceWei, gasLimit, fee, usd }) => (
<div> <div>
<span> <span>
{EAC_SCHEDULING_CONFIG.PAYMENT} + {gasPriceWei} * ({ <UnitDisplay
EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT value={EAC_SCHEDULING_CONFIG.FEE.mul(EAC_SCHEDULING_CONFIG.FEE_MULTIPLIER)}
}{' '} unit={'ether'}
+ {EAC_SCHEDULING_CONFIG.FUTURE_EXECUTION_COST} + {gasLimit}) = {fee}{' '} displayShortBalance={true}
{usd && <span>~= ${usd} USD</span>} checkOffline={true}
symbol="ETH"
/>{' '}
+ {timeBounty && timeBounty.value.toString()} + {gasPriceWei} * ({EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT.add(
EAC_SCHEDULING_CONFIG.FUTURE_EXECUTION_COST
).toString()}{' '}
+ {gasLimit}) = {fee} {usd && <span>~= ${usd} USD</span>}
</span> </span>
</div> </div>
)} )}

View File

@ -9,3 +9,7 @@
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
} }
.SchedulingFeeSummary {
font-size: 12px;
}

View File

@ -4,11 +4,11 @@ import { connect } from 'react-redux';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import { getNetworkConfig, getOffline } from 'selectors/config'; import { getNetworkConfig, getOffline } from 'selectors/config';
import { getIsEstimating } from 'selectors/gas'; import { getIsEstimating } from 'selectors/gas';
import { getGasLimit } from 'selectors/transaction'; import { getGasLimit, getTimeBounty } from 'selectors/transaction';
import { UnitDisplay, Spinner } from 'components/ui'; import { UnitDisplay, Spinner } from 'components/ui';
import { NetworkConfig } from 'types/network'; import { NetworkConfig } from 'types/network';
import './FeeSummary.scss'; import './FeeSummary.scss';
import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling'; import { calcEACTotalCost } from 'libs/scheduling';
interface RenderData { interface RenderData {
gasPriceWei: string; gasPriceWei: string;
@ -24,6 +24,7 @@ interface ReduxStateProps {
network: NetworkConfig; network: NetworkConfig;
isOffline: AppState['config']['meta']['offline']; isOffline: AppState['config']['meta']['offline'];
isGasEstimating: AppState['gas']['isEstimating']; isGasEstimating: AppState['gas']['isEstimating'];
timeBounty: AppState['transaction']['fields']['timeBounty'];
} }
interface OwnProps { interface OwnProps {
@ -35,7 +36,15 @@ type Props = OwnProps & ReduxStateProps;
class SchedulingFeeSummary extends React.Component<Props> { class SchedulingFeeSummary extends React.Component<Props> {
public render() { public render() {
const { gasPrice, gasLimit, rates, network, isOffline, isGasEstimating } = this.props; const {
gasPrice,
gasLimit,
rates,
network,
isOffline,
isGasEstimating,
timeBounty
} = this.props;
if (isGasEstimating) { if (isGasEstimating) {
return ( return (
@ -48,13 +57,8 @@ class SchedulingFeeSummary extends React.Component<Props> {
const feeBig = const feeBig =
gasPrice.value && gasPrice.value &&
gasLimit.value && gasLimit.value &&
gasPrice.value timeBounty.value &&
.mul( calcEACTotalCost(gasLimit.value, gasPrice.value, timeBounty.value);
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 = ( const fee = (
<UnitDisplay <UnitDisplay
@ -80,7 +84,7 @@ class SchedulingFeeSummary extends React.Component<Props> {
); );
return ( return (
<div className="FeeSummary"> <div className="FeeSummary SchedulingFeeSummary">
{this.props.render({ {this.props.render({
gasPriceWei: gasPrice.value.toString(), gasPriceWei: gasPrice.value.toString(),
gasPriceGwei: gasPrice.raw, gasPriceGwei: gasPrice.raw,
@ -99,7 +103,8 @@ function mapStateToProps(state: AppState): ReduxStateProps {
rates: state.rates.rates, rates: state.rates.rates,
network: getNetworkConfig(state), network: getNetworkConfig(state),
isOffline: getOffline(state), isOffline: getOffline(state),
isGasEstimating: getIsEstimating(state) isGasEstimating: getIsEstimating(state),
timeBounty: getTimeBounty(state)
}; };
} }

View File

@ -0,0 +1,75 @@
import React from 'react';
import Slider, { createSliderWithTooltip } from 'rc-slider';
import translate from 'translations';
import { AppState } from 'reducers';
import {
Wei,
fromTokenBase,
getDecimalFromEtherUnit,
timeBountyValueToRaw,
timeBountyRawToValue
} from 'libs/units';
import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling';
const SliderWithTooltip = createSliderWithTooltip(Slider);
interface OwnProps {
timeBounty: AppState['transaction']['fields']['timeBounty'];
inputTimeBounty(rawTimeBounty: string): void;
}
class TimeBounty extends React.Component<OwnProps> {
public render() {
const { timeBounty } = this.props;
const bounds = {
max: EAC_SCHEDULING_CONFIG.TIME_BOUNTY_MAX,
min: EAC_SCHEDULING_CONFIG.TIME_BOUNTY_MIN
};
return (
<div className="row form-group">
<div className="flex-wrapper">
<label>{translate('SCHEDULE_bounty')} </label>
</div>
<div className="SimpleGas-input-group">
<div className="SimpleGas-slider">
<SliderWithTooltip
onChange={this.handleSlider}
min={bounds.min}
max={bounds.max}
step={bounds.min < 1 ? 0.1 : 1}
value={this.getTimeBountyRaw(timeBounty.value)}
tipFormatter={this.formatTooltip}
disabled={false}
/>
<div className="SimpleGas-slider-labels">
<span>{translate('Small')}</span>
<span>{translate('Big')}</span>
</div>
</div>
</div>
</div>
);
}
private handleSlider = (timeBounty: number) => {
this.props.inputTimeBounty(timeBounty.toString());
};
private getTimeBountyRaw(timeBountyValue: Wei) {
return parseFloat(timeBountyValueToRaw(timeBountyValue));
}
private formatTooltip = (timeBounty: number) => {
const valueInETH = fromTokenBase(
timeBountyRawToValue(timeBounty),
getDecimalFromEtherUnit('ether')
);
return `${valueInETH} ETH`;
};
}
export default TimeBounty;

View File

@ -1,7 +1,67 @@
import BN from 'bn.js';
import abi from 'ethereumjs-abi';
export const EAC_SCHEDULING_CONFIG = { export const EAC_SCHEDULING_CONFIG = {
SCHEDULING_GAS_LIMIT: 1500000, FEE: new BN('2242000000000000'), // $2
FUTURE_EXECUTION_COST: 180000, FEE_MULTIPLIER: new BN('2'),
FEE_MULTIPLIER: 2, FUTURE_EXECUTION_COST: new BN('180000'),
PAYMENT: 10000000, REQUIRED_DEPOSIT: 0,
SCHEDULING_GAS_LIMIT: new BN('1500000'),
TIME_BOUNTY_MIN: 1, // $0.1
TIME_BOUNTY_DEFAULT: 10, // $1
TIME_BOUNTY_MAX: 100, // $10
TIME_BOUNTY_TO_WEI_MULTIPLIER: new BN('100000000000000'),
WINDOW_SIZE_IN_BLOCKS: 90 WINDOW_SIZE_IN_BLOCKS: 90
}; };
export const EAC_ADDRESSES = {
KOVAN: {
blockScheduler: '0x1afc19a7e642761ba2b55d2a45b32c7ef08269d1'
}
};
export const calcEACFutureExecutionCost = (callGas: BN, gasPrice: BN, timeBounty: BN) => {
const totalGas = callGas.add(EAC_SCHEDULING_CONFIG.FUTURE_EXECUTION_COST);
return timeBounty
.add(EAC_SCHEDULING_CONFIG.FEE.mul(EAC_SCHEDULING_CONFIG.FEE_MULTIPLIER))
.add(totalGas.mul(gasPrice));
};
export const calcEACEndowment = (callGas: BN, callValue: BN, gasPrice: BN, timeBounty: BN) =>
callValue.add(calcEACFutureExecutionCost(callGas, gasPrice, timeBounty));
export const calcEACTotalCost = (callGas: BN, gasPrice: BN, timeBounty: BN) => {
const deployCost = gasPrice.mul(EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT);
const futureExecutionCost = calcEACFutureExecutionCost(callGas, gasPrice, timeBounty);
return deployCost.add(futureExecutionCost);
};
export const getScheduleData = (
toAddress: string,
callData = '',
callGas: number,
callValue: BN | null,
windowSize: number,
windowStart: any,
gasPrice: BN | null,
timeBounty: any,
requiredDeposit: any
) => {
if (!callValue || !gasPrice || !windowStart) {
return;
}
return abi.simpleEncode('schedule(address,bytes,uint[8]):(address)', toAddress, callData, [
callGas,
callValue,
windowSize,
windowStart,
gasPrice,
EAC_SCHEDULING_CONFIG.FEE,
timeBounty,
requiredDeposit
]);
};

View File

@ -1,6 +1,7 @@
import BN from 'bn.js'; import BN from 'bn.js';
import { toBuffer, addHexPrefix } from 'ethereumjs-util'; import { toBuffer, addHexPrefix } from 'ethereumjs-util';
import { stripHexPrefix } from 'libs/values'; import { stripHexPrefix } from 'libs/values';
import { EAC_SCHEDULING_CONFIG } from './scheduling';
type UnitKey = keyof typeof Units; type UnitKey = keyof typeof Units;
type Wei = BN; type Wei = BN;
@ -110,7 +111,19 @@ const convertTokenBase = (value: TokenValue, oldDecimal: number, newDecimal: num
return toTokenBase(fromTokenBase(value, oldDecimal), newDecimal); return toTokenBase(fromTokenBase(value, oldDecimal), newDecimal);
}; };
const gasPricetoBase = (price: number) => toWei(price.toString(), getDecimalFromEtherUnit('gwei')); const gasPriceToBase = (price: number) => toWei(price.toString(), getDecimalFromEtherUnit('gwei'));
const timeBountyRawToValue = (timeBounty: number) =>
toWei(
timeBounty.toString(),
EAC_SCHEDULING_CONFIG.TIME_BOUNTY_TO_WEI_MULTIPLIER.toString().length - 1
);
const timeBountyValueToRaw = (timeBounty: BN) =>
baseToConvertedUnit(
timeBounty.toString(),
EAC_SCHEDULING_CONFIG.TIME_BOUNTY_TO_WEI_MULTIPLIER.toString().length - 1
);
export { export {
Data, Data,
@ -126,5 +139,7 @@ export {
UnitKey, UnitKey,
Nonce, Nonce,
handleValues, handleValues,
gasPricetoBase gasPriceToBase,
timeBountyRawToValue,
timeBountyValueToRaw
}; };

View File

@ -12,6 +12,7 @@ import {
GAS_PRICE_GWEI_UPPER_BOUND GAS_PRICE_GWEI_UPPER_BOUND
} from 'config/constants'; } from 'config/constants';
import { dPathRegex } from 'config/dpaths'; import { dPathRegex } from 'config/dpaths';
import { EAC_SCHEDULING_CONFIG } from './scheduling';
// FIXME we probably want to do checksum checks sideways // FIXME we probably want to do checksum checks sideways
export function isValidETHAddress(address: string): boolean { export function isValidETHAddress(address: string): boolean {
@ -148,6 +149,15 @@ export const gasPriceValidator = (gasPrice: number | string): boolean => {
); );
}; };
export const timeBountyValidator = (timeBounty: number | string): boolean => {
const timeBountyFloat = typeof timeBounty === 'string' ? parseFloat(timeBounty) : timeBounty;
return (
validNumber(timeBountyFloat) &&
timeBountyFloat >= EAC_SCHEDULING_CONFIG.TIME_BOUNTY_MIN &&
timeBountyFloat <= EAC_SCHEDULING_CONFIG.TIME_BOUNTY_MAX
);
};
export const isValidByteCode = (byteCode: string) => export const isValidByteCode = (byteCode: string) =>
byteCode && byteCode.length > 0 && byteCode.length % 2 === 0; byteCode && byteCode.length > 0 && byteCode.length % 2 === 0;

View File

@ -10,8 +10,9 @@ import {
} from 'actions/transaction'; } from 'actions/transaction';
import { Reducer } from 'redux'; import { Reducer } from 'redux';
import { State } from './typings'; import { State } from './typings';
import { gasPricetoBase } from 'libs/units'; import { gasPriceToBase, timeBountyRawToValue } from 'libs/units';
import { resetHOF } from 'reducers/transaction/shared'; import { resetHOF } from 'reducers/transaction/shared';
import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling';
const INITIAL_STATE: State = { const INITIAL_STATE: State = {
to: { raw: '', value: null }, to: { raw: '', value: null },
@ -20,7 +21,11 @@ const INITIAL_STATE: State = {
value: { raw: '', value: null }, value: { raw: '', value: null },
windowStart: { raw: '', value: null }, windowStart: { raw: '', value: null },
gasLimit: { raw: '21000', value: new BN(21000) }, gasLimit: { raw: '21000', value: new BN(21000) },
gasPrice: { raw: '20', value: gasPricetoBase(20) } gasPrice: { raw: '20', value: gasPriceToBase(20) },
timeBounty: {
raw: EAC_SCHEDULING_CONFIG.TIME_BOUNTY_DEFAULT.toString(),
value: timeBountyRawToValue(EAC_SCHEDULING_CONFIG.TIME_BOUNTY_DEFAULT)
}
}; };
const updateField = (key: keyof State): Reducer<State> => (state: State, action: FieldAction) => ({ const updateField = (key: keyof State): Reducer<State> => (state: State, action: FieldAction) => ({
@ -70,6 +75,8 @@ export const fields = (
return updateField('nonce')(state, action); return updateField('nonce')(state, action);
case TK.GAS_PRICE_FIELD_SET: case TK.GAS_PRICE_FIELD_SET:
return updateField('gasPrice')(state, action); return updateField('gasPrice')(state, action);
case TK.TIME_BOUNTY_FIELD_SET:
return updateField('timeBounty')(state, action);
case TK.WINDOW_START_FIELD_SET: case TK.WINDOW_START_FIELD_SET:
return updateField('windowStart')(state, action); return updateField('windowStart')(state, action);
case TK.TOKEN_TO_ETHER_SWAP: case TK.TOKEN_TO_ETHER_SWAP:

View File

@ -11,8 +11,9 @@ export interface State {
to: SetToFieldAction['payload']; to: SetToFieldAction['payload'];
data: SetDataFieldAction['payload']; data: SetDataFieldAction['payload'];
nonce: SetNonceFieldAction['payload']; nonce: SetNonceFieldAction['payload'];
windowStart: SetWindowStartFieldAction['payload'];
value: { raw: string; value: Wei | null }; // TODO: fix this workaround since some of the payload is optional value: { raw: string; value: Wei | null }; // TODO: fix this workaround since some of the payload is optional
gasLimit: SetGasLimitFieldAction['payload']; gasLimit: SetGasLimitFieldAction['payload'];
gasPrice: { raw: string; value: Wei }; gasPrice: { raw: string; value: Wei };
timeBounty: { raw: string; value: Wei };
windowStart: SetWindowStartFieldAction['payload'];
} }

View File

@ -3,9 +3,11 @@ import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
import { SagaIterator, delay } from 'redux-saga'; import { SagaIterator, delay } from 'redux-saga';
import { import {
inputGasPrice, inputGasPrice,
inputTimeBounty,
setDataField, setDataField,
setGasLimitField, setGasLimitField,
setGasPriceField, setGasPriceField,
setTimeBountyField,
setNonceField setNonceField
} from 'actions/transaction/actionCreators'; } from 'actions/transaction/actionCreators';
import { import {
@ -14,10 +16,20 @@ import {
InputGasPriceAction, InputGasPriceAction,
InputGasPriceIntentAction, InputGasPriceIntentAction,
InputNonceAction, InputNonceAction,
TypeKeys TypeKeys,
InputTimeBountyIntentAction,
InputTimeBountyAction
} from 'actions/transaction'; } from 'actions/transaction';
import { isValidHex, isValidNonce, gasPriceValidator, gasLimitValidator } from 'libs/validators'; import {
import { Data, Wei, Nonce, gasPricetoBase } from 'libs/units'; isValidHex,
isValidNonce,
gasPriceValidator,
gasLimitValidator,
timeBountyValidator
} from 'libs/validators';
import { Data, Wei, Nonce, gasPriceToBase, timeBountyRawToValue } from 'libs/units';
const SLIDER_DEBOUNCE_INPUT_DELAY = 300;
export function* handleDataInput({ payload }: InputDataAction): SagaIterator { export function* handleDataInput({ payload }: InputDataAction): SagaIterator {
const validData: boolean = yield call(isValidHex, payload); const validData: boolean = yield call(isValidHex, payload);
@ -35,13 +47,13 @@ export function* handleGasPriceInput({ payload }: InputGasPriceAction): SagaIter
yield put( yield put(
setGasPriceField({ setGasPriceField({
raw: payload, raw: payload,
value: validGasPrice ? gasPricetoBase(priceFloat) : new BN(0) value: validGasPrice ? gasPriceToBase(priceFloat) : new BN(0)
}) })
); );
} }
export function* handleGasPriceInputIntent({ payload }: InputGasPriceIntentAction): SagaIterator { export function* handleGasPriceInputIntent({ payload }: InputGasPriceIntentAction): SagaIterator {
yield call(delay, 300); yield call(delay, SLIDER_DEBOUNCE_INPUT_DELAY);
// Important to put and not fork handleGasPriceInput, we want // Important to put and not fork handleGasPriceInput, we want
// action to go to reducers. // action to go to reducers.
yield put(inputGasPrice(payload)); yield put(inputGasPrice(payload));
@ -52,10 +64,32 @@ export function* handleNonceInput({ payload }: InputNonceAction): SagaIterator {
yield put(setNonceField({ raw: payload, value: validNonce ? Nonce(payload) : null })); yield put(setNonceField({ raw: payload, value: validNonce ? Nonce(payload) : null }));
} }
export function* handleTimeBountyInputIntent({
payload
}: InputTimeBountyIntentAction): SagaIterator {
yield call(delay, SLIDER_DEBOUNCE_INPUT_DELAY);
yield put(inputTimeBounty(payload));
}
export function* handleTimeBountyInput({ payload }: InputTimeBountyAction): SagaIterator {
const timeBountyFloat = parseFloat(payload);
const validTimeBounty: boolean = yield call(timeBountyValidator, timeBountyFloat);
yield put(
setTimeBountyField({
raw: payload,
value: validTimeBounty ? timeBountyRawToValue(timeBountyFloat) : new BN(0)
})
);
}
export const fields = [ export const fields = [
takeEvery(TypeKeys.DATA_FIELD_INPUT, handleDataInput), takeEvery(TypeKeys.DATA_FIELD_INPUT, handleDataInput),
takeEvery(TypeKeys.GAS_LIMIT_INPUT, handleGasLimitInput), takeEvery(TypeKeys.GAS_LIMIT_INPUT, handleGasLimitInput),
takeEvery(TypeKeys.GAS_PRICE_INPUT, handleGasPriceInput), takeEvery(TypeKeys.GAS_PRICE_INPUT, handleGasPriceInput),
takeEvery(TypeKeys.TIME_BOUNTY_INPUT, handleTimeBountyInput),
takeEvery(TypeKeys.NONCE_INPUT, handleNonceInput), takeEvery(TypeKeys.NONCE_INPUT, handleNonceInput),
takeLatest(TypeKeys.GAS_PRICE_INPUT_INTENT, handleGasPriceInputIntent) takeLatest(TypeKeys.GAS_PRICE_INPUT_INTENT, handleGasPriceInputIntent),
takeLatest(TypeKeys.TIME_BOUNTY_INPUT_INTENT, handleTimeBountyInputIntent)
]; ];

View File

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

View File

@ -1,6 +1,6 @@
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import { getCurrentTo, getCurrentValue } from './current'; import { getCurrentTo, getCurrentValue } from './current';
import { getFields, getData, getWindowStart, getNonce } from './fields'; import { getFields, getData, getWindowStart, getNonce, getTimeBounty } from './fields';
import { makeTransaction, IHexStrTransaction } from 'libs/transaction'; import { makeTransaction, IHexStrTransaction } from 'libs/transaction';
import EthTx from 'ethereumjs-tx'; import EthTx from 'ethereumjs-tx';
import { getUnit } from 'selectors/transaction/meta'; import { getUnit } from 'selectors/transaction/meta';
@ -17,8 +17,12 @@ import { Wei, Address } from 'libs/units';
import { getTransactionFields } from 'libs/transaction/utils/ether'; import { getTransactionFields } from 'libs/transaction/utils/ether';
import { getNetworkConfig, getLatestBlock } from 'selectors/config'; import { getNetworkConfig, getLatestBlock } from 'selectors/config';
import BN from 'bn.js'; import BN from 'bn.js';
import abi from 'ethereumjs-abi'; import {
import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling'; EAC_SCHEDULING_CONFIG,
calcEACEndowment,
EAC_ADDRESSES,
getScheduleData
} from 'libs/scheduling';
const getTransactionState = (state: AppState) => state.transaction; const getTransactionState = (state: AppState) => state.transaction;
@ -55,54 +59,6 @@ const getTransaction = (state: AppState): IGetTransaction => {
return { transaction, isFullTransaction }; 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 getSchedulingTransaction = (state: AppState): IGetSchedulingTransaction => {
const currentTo = getCurrentTo(state); const currentTo = getCurrentTo(state);
const currentValue = getCurrentValue(state); const currentValue = getCurrentValue(state);
@ -115,6 +71,7 @@ const getSchedulingTransaction = (state: AppState): IGetSchedulingTransaction =>
const gasLimit = getGasLimit(state); const gasLimit = getGasLimit(state);
const nonce = getNonce(state); const nonce = getNonce(state);
const gasPrice = getGasPrice(state); const gasPrice = getGasPrice(state);
const timeBounty = getTimeBounty(state);
const isFullTransaction = isFullTx( const isFullTransaction = isFullTx(
state, state,
@ -126,43 +83,29 @@ const getSchedulingTransaction = (state: AppState): IGetSchedulingTransaction =>
unit 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( const transactionData = getScheduleData(
currentTo.raw, currentTo.raw,
callData.raw, callData.raw,
parseInt(gasLimit.raw, 10), parseInt(gasLimit.raw, 10),
currentValue.value, currentValue.value,
WINDOW_SIZE_IN_BLOCKS, EAC_SCHEDULING_CONFIG.WINDOW_SIZE_IN_BLOCKS,
windowStart.value, windowStart.value,
gasPrice.value, gasPrice.value,
EAC_FEE, timeBounty.value,
PAYMENT, EAC_SCHEDULING_CONFIG.REQUIRED_DEPOSIT
REQUIRED_DEPOSIT
); );
const endowment = calcEACEndowment( const endowment = calcEACEndowment(
gasLimit.value || 21000, gasLimit.value || new BN(21000),
currentValue.value || 0, currentValue.value || new BN(0),
gasPrice.value, gasPrice.value,
EAC_FEE, timeBounty.value
PAYMENT
); );
const transactionOptions = { const transactionOptions = {
to: Address(EAC_ADDRESSES.KOVAN.blockScheduler), to: Address(EAC_ADDRESSES.KOVAN.blockScheduler),
data: transactionData, data: transactionData,
gasLimit: SCHEDULING_GAS_LIMIT, gasLimit: EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT,
gasPrice: gasPrice.value, gasPrice: gasPrice.value,
nonce: new BN(0), nonce: new BN(0),
value: endowment value: endowment
@ -229,6 +172,5 @@ export {
getTransactionState, getTransactionState,
getGasCost, getGasCost,
nonStandardTransaction, nonStandardTransaction,
serializedAndTransactionFieldsMatch, serializedAndTransactionFieldsMatch
getScheduleData
}; };

View File

@ -16,7 +16,7 @@ import createSagaMiddleware from 'redux-saga';
import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage'; import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage';
import RootReducer, { AppState } from 'reducers'; import RootReducer, { AppState } from 'reducers';
import sagas from 'sagas'; import sagas from 'sagas';
import { gasPricetoBase } from 'libs/units'; import { gasPriceToBase } from 'libs/units';
import { import {
rehydrateConfigAndCustomTokenState, rehydrateConfigAndCustomTokenState,
getConfigAndCustomTokensStateToSubscribe getConfigAndCustomTokensStateToSubscribe
@ -59,7 +59,7 @@ const configureStore = () => {
savedTransactionState && savedTransactionState.fields.gasPrice savedTransactionState && savedTransactionState.fields.gasPrice
? { ? {
raw: savedTransactionState.fields.gasPrice.raw, raw: savedTransactionState.fields.gasPrice.raw,
value: gasPricetoBase(+savedTransactionState.fields.gasPrice.raw) value: gasPriceToBase(+savedTransactionState.fields.gasPrice.raw)
} }
: transactionInitialState.fields.gasPrice : transactionInitialState.fields.gasPrice
} }

View File

@ -1,8 +1,9 @@
import { TypeKeys } from 'actions/transaction/constants'; import { TypeKeys } from 'actions/transaction/constants';
import { gasPricetoBase } from 'libs/units'; import { gasPriceToBase, timeBountyRawToValue } from 'libs/units';
import { fields, State } from 'reducers/transaction/fields'; import { fields, State } from 'reducers/transaction/fields';
import * as txActions from 'actions/transaction'; import * as txActions from 'actions/transaction';
import BN from 'bn.js'; import BN from 'bn.js';
import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling';
describe('fields reducer', () => { describe('fields reducer', () => {
const INITIAL_STATE: State = { const INITIAL_STATE: State = {
@ -11,7 +12,11 @@ describe('fields reducer', () => {
nonce: { raw: '', value: null }, nonce: { raw: '', value: null },
value: { raw: '', value: null }, value: { raw: '', value: null },
gasLimit: { raw: '21000', value: new BN(21000) }, gasLimit: { raw: '21000', value: new BN(21000) },
gasPrice: { raw: '20', value: gasPricetoBase(20) }, gasPrice: { raw: '20', value: gasPriceToBase(20) },
timeBounty: {
raw: EAC_SCHEDULING_CONFIG.TIME_BOUNTY_DEFAULT.toString(),
value: timeBountyRawToValue(EAC_SCHEDULING_CONFIG.TIME_BOUNTY_DEFAULT)
},
windowStart: { raw: '', value: null } windowStart: { raw: '', value: null }
}; };
const testPayload = { raw: 'test', value: null }; const testPayload = { raw: 'test', value: null };

View File

@ -3,7 +3,7 @@ import { SagaIterator, delay } from 'redux-saga';
import { call, put } from 'redux-saga/effects'; import { call, put } from 'redux-saga/effects';
import { setDataField, setGasLimitField, setNonceField } from 'actions/transaction/actionCreators'; import { setDataField, setGasLimitField, setNonceField } from 'actions/transaction/actionCreators';
import { isValidHex, isValidNonce, gasPriceValidator, gasLimitValidator } from 'libs/validators'; import { isValidHex, isValidNonce, gasPriceValidator, gasLimitValidator } from 'libs/validators';
import { Data, Wei, Nonce, gasPricetoBase } from 'libs/units'; import { Data, Wei, Nonce, gasPriceToBase } from 'libs/units';
import { import {
handleDataInput, handleDataInput,
handleGasLimitInput, handleGasLimitInput,
@ -129,7 +129,7 @@ describe('handleGasPriceInput*', () => {
put( put(
setGasPriceField({ setGasPriceField({
raw: payload, raw: payload,
value: gasPricetoBase(priceFloat) value: gasPriceToBase(priceFloat)
}) })
) )
); );

View File

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

View File

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