Allow 0 ETH Transactions (#1307)

* Allow zero number

* Fail when request payment is zero value, or if you try to send token with zero value.

* Parseint instead of addition casting to catch empty string.
This commit is contained in:
William O'Beirne 2018-03-14 15:51:37 -04:00 committed by Daniel Ternyak
parent 16469e1a62
commit 4b8adc81ce
6 changed files with 106 additions and 22 deletions

View File

@ -12,7 +12,7 @@ import {
ICurrentValue
} from 'selectors/transaction/current';
import BN from 'bn.js';
import { validNumber, validDecimal } from 'libs/validators';
import { validPositiveNumber, validDecimal } from 'libs/validators';
import { getGasLimit } from 'selectors/transaction';
import { AddressField, AmountField, TXMetaDataPanel } from 'components';
import { SetGasLimitFieldAction } from 'actions/transaction/actionTypes/fields';
@ -45,7 +45,7 @@ interface ActionProps {
type Props = OwnProps & StateProps & ActionProps;
const isValidAmount = (decimal: number) => (amount: string) =>
validNumber(+amount) && validDecimal(amount, decimal);
validPositiveNumber(+amount) && validDecimal(amount, decimal);
class RequestPayment extends React.Component<Props, {}> {
public state = {

View File

@ -103,7 +103,8 @@ export function isValidEncryptedPrivKey(privkey: string): boolean {
}
}
export const validNumber = (num: number) => isFinite(num) && num > 0;
export const validNumber = (num: number) => isFinite(num) && num >= 0;
export const validPositiveNumber = (num: number) => validNumber(num) && num !== 0;
export const validDecimal = (input: string, decimal: number) => {
const arr = input.split('.');

View File

@ -16,7 +16,7 @@ import {
} from 'actions/transaction';
import { toTokenBase } from 'libs/units';
import { validateInput, IInput } from 'sagas/transaction/validationHelpers';
import { validNumber, validDecimal } from 'libs/validators';
import { validNumber, validPositiveNumber, validDecimal } from 'libs/validators';
export function* setCurrentValue(action: SetCurrentValueAction): SagaIterator {
const etherTransaction = yield select(isEtherTransaction);
@ -30,8 +30,10 @@ export function* valueHandler(
) {
const decimal: number = yield select(getDecimal);
const unit: string = yield select(getUnit);
const isEth = yield select(isEtherTransaction);
const validNum = isEth ? validNumber : validPositiveNumber;
if (!validNumber(+payload) || !validDecimal(payload, decimal)) {
if (!validNum(parseInt(payload, 10)) || !validDecimal(payload, decimal)) {
return yield put(setter({ raw: payload, value: null }));
}
const value = toTokenBase(payload, decimal);
@ -53,9 +55,11 @@ export function* revalidateCurrentValue(): SagaIterator {
}
export function* reparseCurrentValue(value: IInput): SagaIterator {
const isEth = yield select(isEtherTransaction);
const decimal = yield select(getDecimal);
const validNum = isEth ? validNumber : validPositiveNumber;
if (validNumber(+value.raw) && validDecimal(value.raw, decimal)) {
if (validNum(parseInt(value.raw, 10)) && validDecimal(value.raw, decimal)) {
return {
raw: value.raw,
value: toTokenBase(value.raw, decimal)

View File

@ -28,7 +28,7 @@ export function* rebaseUserInput(value: IInput): SagaIterator {
// get decimal
const newDecimal: number = yield select(getDecimalFromUnit, unit);
if (validNumber(+value.raw) && validDecimal(value.raw, newDecimal)) {
if (validNumber(parseInt(value.raw, 10)) && validDecimal(value.raw, newDecimal)) {
return {
raw: value.raw,
value: toTokenBase(value.raw, newDecimal)

View File

@ -20,6 +20,7 @@ const itShouldBeDone = (gen: SagaIterator) => {
describe('valueHandler', () => {
const action: any = { payload: '5.1' };
const zeroAction: any = { payload: '0' };
const setter = setValueField;
const decimal = 1;
const gen: { [key: string]: SagaIteratorClone } = {};
@ -29,45 +30,86 @@ describe('valueHandler', () => {
invalidNumber: {
decimal: 1,
action: { payload: 'x' }
},
invalidZeroToken: {
unit: 'GNT',
isEth: false,
setter: setTokenValue
}
};
gen.pass = cloneableGenerator(valueHandler)(action, setter);
gen.zeroPass = cloneableGenerator(valueHandler)(zeroAction, setter);
gen.invalidNumber = cloneableGenerator(valueHandler)(
failCases.invalidNumber.action as any,
setter
);
gen.invalidZeroToken = cloneableGenerator(valueHandler)(zeroAction, setTokenValue);
const value = toTokenBase(action.payload, decimal);
const zeroValue = toTokenBase(zeroAction.payload, decimal);
const unit = 'eth';
const isEth = true;
it('should select getDecimal', () => {
expect(gen.pass.next().value).toEqual(select(getDecimal));
expect(gen.zeroPass.next().value).toEqual(select(getDecimal));
expect(gen.invalidNumber.next().value).toEqual(select(getDecimal));
expect(gen.invalidZeroToken.next().value).toEqual(select(getDecimal));
});
it('should select getUnit', () => {
gen.invalidDecimal = gen.pass.clone();
expect(gen.pass.next(decimal).value).toEqual(select(getUnit));
expect(gen.zeroPass.next(decimal).value).toEqual(select(getUnit));
expect(gen.invalidNumber.next(decimal).value).toEqual(select(getUnit));
expect(gen.invalidDecimal.next(failCases.invalidDecimal).value).toEqual(select(getUnit));
expect(gen.invalidZeroToken.next(decimal).value).toEqual(select(getUnit));
});
it('should fail on invalid number or decimal and put null as a value', () => {
expect(gen.invalidNumber.next(unit).value).toEqual(
it('should select isEtherTransaction', () => {
expect(gen.pass.next(unit).value).toEqual(select(isEtherTransaction));
expect(gen.zeroPass.next(unit).value).toEqual(select(isEtherTransaction));
expect(gen.invalidNumber.next(unit).value).toEqual(select(isEtherTransaction));
expect(gen.invalidDecimal.next(unit).value).toEqual(select(isEtherTransaction));
expect(gen.invalidZeroToken.next(failCases.invalidZeroToken.unit).value).toEqual(
select(isEtherTransaction)
);
});
it('should fail on invalid number or decimal and put null as value', () => {
expect(gen.invalidNumber.next(isEth).value).toEqual(
put(setter({ raw: failCases.invalidNumber.action.payload, value: null }))
);
expect(gen.invalidDecimal.next(unit).value).toEqual(
expect(gen.invalidDecimal.next(isEth).value).toEqual(
put(setter({ raw: action.payload, value: null }))
);
});
it('should call isValid', () => {
expect(gen.pass.next(unit).value).toEqual(call(validateInput, value, unit));
it('should fail if token is given zero value and put null as value', () => {
expect(gen.invalidZeroToken.next(failCases.invalidZeroToken.isEth).value).toEqual(
put(failCases.invalidZeroToken.setter({ raw: zeroAction.payload, value: null }))
);
});
it('should call isValid', () => {
expect(gen.pass.next(isEth).value).toEqual(call(validateInput, value, unit));
expect(gen.zeroPass.next(isEth).value).toEqual(call(validateInput, zeroValue, unit));
});
it('should put setter', () => {
expect(gen.pass.next(true).value).toEqual(put(setter({ raw: action.payload, value })));
expect(gen.zeroPass.next(true).value).toEqual(
put(
setter({
raw: zeroAction.payload,
value: zeroValue
})
)
);
});
itShouldBeDone(gen.pass);
itShouldBeDone(gen.zeroPass);
});
describe('setCurrentValue*', () => {
@ -164,20 +206,25 @@ describe('revalidateCurrentValue*', () => {
});
describe('reparseCurrentValue*', () => {
const sharedLogic = (gen: SagaIterator) => {
const decimal = 5;
const sharedLogic = (gen: SagaIterator, isEth: boolean) => {
it('should select isEtherTransaction', () => {
expect(gen.next().value).toEqual(select(isEtherTransaction));
});
it('should select getDecimal', () => {
expect(gen.next().value).toEqual(select(getDecimal));
expect(gen.next(isEth).value).toEqual(select(getDecimal));
});
};
describe('when valid number and valid decimal', () => {
describe('when eth tx value is positive number and valid decimal', () => {
const value: any = {
raw: '100.0000'
};
const decimal = 5;
const gen = reparseCurrentValue(value);
sharedLogic(gen);
sharedLogic(gen, true);
it('should return correctly', () => {
expect(gen.next(decimal).value).toEqual({
@ -189,14 +236,46 @@ describe('reparseCurrentValue*', () => {
itShouldBeDone(gen);
});
describe('when invalid number', () => {
describe('when eth tx value is zero and decimal is valid', () => {
const value: any = {
raw: '0'
};
const gen = reparseCurrentValue(value);
sharedLogic(gen, true);
it('should return correctly', () => {
expect(gen.next(decimal).value).toEqual({
raw: value.raw,
value: toTokenBase(value.raw, decimal)
});
});
itShouldBeDone(gen);
});
describe('when eth tx value is invalid', () => {
const value: any = {
raw: 'invalidNumber'
};
const decimal = 5;
const gen = reparseCurrentValue(value);
sharedLogic(gen);
sharedLogic(gen, true);
it('should return null', () => {
expect(gen.next(decimal).value).toEqual(null);
});
itShouldBeDone(gen);
});
describe('when non-eth tx value is zero and valid decimal', () => {
const value: any = {
raw: '0'
};
const gen = reparseCurrentValue(value);
sharedLogic(gen, false);
it('should return null', () => {
expect(gen.next(decimal).value).toEqual(null);

View File

@ -24,8 +24,8 @@ describe('rebaseUserInput*', () => {
value: Wei('1')
};
const notValidNumberValue: any = {
raw: '0x0',
value: '0x0'
raw: '-1',
value: '-1'
};
const unit = 'unit';
const newDecimal = 1;