Can't get account information twice with the same connector, get invalid signature on second call

Hi,

I’m currently developing an API connector for Binance futures and during testings I realized that I have an error when I request the account information twice.

My connector works well for all the endpoints I have tried yet. And the signature seems to be correct.

I’m using the future testnet for my tests.

Here is what I’m sending for my request

  • First call
{'url': 'https://testnet.binancefuture.com/fapi/v2/account', 'params': 'timestamp=1654038614603&signature=2e5f44dd809569e7b3a5a9100f1a3061fae2cce20bec2ec8a71161cbed6d2585'}

I get a correct response with all my account information

  • Second call
{'url': 'https://testnet.binancefuture.com/fapi/v2/account', 'params': 'timestamp=1654038675531&signature=b2c5b0c89fa02772ae04a835389a0a3a1e54f65fbc8b8224d88b89c060d478d2'}

On this second call I have the following response

{'code': -1022, 'msg': 'Signature for this request is not valid.'}

I have tried to wait 60 seconds between the two calls but it doesn’t change anything.

Also if I try another method like get_active_orders twice, it does work, and it’s also a signed request.

I’m calling account_information endpoint several times because I use it to update the account balance from time to time, so I need to be able to call it several time.

Is there a reason why the account information endpoint could be requested only once ?

The endpoint works fine with multiple calls.

Thanks for your answer.

I retried this morning but I still have the issue.

I made a minimal example which reproduce my connector and my issue.

#!/usr/bin/python3 -u

from urllib.parse import urlencode
import requests
import hashlib
import hmac
import time

class BinanceConnector:

    def __init__(self, api_key, api_secret):
        self.api_key = api_key
        self.api_secret = api_secret
        self.session = requests.Session()
        self.session.headers.update({
            "Content-Type": "application/json;charset=utf-8",
                "X-MBX-APIKEY": self.api_key,
        })
        self.url = "https://testnet.binancefuture.com"

    def __request(self, method, path, payload={}, auth=False):
        url = '%s%s' % (self.url, path)

        if auth:
            payload["timestamp"] = self.__get_timestamp()
            query_string = self.__query_string(payload)
            signature = self.__generate_signature(query_string)
            payload['signature'] = signature

        params = {"url": url}
        if payload:
            params["params"] = self.__query_string(payload)

        response = self._dispatch_request(method)(**params)
        return response

    def _dispatch_request(self, http_method):
        return {
            "GET": self.session.get,
            "DELETE": self.session.delete,
            "PUT": self.session.put,
            "POST": self.session.post,
        }.get(http_method, "GET")

    def __get_timestamp(self):
        return int(time.time() * 1000)

    def __generate_signature(self, data):
        hash = hmac.new(self.api_secret.encode("utf-8"), data.encode("utf-8"), hashlib.sha256)
        return hash.hexdigest()

    def __query_string(self, query):
        if query == None:
            return ''
        else:
            filtered_query = {}
            for key in query.keys():
                if query[key] is not None:
                    filtered_query[key] = query[key]
            return urlencode(filtered_query, True).replace("%40", "@")

    def get_account_info(self):
        response = self.__request("GET", "/fapi/v2/account", auth=True)
        return response

    def get_active_orders(self, symbol):
        response = self.__request("GET", "/fapi/v1/openOrders", payload={"symbol": str(symbol)}, auth=True)
        return response

my_api_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
my_secret_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

connector = BinanceConnector(my_api_key, my_secret_key)

print(" - First call to 'get_active_orders'")
response = connector.get_active_orders("BTCUSDT")
print(response)
if response.status_code != 200:
    print(response.json())

print(" - Second call to 'get_active_orders'")
response = connector.get_active_orders("BTCUSDT")
print(response)
if response.status_code != 200:
    print(response.json())

# Call which works
print(" - First call to 'get_account_information'")
response = connector.get_account_info()
print(response)
if response.status_code != 200:
    print(response.json())

# Call which fails with invalid signature
print(" - Second call to 'get_account_information'")
response = connector.get_account_info()
print(response)
if response.status_code != 200:
    print(response.json())

I make two calls to the get_active_orders endpoint, they both work well, and then I make two calls to get_account_info endpoint and the second one fails.

Since I’m using the same connector for all the calls, I don’t think there is an issue with my signature.

Here is the execution trace

 - First call to 'get_active_orders'
<Response [200]>

 - Second call to 'get_active_orders'
<Response [200]>

 - First call to 'get_account_information'
<Response [200]>

 - Second call to 'get_account_information'
<Response [400]>
{'code': -1022, 'msg': 'Signature for this request is not valid.'}

I might be doing something wrong but I can’t find what. And since this connector works for all the others endpoints, even with multiple calls I really think there is something strange with this specific endpoint.

Do you have the same result running this test code ?

I finally found where the issue comes from.

Strangely, when the function __request is called with an empty payload argument, instead of setting the payload variable to an empty dictionary, it takes the dictionary of the previous call.

So when I’m doing the second call, it takes back the previous one, and thus the signature is computed onto the updated timestamp but also on the previous signature member.

I fixed it by setting the payload argument to None and by setting the payload variable to an empty dictionary inside the function.

def __request(self, method, path, payload=None, auth=False):
    if payload is None:
        payload = {}

This way it correctly works for several calls.

Weird behavior from python I didn’t know about.