Repository: hummingbot/hummingbot
Pull request: Feat/gateway v2 route validators
Validate all request bodies before performing the route controller.

Problem statement:
Add route request validators
Before running the control logic we should validate that the parameters are correct. While typescript does let you declare that expected types of the request, this doesn't actually prevent ill formed requests from entering the control logic, which might lead to unexpected errors. The validators should check the params then if they are correct, it runs the controller, if they are incorrect then it returns an HTTP error specifying what went wrong.

Hints from issue discussion:

Commit message: Refactor the validation code

Patch to review:
diff --git a/gateway/src/chains/ethereum/ethereum.requests.ts b/gateway/src/chains/ethereum/ethereum.requests.ts
new file mode 100644
index 00000000000..cb57a52eb48
--- /dev/null
+++ b/gateway/src/chains/ethereum/ethereum.requests.ts
@@ -0,0 +1,26 @@
+export interface EthereumNonceRequest {
+  privateKey: string; // the users private Ethereum key
+}
+
+export interface EthereumAllowancesRequest {
+  privateKey: string; // the users private Ethereum key
+  spender: string; // the spender address for whom approvals are checked
+  tokenSymbols: string[]; // a list of token symbol
+}
+
+export interface EthereumBalanceRequest {
+  privateKey: string; // the users private Ethereum key
+  tokenSymbols: string[]; // a list of token symbol
+}
+
+export interface EthereumApproveRequest {
+  amount?: string; // the amount the spender will be approved to use
+  nonce?: number; // the privateKey's next nonce
+  privateKey: string; // the user's private Ethereum key
+  spender: string; // the address of the spend (or a pre-defined string like 'uniswap', 'balancer', etc.)
+  token: string; // the token symbol the spender will be approved for
+}
+
+export interface EthereumPollRequest {
+  txHash: string;
+}
diff --git a/gateway/src/chains/ethereum/ethereum.routes.ts b/gateway/src/chains/ethereum/ethereum.routes.ts
index 1f3dc1d5988..4ac5780c367 100644
--- a/gateway/src/chains/ethereum/ethereum.routes.ts
+++ b/gateway/src/chains/ethereum/ethereum.routes.ts
@@ -15,6 +15,20 @@ import {
   approve,
   poll,
 } from './ethereum.controllers';
+import {
+  EthereumNonceRequest,
+  EthereumAllowancesRequest,
+  EthereumBalanceRequest,
+  EthereumApproveRequest,
+  EthereumPollRequest,
+} from './ethereum.requests';
+import {
+  validateEthereumNonceRequest,
+  validateEthereumAllowancesRequest,
+  validateEthereumBalanceRequest,
+  validateEthereumApproveRequest,
+  validateEthereumPollRequest,
+} from './ethereum.validators';
 
 export namespace EthereumRoutes {
   export const router = Router();
@@ -44,10 +58,6 @@ export namespace EthereumRoutes {
     })
   );
 
-  interface EthereumNonceRequest {
-    privateKey: string; // the user's private Ethereum key
-  }
-
   interface EthereumNonceResponse {
     nonce: number; // the user's nonce
   }
@@ -59,6 +69,8 @@ export namespace EthereumRoutes {
         req: Request<{}, {}, EthereumNonceRequest>,
         res: Response<EthereumNonceResponse | string, {}>
       ) => {
+        validateEthereumNonceRequest(req.body);
+
         // get the address via the private key since we generally use the private
         // key to interact with gateway and the address is not part of the user config
         const wallet = ethereum.getWallet(req.body.privateKey);
@@ -68,12 +80,6 @@ export namespace EthereumRoutes {
     )
   );
 
-  interface EthereumAllowancesRequest {
-    privateKey: string; // the users private Ethereum key
-    spender: string; // the spender address for whom approvals are checked
-    tokenSymbols: string[]; // a list of token symbol
-  }
-
   interface EthereumAllowancesResponse {
     network: string;
     timestamp: number;
@@ -122,6 +128,8 @@ export namespace EthereumRoutes {
         req: Request<{}, {}, EthereumAllowancesRequest>,
         res: Response<EthereumAllowancesResponse | string, {}>
       ) => {
+        validateEthereumAllowancesRequest(req.body);
+
         const initTime = Date.now();
         const wallet = ethereum.getWallet(req.body.privateKey);
 
@@ -154,11 +162,6 @@ export namespace EthereumRoutes {
     )
   );
 
-  interface EthereumBalanceRequest {
-    privateKey: string; // the users private Ethereum key
-    tokenSymbols: string[]; // a list of token symbol
-  }
-
   interface EthereumBalanceResponse {
     network: string;
     timestamp: number;
@@ -174,6 +177,8 @@ export namespace EthereumRoutes {
         res: Response<EthereumBalanceResponse | string, {}>,
         _next: NextFunction
       ) => {
+        validateEthereumBalanceRequest(req.body);
+
         const initTime = Date.now();
 
         let wallet: Wallet;
@@ -212,14 +217,6 @@ export namespace EthereumRoutes {
     )
   );
 
-  interface EthereumApproveRequest {
-    amount?: string;
-    nonce?: number;
-    privateKey: string;
-    spender: string;
-    token: string;
-  }
-
   export interface EthereumApproveResponse {
     network: string;
     timestamp: number;
@@ -238,6 +235,8 @@ export namespace EthereumRoutes {
         req: Request<{}, {}, EthereumApproveRequest>,
         res: Response<EthereumApproveResponse | string, {}>
       ) => {
+        validateEthereumApproveRequest(req.body);
+
         const { nonce, privateKey, token, amount } = req.body;
         const spender = getSpender(req.body.spender);
         const result = await approve(spender, privateKey, token, amount, nonce);
@@ -246,10 +245,6 @@ export namespace EthereumRoutes {
     )
   );
 
-  interface EthereumPollRequest {
-    txHash: string;
-  }
-
   interface EthereumPollResponse {
     network: string;
     timestamp: number;
@@ -266,6 +261,8 @@ export namespace EthereumRoutes {
         req: Request<{}, {}, EthereumPollRequest>,
         res: Response<EthereumPollResponse, {}>
       ) => {
+        validateEthereumPollRequest(req.body);
+
         const result = await poll(req.body.txHash);
         res.status(200).json(result);
       }
diff --git a/gateway/src/chains/ethereum/ethereum.validators.ts b/gateway/src/chains/ethereum/ethereum.validators.ts
new file mode 100644
index 00000000000..843cb6a7340
--- /dev/null
+++ b/gateway/src/chains/ethereum/ethereum.validators.ts
@@ -0,0 +1,149 @@
+import {
+  isNaturalNumberString,
+  missingParameter,
+  throwErrorsIfExist,
+  validate,
+} from '../../services/validators';
+
+import {
+  EthereumNonceRequest,
+  EthereumAllowancesRequest,
+  EthereumBalanceRequest,
+  EthereumApproveRequest,
+  EthereumPollRequest,
+} from './ethereum.requests';
+
+// validate request parameters
+
+// test if a string matches the shape of an Ethereum public key
+export const isPublicKey = (str: string): boolean => {
+  return /^0x[a-fA-F0-9]{40}$/.test(str);
+};
+
+// test if a string matches the shape of an Ethereum private key
+export const isPrivateKey = (str: string): boolean => {
+  return /^[a-fA-F0-9]{64}$/.test(str);
+};
+
+// invalid parameter errors
+
+export const invalidPrivateKeyError: string =
+  'The privateKey param is not a valid Ethereum private key (64 hexidecimal characters).';
+
+export const invalidSpenderError: string =
+  'The spender param is not a valid Ethereum public key (0x followed by 40 hexidecimal characters).';
+
+export const invalidTokenSymbolsError: string =
+  'The tokenSymbols param should be an array of strings.';
+
+export const invalidTokenError: string = 'The token param should be a string.';
+
+export const invalidAmountError: string =
+  'If amount is included it must be a string of a non-negative integer.';
+
+export const invalidNonceError: string =
+  'If nonce is included it must be a non-negative integer.';
+
+export const invalidTxHashError: string = 'The txHash param must be a string.';
+
+// given a request, look for a key called privateKey that is an Ethereum private key
+export const validatePrivateKey = validate(
+  'privateKey',
+  invalidPrivateKeyError,
+  (val) => typeof val === 'string' && isPrivateKey(val)
+);
+
+// given a request, look for a key called spender that is 'uniswap' or an Ethereum public key
+export const validateSpender = validate(
+  'spender',
+  invalidSpenderError,
+  (val) => typeof val === 'string' && (val === 'uniswap' || isPublicKey(val))
+);
+
+// confirm that tokenSymbols is an array of strings
+export const validateTokenSymbols = (req: any): Array<string> => {
+  let errors: Array<string> = [];
+  if (req.tokenSymbols) {
+    if (Array.isArray(req.tokenSymbols)) {
+      req.tokenSymbols.forEach((symbol: any) => {
+        if (typeof symbol === 'string') {
+        } else {
+          errors.push(invalidTokenSymbolsError);
+        }
+      });
+    } else {
+      errors.push(invalidTokenSymbolsError);
+    }
+  } else {
+    errors.push(missingParameter('tokenSymbols'));
+  }
+  return errors;
+};
+
+// confirm that token is a string
+export const validateToken = validate(
+  'token',
+  invalidTokenError,
+  (val) => typeof val === 'string'
+);
+
+// if amount exists, confirm that it is a string of a natural number
+export const validateAmount = validate(
+  'amount',
+  invalidAmountError,
+  (val) => typeof val === 'string' && isNaturalNumberString(val),
+  true
+);
+
+export const validateNonce = validate(
+  'nonce',
+  invalidNonceError,
+  (val) => typeof val === 'number' && val > -1,
+  true
+);
+
+export const validateTxHash = validate(
+  'txHash',
+  invalidTxHashError,
+  (val) => typeof val === 'string'
+);
+
+// request types and corresponding validators
+
+export const validateEthereumNonceRequest = (
+  req: EthereumNonceRequest
+): void => {
+  throwErrorsIfExist(validatePrivateKey(req));
+};
+
+export const validateEthereumAllowancesRequest = (
+  req: EthereumAllowancesRequest
+): void => {
+  const errors = validatePrivateKey(req)
+    .concat(validateSpender(req))
+    .concat(validateTokenSymbols(req));
+  throwErrorsIfExist(errors);
+};
+
+export const validateEthereumBalanceRequest = (
+  req: EthereumBalanceRequest
+): void => {
+  const errors = validatePrivateKey(req).concat(validateTokenSymbols(req));
+  throwErrorsIfExist(errors);
+};
+
+export const validateEthereumApproveRequest = (
+  req: EthereumApproveRequest
+): void => {
+  const errors = validatePrivateKey(req).concat(
+    validateSpender(req),
+    validateToken(req),
+    validateAmount(req),
+    validateNonce(req)
+  );
+  throwErrorsIfExist(errors);
+};
+
+export const validateEthereumPollRequest = (req: EthereumPollRequest): void => {
+  throwErrorsIfExist(validateTxHash(req));
+};
diff --git a/gateway/src/chains/ethereum/uniswap/uniswap.requests.ts b/gateway/src/chains/ethereum/uniswap/uniswap.requests.ts
new file mode 100644
index 00000000000..6a00842ea07
--- /dev/null
+++ b/gateway/src/chains/ethereum/uniswap/uniswap.requests.ts
@@ -0,0 +1,18 @@
+export type Side = 'BUY' | 'SELL';
+
+export interface UniswapPriceRequest {
+  quote: string;
+  base: string;
+  amount: string;
+  side: Side;
+}
+
+export interface UniswapTradeRequest {
+  quote: string;
+  base: string;
+  amount: string;
+  privateKey: string;
+  side: Side;
+  limitPrice?: string; // integer as string
+  nonce?: number;
+}
diff --git a/gateway/src/chains/ethereum/uniswap/uniswap.routes.ts b/gateway/src/chains/ethereum/uniswap/uniswap.routes.ts
index a643325836e..a29766c1cef 100644
--- a/gateway/src/chains/ethereum/uniswap/uniswap.routes.ts
+++ b/gateway/src/chains/ethereum/uniswap/uniswap.routes.ts
@@ -12,6 +12,11 @@ import {
 import { Trade } from '@uniswap/sdk';
 import { verifyEthereumIsAvailable } from '../ethereum-middlewares';
 import { verifyUniswapIsAvailable } from './uniswap-middlewares';
+import { UniswapPriceRequest, UniswapTradeRequest } from './uniswap.requests';
+import {
+  validateUniswapPriceRequest,
+  validateUniswapTradeRequest,
+} from './uniswap.validators';
 
 export namespace UniswapRoutes {
   export const router = Router();
@@ -32,14 +37,6 @@ export namespace UniswapRoutes {
     });
   });
 
-  type Side = 'BUY' | 'SELL';
-  interface UniswapPriceRequest {
-    quote: string;
-    base: string;
-    amount: string;
-    side: Side;
-  }
-
   interface UniswapPriceResponse {
     network: string;
     timestamp: number;
@@ -62,6 +59,7 @@ export namespace UniswapRoutes {
         req: Request<{}, {}, UniswapPriceRequest>,
         res: Response<UniswapPriceResponse, {}>
       ) => {
+        validateUniswapPriceRequest(req.body);
         const initTime = Date.now();
 
         // the amount is passed in as a string. We must validate the value.
@@ -157,16 +155,6 @@ export namespace UniswapRoutes {
     )
   );
 
-  interface UniswapTradeRequest {
-    quote: string;
-    base: string;
-    amount: string;
-    privateKey: string;
-    side: Side;
-    limitPrice?: BigNumber;
-    nonce?: number;
-  }
-
   interface UniswapTradeResponse {
     network: string;
     timestamp: number;
@@ -196,6 +184,7 @@ export namespace UniswapRoutes {
         req: Request<{}, {}, UniswapTradeRequest>,
         res: Response<UniswapTradeResponse | UniswapTradeErrorResponse, {}>
       ) => {
+        validateUniswapTradeRequest(req.body);
         const initTime = Date.now();
 
         const limitPrice = req.body.limitPrice;
diff --git a/gateway/src/chains/ethereum/uniswap/uniswap.validators.ts b/gateway/src/chains/ethereum/uniswap/uniswap.validators.ts
new file mode 100644
index 00000000000..3dace0c1bc0
--- /dev/null
+++ b/gateway/src/chains/ethereum/uniswap/uniswap.validators.ts
@@ -0,0 +1,75 @@
+import {
+  isNaturalNumberString,
+  isFloatString,
+  throwErrorsIfExist,
+  validate,
+} from '../../../services/validators';
+
+import { validateNonce, validatePrivateKey } from '../ethereum.validators';
+
+import { UniswapPriceRequest, UniswapTradeRequest } from './uniswap.requests';
+
+export const invalidQuoteError: string = 'The quote param is not a string.';
+
+export const invalidBaseError: string = 'The base param is not a string.';
+
+export const invalidAmountError: string =
+  'The amount param must be a string of a non-negative integer.';
+
+export const invalidSideError: string =
+  'The side param must be a string of "buy" or "sell".';
+
+export const invalidLimitPriceError: string =
+  'The limitPrice param may be null or a string of a float or integer number.';
+
+export const validateQuote = validate(
+  'quote',
+  invalidQuoteError,
+  (val) => typeof val === 'string'
+);
+
+export const validateBase = validate(
+  'base',
+  invalidBaseError,
+  (val) => typeof val === 'string'
+);
+
+export const validateAmount = validate(
+  'amount',
+  invalidAmountError,
+  (val) => typeof val === 'string' && isNaturalNumberString(val)
+);
+
+export const validateSide = validate(
+  'side',
+  invalidSideError,
+  (val) => typeof val === 'string' && (val === 'BUY' || val === 'SELL')
+);
+
+export const validateLimitPrice = validate(
+  'limitPrice',
+  invalidLimitPriceError,
+  (val) => typeof val === 'string' && isFloatString(val),
+  true
+);
+
+export const validateUniswapPriceRequest = (req: UniswapPriceRequest): void => {
+  const errors = validateQuote(req).concat(
+    validateBase(req),
+    validateAmount(req),
+    validateSide(req)
+  );
+  throwErrorsIfExist(errors);
+};
+
+export const validateUniswapTradeRequest = (req: UniswapTradeRequest): void => {
+  const errors = validateQuote(req).concat(
+    validateBase(req),
+    validateAmount(req),
+    validatePrivateKey(req),
+    validateSide(req),
+    validateLimitPrice(req),
+    validateNonce(req)
+  );
+  throwErrorsIfExist(errors);
+};
diff --git a/gateway/src/services/validators.ts b/gateway/src/services/validators.ts
new file mode 100644
index 00000000000..7e2ef75b66a
--- /dev/null
+++ b/gateway/src/services/validators.ts
@@ -0,0 +1,57 @@
+import { HttpException } from './error-handler';
+
+export const isNaturalNumberString = (str: string): boolean => {
+  return /^[0-9]+$/.test(str);
+};
+
+export const isIntegerString = (str: string): boolean => {
+  return /^[+-]?[0-9]+$/.test(str);
+};
+
+export const isFloatString = (str: string): boolean => {
+  if (isIntegerString(str)) {
+    return true;
+  }
+  const decimalSplit = str.split('.');
+  if (decimalSplit.length === 2) {
+    return (
+      isIntegerString(decimalSplit[0]) && isNaturalNumberString(decimalSplit[1])
+    );
+  }
+  return false;
+};
+
+// throw an error because the request parameter is malformed, collect all the
+// errors related to the request to give the most information possible
+export const throwErrorsIfExist = (errors: Array<string>): void => {
+  if (errors.length > 0) {
+    throw new HttpException(404, errors.join(', '));
+  }
+};
+
+export const missingParameter = (key: string): string => {
+  return `The request is missing the private key: ${key}`;
+};
+
+export const validate = (
+  key: string,
+  errorMsg: string,
+  condition: (x: any) => boolean,
+  optional: boolean = false
+): ((req: any) => Array<string>) => {
+  return (req: any) => {
+    let errors: Array<string> = [];
+    if (req[key]) {
+      if (condition(req[key])) {
+      } else {
+        errors.push(errorMsg);
+      }
+    } else {
+      if (!optional) {
+        errors.push(missingParameter(key));
+      }
+    }
+
+    return errors;
+  };
+};
diff --git a/gateway/test/chains/ethereum/ethereum.routes.test.ts b/gateway/test/chains/ethereum/ethereum.routes.test.ts
index 624ff6b99a6..7baff29e246 100644
--- a/gateway/test/chains/ethereum/ethereum.routes.test.ts
+++ b/gateway/test/chains/ethereum/ethereum.routes.test.ts
@@ -24,26 +24,38 @@ describe('GET /eth', () => {
 describe('POST /eth/nonce', () => {
   it('should return 200', async () => {
     EthereumRoutes.ethereum.getWallet = jest.fn().mockReturnValue({
-      address: '0x0000000000000000000',
+      address: '0xFaA12FD102FE8623C9299c72B03E45107F2772B5',
     });
     EthereumRoutes.ethereum.nonceManager.getNonce = jest
       .fn()
       .mockReturnValue(2);
     await request(app)
       .post(`/eth/nonce`)
-      .send({ privateKey: 'abc123' })
+      .send({
+        privateKey:
+          'da857cbda0ba96757fed842617a40693d06d00001e55aa972955039ae747bac4',
+      })
       .set('Accept', 'application/json')
       .expect('Content-Type', /json/)
       .expect(200)
       .expect((res) => expect(res.body.nonce).toBe(2));
   });
+
+  it('should return 404 when parameters are invalid', async () => {
+    await request(app)
+      .post(`/eth/nonce`)
+      .send({
+        privateKey: 'da857cbda0ba96757fed842617a4',
+      })
+      .expect(404);
+  });
 });
 
 describe('POST /eth/approve', () => {
   it('should return 200', async () => {
     // override getWallet (network call)
     EthereumRoutes.ethereum.getWallet = jest.fn().mockReturnValue({
-      address: '0x0000000000000000000',
+      address: '0xFaA12FD102FE8623C9299c72B03E45107F2772B5',
     });
 
     // override getTokenBySymbol (network call, read file and config dependency)
@@ -73,8 +85,9 @@ describe('POST /eth/approve', () => {
     await request(app)
       .post(`/eth/approve`)
       .send({
-        privateKey: 'abc123',
-        spender: 'xyz098',
+        privateKey:
+          'da857cbda0ba96757fed842617a40693d06d00001e55aa972955039ae747bac4',
+        spender: 'uniswap',
         token: 'WETH',
         nonce: 23,
       })
@@ -85,4 +98,17 @@ describe('POST /eth/approve', () => {
         expect(res.body.nonce).toEqual(23);
       });
   });
+
+  it('should return 404 when parameters are invalid', async () => {
+    await request(app)
+      .post(`/eth/approve`)
+      .send({
+        privateKey:
+          'da857cbda0ba96757fed842617a40693d06d00001e55aa972955039ae747bac4',
+        spender: 'uniswap',
+        token: 123,
+        nonce: '23',
+      })
+      .expect(404);
+  });
 });
diff --git a/gateway/test/chains/ethereum/ethereum.validators.test.ts b/gateway/test/chains/ethereum/ethereum.validators.test.ts
new file mode 100644
index 00000000000..9ab5172b234
--- /dev/null
+++ b/gateway/test/chains/ethereum/ethereum.validators.test.ts
@@ -0,0 +1,276 @@
+import {
+  isPrivateKey,
+  isPublicKey,
+  validatePrivateKey,
+  invalidPrivateKeyError,
+  validateSpender,
+  invalidSpenderError,
+  validateTokenSymbols,
+  invalidTokenSymbolsError,
+  validateToken,
+  invalidTokenError,
+  validateAmount,
+  invalidAmountError,
+  validateNonce,
+  invalidNonceError,
+  validateTxHash,
+  invalidTxHashError,
+} from '../../../src/chains/ethereum/ethereum.validators';
+
+import { missingParameter } from '../../../src/services/validators';
+
+import 'jest-extended';
+
+describe('isPublicKey', () => {
+  it('pass against a well formed public key', () => {
+    expect(isPublicKey('0xFaA12FD102FE8623C9299c72B03E45107F2772B5')).toEqual(
+      true
+    );
+  });
+
+  it('fail against a string that is too short', () => {
+    expect(isPublicKey('0xFaA12FD102FE8623C9299c72')).toEqual(false);
+  });
+
+  it('fail against a string that has non-hexadecimal characters', () => {
+    expect(isPublicKey('0xFaA12FD102FE8623C9299c7iwqpneciqwopienff')).toEqual(
+      false
+    );
+  });
+
+  it('fail against a valid public key that is missing the initial 0x', () => {
+    expect(isPublicKey('FaA12FD102FE8623C9299c72B03E45107F2772B5')).toEqual(
+      false
+    );
+  });
+});
+
+describe('isPrivateKey', () => {
+  it('pass against a well formed public key', () => {
+    expect(
+      isPrivateKey(
+        'da857cbda0ba96757fed842617a40693d06d00001e55aa972955039ae747bac4'
+      )
+    ).toEqual(true);
+  });
+
+  it('fail against a string that is too short', () => {
+    expect(isPrivateKey('da857cbda0ba96757fed842617a40693d0')).toEqual(false);
+  });
+
+  it('fail against a string that has non-hexadecimal characters', () => {
+    expect(
+      isPrivateKey(
+        'da857cbda0ba96757fed842617a40693d06d00001e55aa972955039ae747qwer'
+      )
+    ).toEqual(false);
+  });
+});
+
+describe('validatePrivateKey', () => {
+  it('valid when req.privateKey is a privateKey', () => {
+    expect(
+      validatePrivateKey({
+        privateKey:
+          'da857cbda0ba96757fed842617a40693d06d00001e55aa972955039ae747bac4',
+      })
+    ).toEqual([]);
+  });
+
+  it('return error when req.privateKey does not exist', () => {
+    expect(
+      validatePrivateKey({
+        hello: 'world',
+      })
+    ).toEqual([missingParameter('privateKey')]);
+  });
+
+  it('return error when req.privateKey is invalid', () => {
+    expect(
+      validatePrivateKey({
+        privateKey: 'world',
+      })
+    ).toEqual([invalidPrivateKeyError]);
+  });
+});
+
+describe('validateSpender', () => {
+  it('valid when req.spender is a publicKey', () => {
+    expect(
+      validateSpender({
+        spender: '0xFaA12FD102FE8623C9299c72B03E45107F2772B5',
+      })
+    ).toEqual([]);
+  });
+
+  it("valid when req.spender is a 'uniswap'", () => {
+    expect(
+      validateSpender({
+        spender: 'uniswap',
+      })
+    ).toEqual([]);
+  });
+
+  it('return error when req.spender does not exist', () => {
+    expect(
+      validateSpender({
+        hello: 'world',
+      })
+    ).toEqual([missingParameter('spender')]);
+  });
+
+  it('return error when req.spender is invalid', () => {
+    expect(
+      validateSpender({
+        spender: 'world',
+      })
+    ).toEqual([invalidSpenderError]);
+  });
+});
+
+describe('validateTokenSymbols', () => {
+  it('valid when req.tokenSymbols is an array of strings', () => {
+    expect(
+      validateTokenSymbols({
+        tokenSymbols: ['WETH', 'DAI'],
+      })
+    ).toEqual([]);
+  });
+
+  it('return error when req.tokenSymbols does not exist', () => {
+    expect(
+      validateTokenSymbols({
+        hello: 'world',
+      })
+    ).toEqual([missingParameter('tokenSymbols')]);
+  });
+
+  it('return error when req.tokenSymbols is invalid', () => {
+    expect(
+      validateTokenSymbols({
+        tokenSymbols: 'WETH',
+      })
+    ).toEqual([invalidTokenSymbolsError]);
+  });
+});
+
+describe('validateToken', () => {
+  it('valid when req.token is a string', () => {
+    expect(
+      validateToken({
+        token: 'DAI',
+      })
+    ).toEqual([]);
+
+    expect(
+      validateToken({
+        token: 'WETH',
+      })
+    ).toEqual([]);
+  });
+
+  it('return error when req.token does not exist', () => {
+    expect(
+      validateToken({
+        hello: 'world',
+      })
+    ).toEqual([missingParameter('token')]);
+  });
+
+  it('return error when req.token is invalid', () => {
+    expect(
+      validateToken({
+        token: 123,
+      })
+    ).toEqual([invalidTokenError]);
+  });
+});
+
+describe('validateAmount', () => {
+  it('valid when req.amount is a string of an integer', () => {
+    expect(
+      validateAmount({
+        amount: '0',
+      })
+    ).toEqual([]);
+    expect(
+      validateAmount({
+        amount: '9999999999999999999999',
+      })
+    ).toEqual([]);
+  });
+
+  it('valid when req.amount does not exist', () => {
+    expect(
+      validateAmount({
+        hello: 'world',
+      })
+    ).toEqual([]);
+  });
+
+  it('return error when req.amount is invalid', () => {
+    expect(
+      validateAmount({
+        amount: 'WETH',
+      })
+    ).toEqual([invalidAmountError]);
+  });
+});
+
+describe('validateNonce', () => {
+  it('valid when req.nonce is a number', () => {
+    expect(
+      validateNonce({
+        nonce: 0,
+      })
+    ).toEqual([]);
+    expect(
+      validateNonce({
+        nonce: 999,
+      })
+    ).toEqual([]);
+  });
+
+  it('valid when req.nonce does not exist', () => {
+    expect(
+      validateNonce({
+        hello: 'world',
+      })
+    ).toEqual([]);
+  });
+
+  it('return error when req.nonce is invalid', () => {
+    expect(
+      validateNonce({
+        nonce: '123',
+      })
+    ).toEqual([invalidNonceError]);
+  });
+});
+
+describe('validateTxHash', () => {
+  it('valid when req.txHash is a string', () => {
+    expect(
+      validateTxHash({
+        txHash:
+          '0x6d068067a5e5a0f08c6395b31938893d1cdad81f54a54456221ecd8c1941294d',
+      })
+    ).toEqual([]);
+  });
+
+  it('invalid when req.txHash does not exist', () => {
+    expect(
+      validateTxHash({
+        hello: 'world',
+      })
+    ).toEqual([missingParameter('txHash')]);
+  });
+
+  it('return error when req.txHash is invalid', () => {
+    expect(
+      validateTxHash({
+        txHash: 123,
+      })
+    ).toEqual([invalidTxHashError]);
+  });
+});
diff --git a/gateway/test/chains/ethereum/uniswap/uniswap.routes.test.ts b/gateway/test/chains/ethereum/uniswap/uniswap.routes.test.ts
index 7fbed57bfd9..9e23374088b 100644
--- a/gateway/test/chains/ethereum/uniswap/uniswap.routes.test.ts
+++ b/gateway/test/chains/ethereum/uniswap/uniswap.routes.test.ts
@@ -102,7 +102,8 @@ describe('POST /eth/uniswap/trade', () => {
         quote: 'DAI',
         base: 'WETH',
         amount: '10000',
-        privateKey: 'abc123',
+        privateKey:
+          'da857cbda0ba96757fed842617a40693d06d00001e55aa972955039ae747bac4',
         side: 'BUY',
         nonce: 21,
       })
@@ -112,4 +113,18 @@ describe('POST /eth/uniswap/trade', () => {
         expect(res.body.nonce).toEqual(21);
       });
   });
+
+  it('should return 404 when parameters are incorrect', async () => {
+    await request(app)
+      .post(`/eth/uniswap/trade`)
+      .send({
+        quote: 'DAI',
+        base: 'WETH',
+        amount: 10000,
+        privateKey: 'da8',
+        side: 'comprar',
+      })
+      .set('Accept', 'application/json')
+      .expect(404);
+  });
 });
diff --git a/gateway/test/chains/ethereum/uniswap/uniswap.validators.test.ts b/gateway/test/chains/ethereum/uniswap/uniswap.validators.test.ts
new file mode 100644
index 00000000000..5c7b09ec61d
--- /dev/null
+++ b/gateway/test/chains/ethereum/uniswap/uniswap.validators.test.ts
@@ -0,0 +1,142 @@
+import {
+  invalidQuoteError,
+  validateQuote,
+  invalidBaseError,
+  validateBase,
+  invalidSideError,
+  validateSide,
+  invalidLimitPriceError,
+  validateLimitPrice,
+} from '../../../../src/chains/ethereum/uniswap/uniswap.validators';
+
+import { missingParameter } from '../../../../src/services/validators';
+
+import 'jest-extended';
+
+describe('validateQuote', () => {
+  it('valid when req.quote is a string', () => {
+    expect(
+      validateQuote({
+        quote: 'DAI',
+      })
+    ).toEqual([]);
+
+    expect(
+      validateQuote({
+        quote: 'WETH',
+      })
+    ).toEqual([]);
+  });
+
+  it('return error when req.quote does not exist', () => {
+    expect(
+      validateQuote({
+        hello: 'world',
+      })
+    ).toEqual([missingParameter('quote')]);
+  });
+
+  it('return error when req.quote is invalid', () => {
+    expect(
+      validateQuote({
+        quote: 123,
+      })
+    ).toEqual([invalidQuoteError]);
+  });
+});
+
+describe('validateBase', () => {
+  it('valid when req.base is a string', () => {
+    expect(
+      validateBase({
+        base: 'DAI',
+      })
+    ).toEqual([]);
+
+    expect(
+      validateBase({
+        base: 'WETH',
+      })
+    ).toEqual([]);
+  });
+
+  it('return error when req.base does not exist', () => {
+    expect(
+      validateBase({
+        hello: 'world',
+      })
+    ).toEqual([missingParameter('base')]);
+  });
+
+  it('return error when req.base is invalid', () => {
+    expect(
+      validateBase({
+        base: 123,
+      })
+    ).toEqual([invalidBaseError]);
+  });
+});
+
+describe('validateSide', () => {
+  it('valid when req.side is a string', () => {
+    expect(
+      validateSide({
+        side: 'BUY',
+      })
+    ).toEqual([]);
+
+    expect(
+      validateSide({
+        side: 'SELL',
+      })
+    ).toEqual([]);
+  });
+
+  it('return error when req.side does not exist', () => {
+    expect(
+      validateSide({
+        hello: 'world',
+      })
+    ).toEqual([missingParameter('side')]);
+  });
+
+  it('return error when req.side is invalid', () => {
+    expect(
+      validateSide({
+        side: 'comprar',
+      })
+    ).toEqual([invalidSideError]);
+  });
+});
+
+describe('validateLimitPrice', () => {
+  it('valid when req.limitPrice is a string', () => {
+    expect(
+      validateLimitPrice({
+        limitPrice: '12000.123',
+      })
+    ).toEqual([]);
+
+    expect(
+      validateLimitPrice({
+        limitPrice: '89425894',
+      })
+    ).toEqual([]);
+  });
+
+  it('pass when req.limitPrice does not exist', () => {
+    expect(
+      validateLimitPrice({
+        hello: 'world',
+      })
+    ).toEqual([]);
+  });
+
+  it('return error when req.limitPrice is invalid', () => {
+    expect(
+      validateLimitPrice({
+        limitPrice: 'comprar',
+      })
+    ).toEqual([invalidLimitPriceError]);
+  });
+});
diff --git a/gateway/test/services/validators.test.ts b/gateway/test/services/validators.test.ts
new file mode 100644
index 00000000000..45b828ee1bd
--- /dev/null
+++ b/gateway/test/services/validators.test.ts
@@ -0,0 +1,61 @@
+import {
+  isNaturalNumberString,
+  isIntegerString,
+  isFloatString,
+} from '../../src/services/validators';
+
+import 'jest-extended';
+
+describe('isNaturalNumberString', () => {
+  it('pass against a well formed natural number in a string', () => {
+    expect(isNaturalNumberString('12345')).toEqual(true);
+  });
+
+  it('fail against a negative number in a string', () => {
+    expect(isNaturalNumberString('-12')).toEqual(false);
+  });
+
+  it('fail against a non number string', () => {
+    expect(isNaturalNumberString('Hello world')).toEqual(false);
+  });
+});
+
+describe('isIntegerString', () => {
+  it('pass against a positive number in a string', () => {
+    expect(isIntegerString('12345')).toEqual(true);
+  });
+
+  it('pass against a negative number in a string', () => {
+    expect(isIntegerString('-12')).toEqual(true);
+  });
+
+  it('fail against a non number string', () => {
+    expect(isIntegerString('Hello world')).toEqual(false);
+  });
+});
+
+describe('isFloatString', () => {
+  it('pass against a positive number in a string', () => {
+    expect(isFloatString('12345')).toEqual(true);
+
+    expect(isFloatString('12.345')).toEqual(true);
+
+    expect(isFloatString('0.45')).toEqual(true);
+
+    expect(isFloatString('0')).toEqual(true);
+
+    expect(isFloatString('0.00001')).toEqual(true);
+  });
+
+  it('pass against a negative number in a string', () => {
+    expect(isFloatString('-12')).toEqual(true);
+
+    expect(isFloatString('-12.3123')).toEqual(true);
+
+    expect(isFloatString('-0.123')).toEqual(true);
+  });
+
+  it('fail against a non number string', () => {
+    expect(isFloatString('Hello world')).toEqual(false);
+  });
+});


Code review comments:
diff_hunk: @@ -0,0 +1,149 @@
+import {
+  isNaturalNumberString,
+  missingParameter,
+  throwErrorsIfExist,
+  validate,
+} from '../../services/validators';
+
+import {
+  EthereumNonceRequest,
+  EthereumAllowancesRequest,
+  EthereumBalanceRequest,
+  EthereumApproveRequest,
+  EthereumPollRequest,
+} from './ethereum.requests';
+
+// validate request parameters
+
+// test if a string matches the shape of an Ethereum public key
+export const isPublicKey = (str: string): boolean => {
+  return /^0x[a-fA-F0-9]{40}$/.test(str);
+};
+
+// test if a string matches the shape of an Ethereum private key
+export const isPrivateKey = (str: string): boolean => {
+  return /^[a-fA-F0-9]{64}$/.test(str);
+};
+
+// invalid parameter errors
+
+export const invalidPrivateKeyError: string =
+  'The privateKey param is not a valid Ethereum private key (64 hexidecimal characters).';
+
+export const invalidSpenderError: string =
+  'The spender param is not a valid Ethereum public key (0x followed by 40 hexidecimal characters).';
+
+export const invalidTokenSymbolsError: string =
+  'The tokenSymbols param should be an array of strings.';
+
+export const invalidTokenError: string = 'The token param should be a string.';
+
+export const invalidAmountError: string =
+  'If amount is included it must be a string of a non-negative integer.';
+
+export const invalidNonceError: string =
+  'If nonce is included it must be a non-negative integer.';
+
+export const invalidTxHashError: string = 'The txHash param must be a string.';
+
+// given a request, look for a key called privateKey that is an Ethereum private key
+export const validatePrivateKey = validate(
+  'privateKey',
+  invalidPrivateKeyError,
+  (val) => typeof val === 'string' && isPrivateKey(val)
+);
+
+// given a request, look for a key called spender that is 'uniswap' or an Ethereum public key
+export const validateSpender = validate(
+  'spender',
+  invalidSpenderError,
+  (val) => typeof val === 'string' && (val === 'uniswap' || isPublicKey(val))
+);
+
+// confirm that tokenSymbols is an array of strings
+export const validateTokenSymbols = (req: any): Array<string> => {
+  let errors: Array<string> = [];
+  if (req.tokenSymbols) {
+    if (Array.isArray(req.tokenSymbols)) {
+      req.tokenSymbols.forEach((symbol: any) => {
+        if (typeof symbol === 'string') {
+        } else {
+          errors.push(invalidTokenSymbolsError);
+        }
+      });
+    } else {
+      errors.push(invalidTokenSymbolsError);
+    }
+  } else {
+    errors.push(missingParameter('tokenSymbols'));
+  }
+  return errors;
+};
+
+// confirm that token is a string
+export const validateToken = validate(
+  'token',
+  invalidTokenError,
+  (val) => typeof val === 'string'
+);
+
+// if amount exists, confirm that it is a string of a natural number
+export const validateAmount = validate(
+  'amount',
+  invalidAmountError,
+  (val) => typeof val === 'string' && isNaturalNumberString(val),
+  true
+);
+
+export const validateNonce = validate(
+  'nonce',
+  invalidNonceError,
+  (val) => typeof val === 'number' && val > -1,
+  true
+);
+
+export const validateTxHash = validate(
+  'txHash',
+  invalidTxHashError,
+  (val) => typeof val === 'string'
+);
+
+// request types and corresponding validators
+
+export const validateEthereumNonceRequest = (
+  req: EthereumNonceRequest
+): void => {
+  throwErrorsIfExist(validatePrivateKey(req));
+};
+
+export const validateEthereumAllowancesRequest = (
+  req: EthereumAllowancesRequest
+): void => {
+  const errors = validatePrivateKey(req)
+    .concat(validateSpender(req))
+    .concat(validateTokenSymbols(req));
+  throwErrorsIfExist(errors);
+};
+
+export const validateEthereumBalanceRequest = (
path: gateway/src/chains/ethereum/ethereum.validators.ts
text: @user1:
There are really two types of validator functions here - the first type are the validator "components" that output a list of error strings. The other type are the request validators which raises an error if any of the components output an error.

We should declare the function types of these two validator types, for better documentation. e.g.

```
type Validator = (req: any) => Array<string>;
type RequestValidator = (req: any) => void;
```

Also, a lot of the request validator functions are really the same - as in they're concatenating the results from multiple validators. It looks like we can just turn that into a factory method for the common case.

@author:
Thanks, those are great suggestions. I've added them and renamed the `validate` function to `mkValidator` since that makes it clear what it is doing. It creates a validating function, it doesn't validate by itself. Then i did the same for `RequestValidator` and made `mkRequestValidator`.