Repository: hummingbot/hummingbot
Pull request: Update hummingbot integration to gateway-v2 and use nonces in hummingbot to gateway-v2 transactions

Problem statement:
Use nonces on Hummingbot for creating Ethereum transactions
## Why

After the nonce manager has been implemented and integrated to gateway v2, we need Hummingbot to provide the nonce on creating new transactions to allow the two-token approval workflow to work correctly.

## What

* Add logic on Hummingbot side to query and provide the `nonce` parameter, when creating new transactions.
* Add test cases to test for the two-token approval workflow.

Hints from issue discussion:

Commit message: Remove unnecessary logs

Patch to review:
diff --git a/gateway/src/app.ts b/gateway/src/app.ts
index 2c2f744c038..ae65932b4b8 100644
--- a/gateway/src/app.ts
+++ b/gateway/src/app.ts
@@ -23,7 +23,7 @@ app.use('/eth/uniswap', UniswapRoutes.router);
 
 // a simple route to test that the server is running
 app.get('/', (_req: Request, res: Response) => {
-  res.status(200).json({ message: 'ok' });
+  res.status(200).json({ status: 'ok' });
 });
 
 app.get(
diff --git a/gateway/src/chains/ethereum/ethereum.controllers.ts b/gateway/src/chains/ethereum/ethereum.controllers.ts
index e4a8ce5b377..c10a2097f6e 100644
--- a/gateway/src/chains/ethereum/ethereum.controllers.ts
+++ b/gateway/src/chains/ethereum/ethereum.controllers.ts
@@ -157,6 +157,7 @@ export async function balances(
 export async function approve(
   req: EthereumApproveRequest
 ): Promise<EthereumApproveResponse> {
+  console.log(req);
   validateEthereumApproveRequest(req);
   const { amount, nonce, privateKey, token } = req;
   const spender = getSpender(req.spender);
diff --git a/gateway/src/chains/ethereum/ethereum.validators.ts b/gateway/src/chains/ethereum/ethereum.validators.ts
index e5bdd9b650f..d11eea41818 100644
--- a/gateway/src/chains/ethereum/ethereum.validators.ts
+++ b/gateway/src/chains/ethereum/ethereum.validators.ts
@@ -35,7 +35,7 @@ export const isPublicKey = (str: string): boolean => {
 
 // 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);
+  return /^(0x)?[a-fA-F0-9]{64}$/.test(str);
 };
 
 // given a request, look for a key called privateKey that is an Ethereum private key
@@ -89,7 +89,7 @@ export const validateAmount: Validator = mkValidator(
 export const validateNonce: Validator = mkValidator(
   'nonce',
   invalidNonceError,
-  (val) => typeof val === 'number' && val > -1,
+  (val) => typeof val === 'number' && val >= 0,
   true
 );
 
diff --git a/gateway/src/chains/ethereum/uniswap/uniswap.validators.ts b/gateway/src/chains/ethereum/uniswap/uniswap.validators.ts
index 3335980a391..9a2dd41b665 100644
--- a/gateway/src/chains/ethereum/uniswap/uniswap.validators.ts
+++ b/gateway/src/chains/ethereum/uniswap/uniswap.validators.ts
@@ -1,5 +1,4 @@
 import {
-  isNaturalNumberString,
   isFloatString,
   mkValidator,
   mkRequestValidator,
@@ -37,7 +36,7 @@ export const validateBase: Validator = mkValidator(
 export const validateAmount: Validator = mkValidator(
   'amount',
   invalidAmountError,
-  (val) => typeof val === 'string' && isNaturalNumberString(val)
+  (val) => typeof val === 'string' && isFloatString(val)
 );
 
 export const validateSide: Validator = mkValidator(
diff --git a/gateway/src/services/validators.ts b/gateway/src/services/validators.ts
index a1a021f9c8e..f5a1adb11b1 100644
--- a/gateway/src/services/validators.ts
+++ b/gateway/src/services/validators.ts
@@ -30,7 +30,7 @@ export const throwIfErrorsExist = (errors: Array<string>): void => {
 };
 
 export const missingParameter = (key: string): string => {
-  return `The request is missing the private key: ${key}`;
+  return `The request is missing the key: ${key}`;
 };
 
 export type Validator = (req: any) => Array<string>;
@@ -44,10 +44,9 @@ export const mkValidator = (
   optional: boolean = false
 ): Validator => {
   return (req: any) => {
-    let errors: Array<string> = [];
+    const errors: Array<string> = [];
     if (req[key]) {
-      if (condition(req[key])) {
-      } else {
+      if (!condition(req[key])) {
         errors.push(errorMsg);
       }
     } else {
diff --git a/hummingbot/connector/gateway_base.py b/hummingbot/connector/gateway_base.py
index dab6fff23d0..a31c16c36d9 100644
--- a/hummingbot/connector/gateway_base.py
+++ b/hummingbot/connector/gateway_base.py
@@ -82,10 +82,9 @@ def __init__(self,
         self._chain_info = {}
         self._status_polling_task = None
         self._auto_approve_task = None
-        self._initiate_pool_task = None
         self._get_chain_info_task = None
-        self._initiate_pool_status = None
         self._poll_notifier = None
+        self._nonce = None
 
     @property
     def name(self):
@@ -124,26 +123,6 @@ def is_pending_approval(self, token: str) -> bool:
         pending_approval_tokens = [tk.split("_")[2] for tk in self._in_flight_orders.keys()]
         return True if token in pending_approval_tokens else False
 
-    async def initiate_pool(self):
-        """
-        Basically calls a start endpoint which might be used on the gateway to initiate cache pools or get basic info about protocol.
-        """
-        try:
-            self.logger().info(f"Initializing {self.name} connector and caching for {self._trading_pairs} on Gateway.")
-            resp = await self._api_request("get", f"{self.base_path}/start",
-                                           {"pairs": json.dumps(self._trading_pairs)})
-            status = bool(str(resp["success"]))
-            if bool(str(resp["success"])):
-                self._initiate_pool_status = status
-        except asyncio.CancelledError:
-            raise
-        except Exception as e:
-            self.logger().network(
-                f"Error initializing {self._trading_pairs} swap pools",
-                exc_info=True,
-                app_warning_msg=str(e)
-            )
-
     async def get_chain_info(self):
         """
         Calls the base endpoint of the connector on Gateway to know basic info about chain being used.
@@ -166,7 +145,6 @@ async def auto_approve(self):
         Automatically approves trading pair tokens for contract(s).
         It first checks if there are any already approved amount (allowance)
         """
-        self.logger().info("Checking for allowances...")
         self._allowances = await self.get_allowances()
         for token, amount in self._allowances.items():
             if amount <= s_decimal_0 and not self.is_pending_approval(token):
@@ -178,10 +156,12 @@ async def approve_token(self, token_symbol: str):
         :param token_symbol: token to approve.
         """
         order_id = f"approve_{self.name}_{token_symbol}"
+        await self._update_nonce()
         resp = await self._api_request("post",
                                        "eth/approve",
                                        {"token": token_symbol,
-                                        "connector": self.name})
+                                        "spender": self.name,
+                                        "nonce": int(self._nonce)})
         self.start_tracking_order(order_id, None, token_symbol)
 
         if "hash" in resp.get("approval", {}).keys():
@@ -200,8 +180,8 @@ async def get_allowances(self) -> Dict[str, Decimal]:
         """
         ret_val = {}
         resp = await self._api_request("post", "eth/allowances",
-                                       {"tokenList": "[" + (",".join(['"' + t + '"' for t in self._tokens])) + "]",
-                                        "connector": self.name})
+                                       {"tokenSymbols": list(self._tokens),
+                                        "spender": self.name})
         for token, amount in resp["approvals"].items():
             ret_val[token] = Decimal(str(amount))
         return ret_val
@@ -223,7 +203,7 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal
                                            f"{self.base_path}/price",
                                            {"base": base,
                                             "quote": quote,
-                                            "amount": amount,
+                                            "amount": str(amount),
                                             "side": side.upper()})
             required_items = ["price", "gasLimit", "gasPrice", "gasCost"]
             if any(item not in resp.keys() for item in required_items):
@@ -324,11 +304,13 @@ async def _create_order(self,
         amount = self.quantize_order_amount(trading_pair, amount)
         price = self.quantize_order_price(trading_pair, price)
         base, quote = trading_pair.split("-")
+        await self._update_nonce()
         api_params = {"base": base,
                       "quote": quote,
                       "side": trade_type.name.upper(),
                       "amount": str(amount),
                       "limitPrice": str(price),
+                      "nonce": self._nonce,
                       }
         try:
             order_result = await self._api_request("post", f"{self.base_path}/trade", api_params)
@@ -409,7 +391,7 @@ async def _update_approval_order_status(self, tracked_orders: GatewayInFlightOrd
             for tracked_order in tracked_orders:
                 order_id = await tracked_order.get_exchange_order_id()
                 tasks.append(self._api_request("post",
-                                               f"{self.base_path}/poll",
+                                               "eth/poll",
                                                {"txHash": order_id}))
             update_results = await safe_gather(*tasks, return_exceptions=True)
             for tracked_order, update_result in zip(tracked_orders, update_results):
@@ -507,9 +489,7 @@ def status_dict(self) -> Dict[str, bool]:
     async def start_network(self):
         if self._trading_required:
             self._status_polling_task = safe_ensure_future(self._status_polling_loop())
-            self._initiate_pool_task = safe_ensure_future(self.initiate_pool())
             self._auto_approve_task = safe_ensure_future(self.auto_approve())
-        # self._get_chain_info_task = safe_ensure_future(self.get_chain_info())
 
     async def stop_network(self):
         if self._status_polling_task is not None:
@@ -518,18 +498,17 @@ async def stop_network(self):
         if self._auto_approve_task is not None:
             self._auto_approve_task.cancel()
             self._auto_approve_task = None
-        if self._initiate_pool_task is not None:
-            self._initiate_pool_task.cancel()
-            self._initiate_pool_task = None
         if self._get_chain_info_task is not None:
             self._get_chain_info_task.cancel()
             self._get_chain_info_task = None
 
     async def check_network(self) -> NetworkStatus:
         try:
-            response = await self._api_request("get", "api")
-            if response["status"] != "ok":
-                raise Exception(f"Error connecting to Gateway API. HTTP status is {response.status}.")
+            response = await self._api_request("get", "")
+            if 'status' in response and response['status'] == 'ok':
+                pass
+            else:
+                raise Exception(f"Error connecting to Gateway API. Response is {response}.")
         except asyncio.CancelledError:
             raise
         except Exception:
@@ -545,7 +524,15 @@ def tick(self, timestamp: float):
             if self._poll_notifier is not None and not self._poll_notifier.is_set():
                 self._poll_notifier.set()
 
+    async def _update_nonce(self):
+        """
+        Call the gateway API to get the current nonce for self._wallet_private_key
+        """
+        resp_json = await self._api_request("post", "eth/nonce", {})
+        self._nonce = int(resp_json['nonce'])
+
     async def _status_polling_loop(self):
+        await self._update_balances(on_interval = False)
         while True:
             try:
                 self._poll_notifier = asyncio.Event()
@@ -576,8 +563,7 @@ async def _update_balances(self, on_interval = False):
             remote_asset_names = set()
             resp_json = await self._api_request("post",
                                                 "eth/balances",
-                                                {"tokenList": "[" + (",".join(['"' + t + '"' for t in self._tokens])) + "]"})
-
+                                                {"tokenSymbols": list(self._tokens)})
             for token, bal in resp_json["balances"].items():
                 self._account_available_balances[token] = Decimal(str(bal))
                 self._account_balances[token] = Decimal(str(bal))
@@ -626,13 +612,14 @@ async def _api_request(self,
             params["privateKey"] = self._wallet_private_key
             if params["privateKey"][:2] != "0x":
                 params["privateKey"] = "0x" + params["privateKey"]
-            response = await client.post(url, data=params)
-
+            response = await client.post(url, json=params)
         parsed_response = json.loads(await response.text())
         if response.status != 200:
             err_msg = ""
             if "error" in parsed_response:
                 err_msg = f" Message: {parsed_response['error']}"
+            elif "message" in parsed_response:
+                err_msg = f" Message: {parsed_response['message']}"
             raise IOError(f"Error fetching data from {url}. HTTP status is {response.status}.{err_msg}")
         if "error" in parsed_response:
             raise Exception(f"Error: {parsed_response['error']} {parsed_response['message']}")
diff --git a/hummingbot/strategy/amm_arb/amm_arb.py b/hummingbot/strategy/amm_arb/amm_arb.py
index bbea42757f6..e0818b8db50 100644
--- a/hummingbot/strategy/amm_arb/amm_arb.py
+++ b/hummingbot/strategy/amm_arb/amm_arb.py
@@ -375,5 +375,4 @@ async def quote_in_eth_rate_fetch_loop(self):
     async def request_rate_in_eth(self, quote: str) -> int:
         if self._uniswap is None:
             self._uniswap = UniswapConnector([f"{quote}-WETH"], "", None)
-            await self._uniswap.initiate_pool()  # initiate to cache swap pool
         return await self._uniswap.get_quote_price(f"{quote}-WETH", True, 1)


Code review comments:
diff_hunk: @@ -89,7 +89,7 @@ export const validateAmount: Validator = mkValidator(
 export const validateNonce: Validator = mkValidator(
   'nonce',
   invalidNonceError,
-  (val) => typeof val === 'number' && val > -1,
+  (val) => typeof val === 'number' && val >= 0,
path: gateway/src/chains/ethereum/ethereum.validators.ts
text: @author:
this looks a bit nicer. probably need to add a check that it is an integer because number can also be a float. nonces should not be floats.