From d2b3d9c47f85d4d48e9a532ffdc9bb2aacce21e4 Mon Sep 17 00:00:00 2001 From: Daniel Kmak Date: Wed, 21 Mar 2018 11:14:08 +0300 Subject: [PATCH] [FEATURE] TimeBounty slider. --- .../transaction/actionCreators/fields.ts | 29 ++++++ .../actions/transaction/actionTypes/fields.ts | 20 +++++ common/actions/transaction/constants.ts | 3 + .../TXMetaDataPanel/TXMetaDataPanel.tsx | 2 + .../components/AdvancedGas.tsx | 21 +++-- .../components/FeeSummary.scss | 4 + .../components/SchedulingFeeSummary.tsx | 29 +++--- .../TXMetaDataPanel/components/TimeBounty.tsx | 75 ++++++++++++++++ common/libs/scheduling.ts | 68 +++++++++++++- common/libs/units.ts | 19 +++- common/libs/validators.ts | 10 +++ common/reducers/transaction/fields/fields.ts | 11 ++- common/reducers/transaction/fields/typings.ts | 3 +- common/sagas/transaction/fields/fields.ts | 46 ++++++++-- common/selectors/transaction/fields.ts | 2 + common/selectors/transaction/transaction.ts | 90 ++++--------------- common/store/store.ts | 4 +- .../transaction/fields/fields.spec.ts | 9 +- spec/sagas/transaction/fields/fields.spec.ts | 4 +- spec/selectors/transaction/fields.spec.ts | 4 + spec/selectors/transaction/helpers.spec.ts | 5 ++ 21 files changed, 344 insertions(+), 114 deletions(-) create mode 100644 common/components/TXMetaDataPanel/components/TimeBounty.tsx diff --git a/common/actions/transaction/actionCreators/fields.ts b/common/actions/transaction/actionCreators/fields.ts index 96251a81..7736e8d1 100644 --- a/common/actions/transaction/actionCreators/fields.ts +++ b/common/actions/transaction/actionCreators/fields.ts @@ -7,13 +7,16 @@ import { InputGasLimitAction, InputGasPriceAction, InputGasPriceIntentAction, + InputTimeBountyAction, InputDataAction, InputNonceAction, ResetAction, SetGasPriceFieldAction, + SetTimeBountyFieldAction, SetWindowStartFieldAction } from '../actionTypes'; import { TypeKeys } from 'actions/transaction/constants'; +import { InputTimeBountyIntentAction } from 'actions/transaction'; type TInputGasLimit = typeof inputGasLimit; const inputGasLimit = (payload: InputGasLimitAction['payload']) => ({ @@ -33,6 +36,26 @@ const inputGasPriceIntent = (payload: InputGasPriceIntentAction['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; const inputNonce = (payload: InputNonceAction['payload']) => ({ type: TypeKeys.NONCE_INPUT, @@ -99,6 +122,8 @@ export { TInputGasLimit, TInputGasPrice, TInputGasPriceIntent, + TInputTimeBounty, + TInputTimeBountyIntent, TInputNonce, TInputData, TSetGasLimitField, @@ -108,10 +133,14 @@ export { TSetValueField, TSetGasPriceField, TSetWindowStartField, + TSetTimeBountyField, TReset, inputGasLimit, inputGasPrice, inputGasPriceIntent, + inputTimeBounty, + inputTimeBountyIntent, + setTimeBountyField, inputNonce, inputData, setGasLimitField, diff --git a/common/actions/transaction/actionTypes/fields.ts b/common/actions/transaction/actionTypes/fields.ts index 23b8c323..de10442a 100644 --- a/common/actions/transaction/actionTypes/fields.ts +++ b/common/actions/transaction/actionTypes/fields.ts @@ -14,6 +14,14 @@ interface InputGasPriceIntentAction { type: TypeKeys.GAS_PRICE_INPUT_INTENT; payload: string; } +interface InputTimeBountyAction { + type: TypeKeys.TIME_BOUNTY_INPUT; + payload: string; +} +interface InputTimeBountyIntentAction { + type: TypeKeys.TIME_BOUNTY_INPUT_INTENT; + payload: string; +} interface InputDataAction { type: TypeKeys.DATA_FIELD_INPUT; payload: string; @@ -43,6 +51,14 @@ interface SetGasPriceFieldAction { }; } +interface SetTimeBountyFieldAction { + type: TypeKeys.TIME_BOUNTY_FIELD_SET; + payload: { + raw: string; + value: Wei | null; + }; +} + interface SetDataFieldAction { type: TypeKeys.DATA_FIELD_SET; payload: { @@ -92,12 +108,15 @@ type FieldAction = | SetNonceFieldAction | SetValueFieldAction | SetGasPriceFieldAction + | SetTimeBountyFieldAction | SetWindowStartFieldAction; export { InputGasLimitAction, InputGasPriceAction, InputGasPriceIntentAction, + InputTimeBountyAction, + InputTimeBountyIntentAction, InputDataAction, InputNonceAction, SetGasLimitFieldAction, @@ -108,5 +127,6 @@ export { FieldAction, InputFieldAction, SetGasPriceFieldAction, + SetTimeBountyFieldAction, SetWindowStartFieldAction }; diff --git a/common/actions/transaction/constants.ts b/common/actions/transaction/constants.ts index 043cd1f5..4ed42b9e 100644 --- a/common/actions/transaction/constants.ts +++ b/common/actions/transaction/constants.ts @@ -31,6 +31,8 @@ export enum TypeKeys { GAS_LIMIT_INPUT = 'GAS_LIMIT_INPUT', GAS_PRICE_INPUT = 'GAS_PRICE_INPUT', 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', DATA_FIELD_SET = 'DATA_FIELD_SET', @@ -39,6 +41,7 @@ export enum TypeKeys { VALUE_FIELD_SET = 'VALUE_FIELD_SET', NONCE_FIELD_SET = 'NONCE_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', TOKEN_TO_META_SET = 'TOKEN_TO_META_SET', diff --git a/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx b/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx index 1f6aac12..910be50e 100644 --- a/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx +++ b/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx @@ -94,8 +94,10 @@ class TXMetaDataPanel extends React.Component { const { offline, disableToggle, advancedGasOptions, className = '', scheduling } = this.props; const { gasPrice } = this.state; const showAdvanced = this.state.sliderState === 'advanced' || offline; + return (
+
{showAdvanced ? ( { } private renderFee() { - const { gasPrice, scheduling } = this.props; + const { gasPrice, scheduling, timeBounty } = this.props; const { feeSummary } = this.state.options; if (!feeSummary) { @@ -132,11 +133,17 @@ class AdvancedGas extends React.Component { render={({ gasPriceWei, gasLimit, fee, usd }) => (
- {EAC_SCHEDULING_CONFIG.PAYMENT} + {gasPriceWei} * ({ - EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT - }{' '} - + {EAC_SCHEDULING_CONFIG.FUTURE_EXECUTION_COST} + {gasLimit}) = {fee}{' '} - {usd && ~= ${usd} USD} + {' '} + + {timeBounty && timeBounty.value.toString()} + {gasPriceWei} * ({EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT.add( + EAC_SCHEDULING_CONFIG.FUTURE_EXECUTION_COST + ).toString()}{' '} + + {gasLimit}) = {fee} {usd && ~= ${usd} USD}
)} diff --git a/common/components/TXMetaDataPanel/components/FeeSummary.scss b/common/components/TXMetaDataPanel/components/FeeSummary.scss index 3910465e..ad2706ea 100644 --- a/common/components/TXMetaDataPanel/components/FeeSummary.scss +++ b/common/components/TXMetaDataPanel/components/FeeSummary.scss @@ -9,3 +9,7 @@ text-align: center; font-size: 14px; } + +.SchedulingFeeSummary { + font-size: 12px; +} diff --git a/common/components/TXMetaDataPanel/components/SchedulingFeeSummary.tsx b/common/components/TXMetaDataPanel/components/SchedulingFeeSummary.tsx index 80620989..31f051ac 100644 --- a/common/components/TXMetaDataPanel/components/SchedulingFeeSummary.tsx +++ b/common/components/TXMetaDataPanel/components/SchedulingFeeSummary.tsx @@ -4,11 +4,11 @@ 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 { getGasLimit, getTimeBounty } from 'selectors/transaction'; import { UnitDisplay, Spinner } from 'components/ui'; import { NetworkConfig } from 'types/network'; import './FeeSummary.scss'; -import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling'; +import { calcEACTotalCost } from 'libs/scheduling'; interface RenderData { gasPriceWei: string; @@ -24,6 +24,7 @@ interface ReduxStateProps { network: NetworkConfig; isOffline: AppState['config']['meta']['offline']; isGasEstimating: AppState['gas']['isEstimating']; + timeBounty: AppState['transaction']['fields']['timeBounty']; } interface OwnProps { @@ -35,7 +36,15 @@ type Props = OwnProps & ReduxStateProps; class SchedulingFeeSummary extends React.Component { public render() { - const { gasPrice, gasLimit, rates, network, isOffline, isGasEstimating } = this.props; + const { + gasPrice, + gasLimit, + rates, + network, + isOffline, + isGasEstimating, + timeBounty + } = this.props; if (isGasEstimating) { return ( @@ -48,13 +57,8 @@ class SchedulingFeeSummary extends React.Component { 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)); + timeBounty.value && + calcEACTotalCost(gasLimit.value, gasPrice.value, timeBounty.value); const fee = ( { ); return ( -
+
{this.props.render({ gasPriceWei: gasPrice.value.toString(), gasPriceGwei: gasPrice.raw, @@ -99,7 +103,8 @@ function mapStateToProps(state: AppState): ReduxStateProps { rates: state.rates.rates, network: getNetworkConfig(state), isOffline: getOffline(state), - isGasEstimating: getIsEstimating(state) + isGasEstimating: getIsEstimating(state), + timeBounty: getTimeBounty(state) }; } diff --git a/common/components/TXMetaDataPanel/components/TimeBounty.tsx b/common/components/TXMetaDataPanel/components/TimeBounty.tsx new file mode 100644 index 00000000..8efa47cb --- /dev/null +++ b/common/components/TXMetaDataPanel/components/TimeBounty.tsx @@ -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 { + public render() { + const { timeBounty } = this.props; + + const bounds = { + max: EAC_SCHEDULING_CONFIG.TIME_BOUNTY_MAX, + min: EAC_SCHEDULING_CONFIG.TIME_BOUNTY_MIN + }; + + return ( +
+
+ +
+ +
+
+ +
+ {translate('Small')} + {translate('Big')} +
+
+
+
+ ); + } + + 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; diff --git a/common/libs/scheduling.ts b/common/libs/scheduling.ts index 739c010f..ac58d294 100644 --- a/common/libs/scheduling.ts +++ b/common/libs/scheduling.ts @@ -1,7 +1,67 @@ +import BN from 'bn.js'; +import abi from 'ethereumjs-abi'; + export const EAC_SCHEDULING_CONFIG = { - SCHEDULING_GAS_LIMIT: 1500000, - FUTURE_EXECUTION_COST: 180000, - FEE_MULTIPLIER: 2, - PAYMENT: 10000000, + FEE: new BN('2242000000000000'), // $2 + FEE_MULTIPLIER: new BN('2'), + FUTURE_EXECUTION_COST: new BN('180000'), + 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 }; + +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 + ]); +}; diff --git a/common/libs/units.ts b/common/libs/units.ts index 674c9fa0..10313dae 100644 --- a/common/libs/units.ts +++ b/common/libs/units.ts @@ -1,6 +1,7 @@ import BN from 'bn.js'; import { toBuffer, addHexPrefix } from 'ethereumjs-util'; import { stripHexPrefix } from 'libs/values'; +import { EAC_SCHEDULING_CONFIG } from './scheduling'; type UnitKey = keyof typeof Units; type Wei = BN; @@ -110,7 +111,19 @@ const convertTokenBase = (value: TokenValue, oldDecimal: number, newDecimal: num 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 { Data, @@ -126,5 +139,7 @@ export { UnitKey, Nonce, handleValues, - gasPricetoBase + gasPriceToBase, + timeBountyRawToValue, + timeBountyValueToRaw }; diff --git a/common/libs/validators.ts b/common/libs/validators.ts index d1247bb8..6e0ac442 100644 --- a/common/libs/validators.ts +++ b/common/libs/validators.ts @@ -12,6 +12,7 @@ import { GAS_PRICE_GWEI_UPPER_BOUND } from 'config/constants'; import { dPathRegex } from 'config/dpaths'; +import { EAC_SCHEDULING_CONFIG } from './scheduling'; // FIXME we probably want to do checksum checks sideways 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) => byteCode && byteCode.length > 0 && byteCode.length % 2 === 0; diff --git a/common/reducers/transaction/fields/fields.ts b/common/reducers/transaction/fields/fields.ts index 17e48e57..975b28d0 100644 --- a/common/reducers/transaction/fields/fields.ts +++ b/common/reducers/transaction/fields/fields.ts @@ -10,8 +10,9 @@ import { } from 'actions/transaction'; import { Reducer } from 'redux'; import { State } from './typings'; -import { gasPricetoBase } from 'libs/units'; +import { gasPriceToBase, timeBountyRawToValue } from 'libs/units'; import { resetHOF } from 'reducers/transaction/shared'; +import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling'; const INITIAL_STATE: State = { to: { raw: '', value: null }, @@ -20,7 +21,11 @@ const INITIAL_STATE: State = { value: { raw: '', value: null }, windowStart: { raw: '', value: null }, 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, action: FieldAction) => ({ @@ -70,6 +75,8 @@ export const fields = ( return updateField('nonce')(state, action); case TK.GAS_PRICE_FIELD_SET: return updateField('gasPrice')(state, action); + case TK.TIME_BOUNTY_FIELD_SET: + return updateField('timeBounty')(state, action); case TK.WINDOW_START_FIELD_SET: return updateField('windowStart')(state, action); case TK.TOKEN_TO_ETHER_SWAP: diff --git a/common/reducers/transaction/fields/typings.ts b/common/reducers/transaction/fields/typings.ts index da7b5fd1..cfafd595 100644 --- a/common/reducers/transaction/fields/typings.ts +++ b/common/reducers/transaction/fields/typings.ts @@ -11,8 +11,9 @@ 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 }; + timeBounty: { raw: string; value: Wei }; + windowStart: SetWindowStartFieldAction['payload']; } diff --git a/common/sagas/transaction/fields/fields.ts b/common/sagas/transaction/fields/fields.ts index 1671d247..6da9dfec 100644 --- a/common/sagas/transaction/fields/fields.ts +++ b/common/sagas/transaction/fields/fields.ts @@ -3,9 +3,11 @@ import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'; import { SagaIterator, delay } from 'redux-saga'; import { inputGasPrice, + inputTimeBounty, setDataField, setGasLimitField, setGasPriceField, + setTimeBountyField, setNonceField } from 'actions/transaction/actionCreators'; import { @@ -14,10 +16,20 @@ import { InputGasPriceAction, InputGasPriceIntentAction, InputNonceAction, - TypeKeys + TypeKeys, + InputTimeBountyIntentAction, + InputTimeBountyAction } from 'actions/transaction'; -import { isValidHex, isValidNonce, gasPriceValidator, gasLimitValidator } from 'libs/validators'; -import { Data, Wei, Nonce, gasPricetoBase } from 'libs/units'; +import { + 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 { const validData: boolean = yield call(isValidHex, payload); @@ -35,13 +47,13 @@ export function* handleGasPriceInput({ payload }: InputGasPriceAction): SagaIter yield put( setGasPriceField({ raw: payload, - value: validGasPrice ? gasPricetoBase(priceFloat) : new BN(0) + value: validGasPrice ? gasPriceToBase(priceFloat) : new BN(0) }) ); } 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 // action to go to reducers. yield put(inputGasPrice(payload)); @@ -52,10 +64,32 @@ export function* handleNonceInput({ payload }: InputNonceAction): SagaIterator { 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 = [ takeEvery(TypeKeys.DATA_FIELD_INPUT, handleDataInput), takeEvery(TypeKeys.GAS_LIMIT_INPUT, handleGasLimitInput), takeEvery(TypeKeys.GAS_PRICE_INPUT, handleGasPriceInput), + takeEvery(TypeKeys.TIME_BOUNTY_INPUT, handleTimeBountyInput), 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) ]; diff --git a/common/selectors/transaction/fields.ts b/common/selectors/transaction/fields.ts index bd6b13cb..ced6b077 100644 --- a/common/selectors/transaction/fields.ts +++ b/common/selectors/transaction/fields.ts @@ -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 getTimeBounty = (state: AppState) => getFields(state).timeBounty; const getWindowStart = (state: AppState) => getFields(state).windowStart; const getDataExists = (state: AppState) => { @@ -37,5 +38,6 @@ export { getGasPrice, getDataExists, getValidGasCost, + getTimeBounty, getWindowStart }; diff --git a/common/selectors/transaction/transaction.ts b/common/selectors/transaction/transaction.ts index 8dd4ff41..84aaa6cb 100644 --- a/common/selectors/transaction/transaction.ts +++ b/common/selectors/transaction/transaction.ts @@ -1,6 +1,6 @@ import { AppState } from 'reducers'; 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 EthTx from 'ethereumjs-tx'; import { getUnit } from 'selectors/transaction/meta'; @@ -17,8 +17,12 @@ import { Wei, Address } from 'libs/units'; import { getTransactionFields } from 'libs/transaction/utils/ether'; import { getNetworkConfig, getLatestBlock } from 'selectors/config'; import BN from 'bn.js'; -import abi from 'ethereumjs-abi'; -import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling'; +import { + EAC_SCHEDULING_CONFIG, + calcEACEndowment, + EAC_ADDRESSES, + getScheduleData +} from 'libs/scheduling'; const getTransactionState = (state: AppState) => state.transaction; @@ -55,54 +59,6 @@ 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); @@ -115,6 +71,7 @@ const getSchedulingTransaction = (state: AppState): IGetSchedulingTransaction => const gasLimit = getGasLimit(state); const nonce = getNonce(state); const gasPrice = getGasPrice(state); + const timeBounty = getTimeBounty(state); const isFullTransaction = isFullTx( state, @@ -126,43 +83,29 @@ const getSchedulingTransaction = (state: AppState): IGetSchedulingTransaction => 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, + EAC_SCHEDULING_CONFIG.WINDOW_SIZE_IN_BLOCKS, windowStart.value, gasPrice.value, - EAC_FEE, - PAYMENT, - REQUIRED_DEPOSIT + timeBounty.value, + EAC_SCHEDULING_CONFIG.REQUIRED_DEPOSIT ); const endowment = calcEACEndowment( - gasLimit.value || 21000, - currentValue.value || 0, + gasLimit.value || new BN(21000), + currentValue.value || new BN(0), gasPrice.value, - EAC_FEE, - PAYMENT + timeBounty.value ); const transactionOptions = { to: Address(EAC_ADDRESSES.KOVAN.blockScheduler), data: transactionData, - gasLimit: SCHEDULING_GAS_LIMIT, + gasLimit: EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT, gasPrice: gasPrice.value, nonce: new BN(0), value: endowment @@ -229,6 +172,5 @@ export { getTransactionState, getGasCost, nonStandardTransaction, - serializedAndTransactionFieldsMatch, - getScheduleData + serializedAndTransactionFieldsMatch }; diff --git a/common/store/store.ts b/common/store/store.ts index 0ef7bd91..dcac0e02 100644 --- a/common/store/store.ts +++ b/common/store/store.ts @@ -16,7 +16,7 @@ import createSagaMiddleware from 'redux-saga'; import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage'; import RootReducer, { AppState } from 'reducers'; import sagas from 'sagas'; -import { gasPricetoBase } from 'libs/units'; +import { gasPriceToBase } from 'libs/units'; import { rehydrateConfigAndCustomTokenState, getConfigAndCustomTokensStateToSubscribe @@ -59,7 +59,7 @@ const configureStore = () => { savedTransactionState && savedTransactionState.fields.gasPrice ? { raw: savedTransactionState.fields.gasPrice.raw, - value: gasPricetoBase(+savedTransactionState.fields.gasPrice.raw) + value: gasPriceToBase(+savedTransactionState.fields.gasPrice.raw) } : transactionInitialState.fields.gasPrice } diff --git a/spec/reducers/transaction/fields/fields.spec.ts b/spec/reducers/transaction/fields/fields.spec.ts index c2aa8dd2..fec177fa 100644 --- a/spec/reducers/transaction/fields/fields.spec.ts +++ b/spec/reducers/transaction/fields/fields.spec.ts @@ -1,8 +1,9 @@ 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 * as txActions from 'actions/transaction'; import BN from 'bn.js'; +import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling'; describe('fields reducer', () => { const INITIAL_STATE: State = { @@ -11,7 +12,11 @@ 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) }, + timeBounty: { + raw: EAC_SCHEDULING_CONFIG.TIME_BOUNTY_DEFAULT.toString(), + value: timeBountyRawToValue(EAC_SCHEDULING_CONFIG.TIME_BOUNTY_DEFAULT) + }, windowStart: { raw: '', value: null } }; const testPayload = { raw: 'test', value: null }; diff --git a/spec/sagas/transaction/fields/fields.spec.ts b/spec/sagas/transaction/fields/fields.spec.ts index 414b3492..692bdde7 100644 --- a/spec/sagas/transaction/fields/fields.spec.ts +++ b/spec/sagas/transaction/fields/fields.spec.ts @@ -3,7 +3,7 @@ import { SagaIterator, delay } from 'redux-saga'; import { call, put } from 'redux-saga/effects'; import { setDataField, setGasLimitField, setNonceField } from 'actions/transaction/actionCreators'; import { isValidHex, isValidNonce, gasPriceValidator, gasLimitValidator } from 'libs/validators'; -import { Data, Wei, Nonce, gasPricetoBase } from 'libs/units'; +import { Data, Wei, Nonce, gasPriceToBase } from 'libs/units'; import { handleDataInput, handleGasLimitInput, @@ -129,7 +129,7 @@ describe('handleGasPriceInput*', () => { put( setGasPriceField({ raw: payload, - value: gasPricetoBase(priceFloat) + value: gasPriceToBase(priceFloat) }) ) ); diff --git a/spec/selectors/transaction/fields.spec.ts b/spec/selectors/transaction/fields.spec.ts index 19406b0a..bfd90f0e 100644 --- a/spec/selectors/transaction/fields.spec.ts +++ b/spec/selectors/transaction/fields.spec.ts @@ -40,6 +40,10 @@ describe('fields selector', () => { raw: '1500', value: Wei('1500') }, + timeBounty: { + raw: '1500', + value: Wei('1500') + }, windowStart: { raw: '', value: null diff --git a/spec/selectors/transaction/helpers.spec.ts b/spec/selectors/transaction/helpers.spec.ts index dd37b42b..2c44d7a5 100644 --- a/spec/selectors/transaction/helpers.spec.ts +++ b/spec/selectors/transaction/helpers.spec.ts @@ -44,6 +44,10 @@ describe('helpers selector', () => { raw: '1500', value: Wei('1500') }, + timeBounty: { + raw: '1500', + value: Wei('1500') + }, windowStart: { raw: '', value: null @@ -59,6 +63,7 @@ describe('helpers selector', () => { nonce: new BN('0'), to: new Buffer([0, 1, 2, 3]), value: Wei('1000000000'), + timeBounty: Wei('1500'), windowStart: null }; expect(reduceToValues(state.transaction.fields)).toEqual(values);