[FEATURE] Call contract to validate scheduling params

This commit is contained in:
Daniel Kmak 2018-04-05 14:44:31 +02:00 committed by Bagaric
parent 1630e9fee9
commit 2cf2cc5b26
17 changed files with 391 additions and 60 deletions

View File

@ -20,7 +20,8 @@ import {
SetScheduleTimezoneAction,
SetScheduleGasPriceFieldAction,
SetScheduleGasLimitFieldAction,
SetScheduleDepositFieldAction
SetScheduleDepositFieldAction,
SetScheduleParamsValidityAction
} from '../actionTypes';
import { TypeKeys } from 'actions/transaction/constants';
@ -162,6 +163,12 @@ const setScheduleDepositField = (payload: SetScheduleDepositFieldAction['payload
payload
});
type TSetScheduleParamsValidity = typeof setScheduleParamsValidity;
const setScheduleParamsValidity = (payload: SetScheduleParamsValidityAction['payload']) => ({
type: TypeKeys.SCHEDULE_PARAMS_VALIDITY_SET,
payload
});
type TReset = typeof reset;
const reset = (payload: ResetAction['payload'] = { include: {}, exclude: {} }): ResetAction => ({
type: TypeKeys.RESET,
@ -190,6 +197,7 @@ export {
TSetScheduleGasPriceField,
TSetScheduleGasLimitField,
TSetScheduleDepositField,
TSetScheduleParamsValidity,
TReset,
inputGasLimit,
inputGasPrice,
@ -212,5 +220,6 @@ export {
setScheduleGasPriceField,
setScheduleGasLimitField,
setScheduleDepositField,
setScheduleParamsValidity,
reset
};

View File

@ -163,6 +163,14 @@ interface SetScheduleDepositFieldAction {
};
}
interface SetScheduleParamsValidityAction {
type: TypeKeys.SCHEDULE_PARAMS_VALIDITY_SET;
payload: {
raw: boolean;
value: boolean;
};
}
type InputFieldAction = InputNonceAction | InputGasLimitAction | InputDataAction;
type FieldAction =
@ -181,7 +189,8 @@ type FieldAction =
| SetScheduleGasPriceFieldAction
| SetScheduleGasLimitFieldAction
| SetScheduleDepositFieldAction
| SetScheduleTimezoneAction;
| SetScheduleTimezoneAction
| SetScheduleParamsValidityAction;
export {
InputGasLimitAction,
@ -208,5 +217,6 @@ export {
SetScheduleGasPriceFieldAction,
SetScheduleGasLimitFieldAction,
SetScheduleDepositFieldAction,
SetScheduleTimezoneAction
SetScheduleTimezoneAction,
SetScheduleParamsValidityAction
};

View File

@ -57,6 +57,7 @@ export enum TypeKeys {
SCHEDULE_TYPE_SET = 'SCHEDULE_TYPE_SET',
SCHEDULING_TOGGLE_SET = 'SCHEDULING_TOGGLE_SET',
SCHEDULE_DEPOSIT_FIELD_SET = 'SCHEDULE_DEPOSIT_FIELD_SET',
SCHEDULE_PARAMS_VALIDITY_SET = 'SCHEDULE_PARAMS_VALIDITY_SET',
TOKEN_TO_META_SET = 'TOKEN_TO_META_SET',
UNIT_META_SET = 'UNIT_META_SET',

View File

@ -1,32 +1,48 @@
import React from 'react';
import React, { Component } from 'react';
import translate from 'translations';
import { ConfirmationModal } from 'components/ConfirmationModal';
import { SigningStatus } from 'components';
import { SendScheduleTransactionButtonFactory } from 'containers/Tabs/ScheduleTransaction/components/SendScheduleTransactionButtonFactory';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getScheduleParamsValidity } from 'selectors/transaction';
export const SendScheduleTransactionButton: React.SFC<{
interface Props {
className?: string;
signing?: boolean;
customModal?: typeof ConfirmationModal;
}> = ({ signing, customModal, className }) => (
<React.Fragment>
<SendScheduleTransactionButtonFactory
signing={signing}
Modal={customModal ? customModal : ConfirmationModal}
withProps={({ disabled, openModal, signTx }) => (
<React.Fragment>
<button
disabled={disabled}
className={`SendButton btn btn-primary btn-block ${className}`}
onClick={() => {
!!signing ? (signTx(), openModal()) : openModal();
}}
>
{translate('SCHEDULE_SCHEDULE')}
</button>
</React.Fragment>
)}
/>
<SigningStatus />
</React.Fragment>
);
paramsValidity: boolean;
}
class SendScheduleTransactionButtonClass extends Component<Props> {
public render() {
const { className, customModal, paramsValidity, signing } = this.props;
return (
<React.Fragment>
<SendScheduleTransactionButtonFactory
signing={signing}
Modal={customModal ? customModal : ConfirmationModal}
withProps={({ disabled, openModal, signTx }) => (
<React.Fragment>
<button
disabled={disabled || !paramsValidity}
className={`SendButton btn btn-primary btn-block ${className}`}
onClick={() => {
!!signing ? (signTx(), openModal()) : openModal();
}}
>
{translate('SCHEDULE_SCHEDULE')}
</button>
</React.Fragment>
)}
/>
<SigningStatus />
</React.Fragment>
);
}
}
export const SendScheduleTransactionButton = connect((state: AppState) => ({
paramsValidity: getScheduleParamsValidity(state).value
}))(SendScheduleTransactionButtonClass);

View File

@ -0,0 +1,3 @@
import { schedulingParamsValidity } from './paramsValidity';
export const schedulingTransactionNetworkSagas = [schedulingParamsValidity];

View File

@ -0,0 +1,149 @@
import { SagaIterator, delay } from 'redux-saga';
import { select, fork, call, take, apply, put } from 'redux-saga/effects';
import { getOffline, getNodeLib } from 'selectors/config';
import {
ICurrentSchedulingToggle,
ICurrentWindowSize
} from 'containers/Tabs/ScheduleTransaction/selectors';
import {
getSchedulingToggle,
getScheduleTimestamp,
getScheduleTimezone
} from '../../../selectors/fields';
import {
TypeKeys,
SetScheduleParamsValidityAction,
setScheduleParamsValidity
} from 'actions/transaction';
import {
getCurrentTo,
getCurrentValue,
getData,
getScheduleType,
getWindowStart,
getWindowSize,
getTimeBounty,
getScheduleGasPrice,
getScheduleGasLimit,
getScheduleDeposit
} from 'selectors/transaction';
import { getWalletInst } from 'selectors/wallet';
import {
EAC_SCHEDULING_CONFIG,
calcEACEndowment,
getValidateRequestParamsData,
EAC_ADDRESSES,
parseSchedulingParametersValidity
} from 'libs/scheduling';
import { gasPriceToBase } from 'libs/units';
import BN from 'bn.js';
import { bufferToHex } from 'ethereumjs-util';
import RequestFactory from 'libs/scheduling/contracts/RequestFactory';
import { dateTimeToUnixTimestamp, windowSizeBlockToMin } from 'selectors/transaction/helpers';
export function* shouldValidateParams(): SagaIterator {
while (true) {
yield take([
TypeKeys.TO_FIELD_SET,
TypeKeys.DATA_FIELD_SET,
TypeKeys.CURRENT_TIME_BOUNTY_SET,
TypeKeys.WINDOW_SIZE_FIELD_SET,
TypeKeys.WINDOW_START_FIELD_SET,
TypeKeys.SCHEDULE_TIMESTAMP_FIELD_SET,
TypeKeys.TIME_BOUNTY_FIELD_SET,
TypeKeys.SCHEDULE_TYPE_SET,
TypeKeys.SCHEDULING_TOGGLE_SET,
TypeKeys.SCHEDULE_TIMEZONE_SET
]);
yield call(delay, 250);
const isOffline: boolean = yield select(getOffline);
const schedulingToggle: ICurrentSchedulingToggle = yield select(getSchedulingToggle);
const scheduling = Boolean(schedulingToggle && schedulingToggle.value);
if (isOffline || !scheduling) {
continue;
}
yield call(checkSchedulingParametersValidity);
}
}
function* checkSchedulingParametersValidity() {
const currentTo = yield select(getCurrentTo);
const currentValue = yield select(getCurrentValue);
const callData = yield select(getData);
const scheduleType = yield select(getScheduleType);
const windowStart = yield select(getWindowStart);
const windowSize: ICurrentWindowSize = yield select(getWindowSize);
const timeBounty = yield select(getTimeBounty);
const scheduleGasPrice = yield select(getScheduleGasPrice);
const scheduleGasLimit = yield select(getScheduleGasLimit);
const deposit = yield select(getScheduleDeposit);
const node = yield select(getNodeLib);
const wallet = yield select(getWalletInst);
const scheduleTimestamp = yield select(getScheduleTimestamp);
const scheduleTimezone = yield select(getScheduleTimezone);
if (
!currentValue.value ||
!currentTo.value ||
!scheduleGasPrice.value ||
!wallet ||
!windowSize.value
) {
return;
}
const callGasLimit = scheduleGasLimit.value || EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_LIMIT_FALLBACK;
const endowment = calcEACEndowment(
callGasLimit,
currentValue.value || new BN(0),
scheduleGasPrice.value || gasPriceToBase(EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_PRICE_FALLBACK),
timeBounty.value
);
const fromAddress = yield apply(wallet, wallet.getAddressString);
const data = getValidateRequestParamsData(
bufferToHex(currentTo.value),
callData.value ? bufferToHex(callData.value) : '',
callGasLimit,
currentValue.value,
windowSizeBlockToMin(windowSize.value, scheduleType.value) || 0,
scheduleType.value === 'time'
? dateTimeToUnixTimestamp(scheduleTimestamp, scheduleTimezone.value)
: windowStart.value,
scheduleGasPrice.value,
timeBounty.value,
deposit.value || new BN(0),
scheduleType.value === 'time',
endowment,
fromAddress
);
const callResult: string = yield apply(node, node.sendCallRequest, [
{
to: EAC_ADDRESSES.KOVAN.requestFactory,
data
}
]);
const { paramsValidity } = RequestFactory.validateRequestParams.decodeOutput(callResult);
const errors = parseSchedulingParametersValidity(paramsValidity);
const paramsValid = errors.length === 0;
yield call(setField, {
raw: paramsValid,
value: paramsValid
});
}
export function* setField(payload: SetScheduleParamsValidityAction['payload']) {
yield put(setScheduleParamsValidity(payload));
}
export const schedulingParamsValidity = fork(shouldValidateParams);

View File

@ -11,6 +11,7 @@ const getSchedulingToggle = (state: AppState) => getFields(state).schedulingTogg
const getScheduleGasLimit = (state: AppState) => getFields(state).scheduleGasLimit;
const getScheduleGasPrice = (state: AppState) => getFields(state).scheduleGasPrice;
const getScheduleDeposit = (state: AppState) => getFields(state).scheduleDeposit;
const getScheduleParamsValidity = (state: AppState) => getFields(state).scheduleParamsValidity;
const schedulingFields = [
'windowStart',
@ -20,7 +21,8 @@ const schedulingFields = [
'schedulingToggle',
'scheduleDeposit',
'scheduleGasLimit',
'scheduleGasPrice'
'scheduleGasPrice',
'scheduleParamsValidity'
];
export {
@ -34,5 +36,6 @@ export {
getScheduleGasLimit,
getScheduleGasPrice,
getScheduleDeposit,
schedulingFields
schedulingFields,
getScheduleParamsValidity
};

View File

@ -1,6 +1,5 @@
import { AppState } from 'reducers';
import {
IGetTransaction,
getCurrentTo,
getCurrentValue,
getFields,
@ -21,7 +20,8 @@ import {
isValidScheduleDeposit,
getScheduleDeposit,
getScheduleTimestamp,
getScheduleTimezone
getScheduleTimezone,
IGetTransaction
} from 'selectors/transaction';
import { Address, gasPriceToBase } from 'libs/units';
import {
@ -43,7 +43,7 @@ import {
import EthTx from 'ethereumjs-tx';
import { getLatestBlock } from 'selectors/config';
const getSchedulingTransaction = (state: AppState): IGetTransaction => {
export const getSchedulingTransaction = (state: AppState): IGetTransaction => {
const currentTo = getCurrentTo(state);
const currentValue = getCurrentValue(state);
const transactionFields = getFields(state);
@ -69,6 +69,13 @@ const getSchedulingTransaction = (state: AppState): IGetTransaction => {
const depositValid = isValidScheduleDeposit(state);
const deposit = getScheduleDeposit(state);
const endowment = calcEACEndowment(
scheduleGasLimit.value || EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_LIMIT_FALLBACK,
currentValue.value || new BN(0),
scheduleGasPrice.value || gasPriceToBase(EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_PRICE_FALLBACK),
timeBounty.value
);
const isFullTransaction =
isFullTx(state, transactionFields, currentTo, currentValue, dataExists, validGasCost, unit) &&
(windowStartValid || scheduleTimestampValid) &&
@ -77,26 +84,23 @@ const getSchedulingTransaction = (state: AppState): IGetTransaction => {
scheduleGasLimitValid &&
depositValid;
const transactionData = getScheduleData(
currentTo.raw,
callData.raw,
scheduleGasLimit.value,
currentValue.value,
windowSizeBlockToMin(windowSize.value, scheduleType.value),
scheduleType.value === 'time'
? dateTimeToUnixTimestamp(scheduleTimestamp, scheduleTimezone.value)
: windowStart.value,
scheduleGasPrice.value,
timeBounty.value,
deposit.value
);
let transactionData = null;
const endowment = calcEACEndowment(
scheduleGasLimit.value || EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_LIMIT_FALLBACK,
currentValue.value || new BN(0),
scheduleGasPrice.value || gasPriceToBase(EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_PRICE_FALLBACK),
timeBounty.value
);
if (isFullTransaction) {
transactionData = getScheduleData(
currentTo.raw,
callData.raw,
scheduleGasLimit.value,
currentValue.value,
windowSizeBlockToMin(windowSize.value, scheduleType.value),
scheduleType.value === 'time'
? dateTimeToUnixTimestamp(scheduleTimestamp, scheduleTimezone.value)
: windowStart.value,
scheduleGasPrice.value,
timeBounty.value,
deposit.value
);
}
const transactionOptions = {
to: Address(
@ -122,5 +126,3 @@ const getSchedulingTransaction = (state: AppState): IGetTransaction => {
isFullTransaction
};
};
export { getSchedulingTransaction };

View File

@ -120,7 +120,7 @@ export default class AbiFunction {
this.inputNames.map(name => {
const type = this.funcParams[name].type;
//TODO: parse args based on type
if (!suppliedArgs[name]) {
if (typeof suppliedArgs[name] === 'undefined') {
throw Error(
`Expected argument "${name}" of type "${type}" missing, suppliedArgs: ${JSON.stringify(
suppliedArgs,

View File

@ -0,0 +1,58 @@
import Contract from 'libs/contracts';
interface ABIFunc<T, K = void> {
encodeInput(x: T): string;
decodeInput(argStr: string): T;
decodeOutput(argStr: string): K;
}
type address = any;
type uint256 = any;
type bytes = any;
interface IRequestFactory {
validateRequestParams: ABIFunc<
{ _addressArgs: address[]; _uintArgs: uint256[]; _callData: bytes; _endowment: uint256 },
{ paramsValidity: boolean[] }
>;
}
const requestFactoryAbi = [
{
constant: true,
inputs: [
{
name: '_addressArgs',
type: 'address[3]'
},
{
name: '_uintArgs',
type: 'uint256[12]'
},
{
name: '_callData',
type: 'bytes'
},
{
name: '_endowment',
type: 'uint256'
}
],
name: 'validateRequestParams',
outputs: [
{
name: '',
type: 'bool[6]'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
}
];
const outputMappings = {
validateRequestParams: ['paramsValidity']
};
export default (new Contract(requestFactoryAbi, outputMappings) as any) as IRequestFactory;

View File

@ -1,7 +1,8 @@
import BN from 'bn.js';
import abi from 'ethereumjs-abi';
import { toWei, Units } from './units';
import { toWei, Units } from '../units';
import { toBuffer } from 'ethereumjs-util';
import RequestFactory from './contracts/RequestFactory';
const TIME_BOUNTY_MIN = new BN('1');
@ -25,6 +26,7 @@ export const EAC_SCHEDULING_CONFIG = {
export const EAC_ADDRESSES = {
KOVAN: {
blockScheduler: '0x1afc19a7e642761ba2b55d2a45b32c7ef08269d1',
requestFactory: '0x496e2b6089bde77293a994469b08e9f266d87adb',
timestampScheduler: '0xc6370807f0164bdf10a66c08d0dab1028dbe80a3'
}
};
@ -106,6 +108,67 @@ export const getScheduleData = (
]);
};
export const parseSchedulingParametersValidity = (isValid: boolean[]) => {
const Errors = [
'InsufficientEndowment',
'ReservedWindowBiggerThanExecutionWindow',
'InvalidTemporalUnit',
'ExecutionWindowTooSoon',
'CallGasTooHigh',
'EmptyToAddress'
];
const errors: string[] = [];
isValid.forEach((boolIsTrue, index) => {
if (!boolIsTrue) {
errors.push(Errors[index]);
}
});
return errors;
};
export const getValidateRequestParamsData = (
toAddress: string,
callData = '',
callGas: BN,
callValue: any,
windowSize: number,
windowStart: BN,
gasPrice: BN,
timeBounty: BN,
requiredDeposit: BN,
isTimestamp: boolean,
endowment: BN,
fromAddress: string
): string => {
const temporalUnit = isTimestamp ? 2 : 1;
const freezePeriod = isTimestamp ? 3 * 60 : 10; // 3 minutes or 10 blocks
const reservedWindowSize = isTimestamp ? 5 * 60 : 16; // 5 minutes or 16 blocks
const claimWindowSize = isTimestamp ? 60 * 60 : 255; // 60 minutes or 255 blocks
const feeRecipient = '0x0'; // stub
return RequestFactory.validateRequestParams.encodeInput({
_addressArgs: [fromAddress, feeRecipient, toAddress],
_uintArgs: [
EAC_SCHEDULING_CONFIG.FEE,
timeBounty,
claimWindowSize,
freezePeriod,
reservedWindowSize,
temporalUnit,
windowSize,
windowStart,
callGas,
callValue,
gasPrice,
requiredDeposit
],
_callData: callData,
_endowment: endowment
});
};
export const getTXDetailsCheckURL = (txHash: string) => {
return `${EAC_SCHEDULING_CONFIG.DAPP_ADDRESS}/awaiting/scheduler/${txHash}`;
};

View File

@ -44,7 +44,8 @@ const INITIAL_STATE: State = {
raw: EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_PRICE_FALLBACK.toString(),
value: gasPriceToBase(EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_PRICE_FALLBACK)
},
scheduleDeposit: { raw: '', value: null }
scheduleDeposit: { raw: '', value: null },
scheduleParamsValidity: { raw: true, value: true }
};
const updateField = (key: keyof State): Reducer<State> => (state: State, action: FieldAction) => ({
@ -114,6 +115,8 @@ export const fields = (
return updateField('scheduleGasPrice')(state, action);
case TK.SCHEDULE_DEPOSIT_FIELD_SET:
return updateField('scheduleDeposit')(state, action);
case TK.SCHEDULE_PARAMS_VALIDITY_SET:
return updateField('scheduleParamsValidity')(state, action);
case TK.TOKEN_TO_ETHER_SWAP:
return tokenToEther(state, action);
case TK.ETHER_TO_TOKEN_SWAP:

View File

@ -11,7 +11,8 @@ import {
SetScheduleGasPriceFieldAction,
SetScheduleGasLimitFieldAction,
SetScheduleDepositFieldAction,
SetScheduleTimezoneAction
SetScheduleTimezoneAction,
SetScheduleParamsValidityAction
} from 'actions/transaction';
import { Wei } from 'libs/units';
@ -32,4 +33,5 @@ export interface State {
scheduleGasLimit: SetScheduleGasLimitFieldAction['payload'];
scheduleGasPrice: SetScheduleGasPriceFieldAction['payload'];
scheduleDeposit: SetScheduleDepositFieldAction['payload'];
scheduleParamsValidity: SetScheduleParamsValidityAction['payload'];
}

View File

@ -1,4 +1,6 @@
import { from } from './from';
import { gas } from './gas';
import { nonce } from './nonce';
export const network = [from, ...gas, nonce];
import { schedulingTransactionNetworkSagas } from '../../../containers/Tabs/ScheduleTransaction/sagas/transaction/network';
export const network = [from, ...gas, nonce, ...schedulingTransactionNetworkSagas];

View File

@ -33,7 +33,8 @@ describe('fields reducer', () => {
value: gasPriceToBase(EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_PRICE_FALLBACK)
},
scheduleGasLimit: { raw: '21000', value: new BN(21000) },
scheduleDeposit: { raw: '', value: null }
scheduleDeposit: { raw: '', value: null },
scheduleParamsValidity: { raw: true, value: true }
};
const testPayload = { raw: 'test', value: null };

View File

@ -81,6 +81,10 @@ describe('fields selector', () => {
scheduleDeposit: {
raw: '1000000000',
value: Wei('1000000000')
},
scheduleParamsValidity: {
raw: false,
value: false
}
};

View File

@ -85,6 +85,10 @@ describe('helpers selector', () => {
scheduleDeposit: {
raw: '1000000000',
value: Wei('1000000000')
},
scheduleParamsValidity: {
raw: false,
value: false
}
}
};
@ -106,7 +110,8 @@ describe('helpers selector', () => {
scheduleTimezone: moment.tz.guess(),
scheduleGasPrice: Wei('1500'),
scheduleGasLimit: Wei('21000'),
scheduleDeposit: Wei('1000000000')
scheduleDeposit: Wei('1000000000'),
scheduleParamsValidity: false
};
expect(reduceToValues(state.transaction.fields)).toEqual(values);
});