fireblocks / fireblocks-sdk-py

Official Python SDK for Fireblocks API
http://docs.fireblocks.com/api/swagger-ui/
MIT License
51 stars 40 forks source link

random `500` internal error and `RemoteDisconnected` #174

Open adradr opened 5 months ago

adradr commented 5 months ago

Describe the bug I am constantly getting 500 internal error responses for get_vault_assets_balance. Other times I am also receiving:

requests.exceptions.ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))

To Reproduce Please see below

Expected behavior I am trying to make a snapshot of our Fireblocks account for backup and also accounting purposes. I am trying to iterate over each vault and its assets and gathering balances for each.

Versions (please complete the following information):

Additional context Here is an excerpt of my related code snippets and logs:

def catch_and_retry_fireblocks_exception(func):
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        max_retries = 3
        retries = 0
        while retries < max_retries:
            try:
                return func(self, *args, **kwargs)
            except FireblocksApiException as e:
                self.logger.error(
                    f"[Fireblocks] Error occurred for function {func.__name__}: {e}"
                )
                self.logger.error(f"[Fireblocks] Arguments: {args}, {kwargs}")
                # Handle the exception as needed
                retries += 1
                if retries < max_retries:
                    # Wait for 1 second before retrying
                    time.sleep(0.1)
                    self.logger.info(f"[Fireblocks] Retrying {func.__name__}...")
                else:
                    self.logger.warning(
                        f"[Fireblocks] Maximum retries reached. Skipping {func.__name__}."
                    )

    return wrapper

class FireblocksSDKWrapper:

    def __init__(self, api_key: str, private_key: str):
        self.fireblocks = FireblocksSDK(api_key=api_key, private_key=private_key)
        self.logger = Logger(name=__name__, level="INFO").get_logger()

    @catch_and_retry_fireblocks_exception
    def get_vault_accounts_paginated(self):
        next_page = True
        paged_filter = PagedVaultAccountsRequestFilters()
        accounts = []
        while next_page:
            vaults = self.fireblocks.get_vault_accounts_with_page_info(paged_filter)
            accounts = vaults["accounts"]
            accounts.extend(accounts)

            # Paginate through accounts
            if vaults.get("paging").get("after") is None:
                next_page = False
            else:
                paged_filter.after = vaults.get("paging").get("after")

        return accounts

    @catch_and_retry_fireblocks_exception
    def get_internal_wallets(self):
        return self.fireblocks.get_internal_wallets()

    @catch_and_retry_fireblocks_exception
    def get_deposit_addresses(self, vault_account_id: typing.Any, asset_id: typing.Any):
        return self.fireblocks.get_deposit_addresses(vault_account_id, asset_id)

    @catch_and_retry_fireblocks_exception
    def get_vault_assets_balance(
        self,
        account_name_prefix: typing.Any | None = None,
        account_name_suffix: typing.Any | None = None,
    ):
        return self.fireblocks.get_vault_assets_balance(
            account_name_prefix,
            account_name_suffix,
        )

@dataclasses.dataclass
class FireblocksAccountingLoader(ExchangeLoaderBase):
    fireblocks: typing.Optional[FireblocksSDKWrapper] = dataclasses.field(init=True)
    vault_accounts: typing.List[dict] = dataclasses.field(init=False)
    internal_wallets: typing.List[dict] = dataclasses.field(init=False)
    accounts: typing.List[dict] = dataclasses.field(init=False)
    logger: logging.Logger = dataclasses.field(
        default_factory=lambda: logging.Logger(__name__)
    )

    def __post_init__(self):
        self.vault_accounts = self.fireblocks.get_vault_accounts_paginated()
        self.internal_wallets = self.fireblocks.get_internal_wallets()
        self.accounts = self.vault_accounts  # + self.internal_wallets

    def fetch_accounts(self, utc_timestamp: str) -> pd.DataFrame:
        df = pd.DataFrame(self.accounts)

        # explode the assets column
        df = df.explode("assets")

        # Cast nan assets to {}
        df.assets = df.assets.fillna({i: {} for i in df.index})

        # create new columns for each key in the dictionaries
        df["asset_id"] = df["assets"].apply(lambda x: x["id"] if "id" in x else None)
        df["asset_total"] = df["assets"].apply(
            lambda x: x["total"] if "total" in x else None
        )
        df["asset_balance"] = df["assets"].apply(
            lambda x: x["balance"] if "balance" in x else None
        )
        df["account_type"] = df["name"].apply(
            lambda x: (
                "internal_wallet" if x == "Gas Station Wallet" else "vault_account"
            )
        )

        # Cast id to int
        df["id"] = df["id"].astype(int)

        # Remove "Network Deposits" vault accounts
        df = df[df.name != "Network Deposits"]

        # Add the timestamp to the dataframe
        df["timestamp"] = utc_timestamp
        # drop the original assets column
        df = df.drop("assets", axis=1)
        # Reset index
        df = df.reset_index(drop=True)

        # Fetch asset_id to a dict

        # Get the vault account asset information
        for row, col in df.iterrows():
            # Skip if the account is an internal wallet
            if col.account_type == "internal_wallet":
                # Get an index of the internal wallet
                internal_wallet_index = [
                    i for i, x in enumerate(self.internal_wallets) if x["id"] == col.id
                ][0]
                asset_info = self.internal_wallets[internal_wallet_index]["assets"]
            elif col.account_type == "vault_account" and col.asset_id is not None:
                asset_info = self.fireblocks.get_deposit_addresses(
                    vault_account_id=col.id,
                    asset_id=col.asset_id,
                )
            else:
                asset_info = []
            # Restructure the asset_info into a dictionary
            asset_dict = {}
            for asset in asset_info:
                asset_dict[asset["address"]] = {
                    "asset_id": asset["assetId"] if "assetId" in asset else None,
                    "address": asset["address"] if "address" in asset else None,
                    "tag": asset["tag"] if "tag" in asset else None,
                    "description": (
                        asset["description"] if "description" in asset else None
                    ),
                    "type": asset["type"] if "type" in asset else None,
                    "addressFormat": (
                        asset["addressFormat"] if "addressFormat" in asset else None
                    ),
                    "legacyAddress": (
                        asset["legacyAddress"] if "legacyAddress" in asset else None
                    ),
                    "bip44AddressIndex": (
                        asset["bip44AddressIndex"]
                        if "bip44AddressIndex" in asset
                        else None
                    ),
                }
            # Add asset_info as a column to the dataframe being iterated over
            df.loc[row, "addresses"] = [[asset_dict]]

        # Get additional balance information
        df_assets = pd.DataFrame()
        for vault_name in df.name.unique():
            # Skip if the account is an internal wallet
            if vault_name == "Gas Station Wallet":
                continue
            asset_balances = pd.DataFrame(
                self.fireblocks.get_vault_assets_balance(vault_name)
            )
            asset_balances = asset_balances.rename(columns={"id": "asset_id"})
            asset_balances["name"] = vault_name
            df_assets = pd.concat([df_assets, asset_balances], axis=0)

        # Merge the asset balances with the dataframe
        df_merged = pd.merge(df, df_assets, on=["name", "asset_id"])

        # Calculate prices
        asset_ids = df_merged.asset_id.unique()
        asset_prices = {}
        for asset_id in asset_ids:
            try:
                asset_id_split = asset_id.split("_")[0]
                asset_prices[asset_id] = self.calculate_usd_price(
                    asset=asset_id_split,
                    exchange=ccxt.bitmart(),  # Using bitmart since only they have RND
                )
            except Exception as e:  # pylint: disable=broad-except
                self.logger.info(
                    "[Fireblocks] Could not fetch price for %s (error: %s)",
                    asset_id,
                    e,
                )
                continue

        for row, col in df_merged.iterrows():
            try:
                asset_price = asset_prices[col.asset_id]
                df_merged.loc[row, "usd_price"] = asset_prices[col.asset_id]
                df_merged.loc[row, "usd_value"] = (
                    col.asset_total * asset_price if asset_price is not None else None
                )
            except Exception as e:  # pylint: disable=broad-except
                self.logger.info(
                    "[Fireblocks] Could not set price for %s: %s)", col.asset_id, e
                )
                continue

        return df_merged

    def to_dataframe(self, utc_timestamp: pd.Timestamp = None):
        return self.fetch_accounts(
            utc_timestamp=(
                pd.Timestamp.utcnow() if utc_timestamp is None else utc_timestamp
            )
        )
2024-03-07 10:56:19,315 - rand_treasury_monitoring.utils.fireblocks - ERROR - [Fireblocks] Error occurred for function get_vault_assets_balance: Got an error from fireblocks server: 500

2024-03-07 10:56:19,426 - rand_treasury_monitoring.utils.fireblocks - INFO - [Fireblocks] Retrying get_vault_assets_balance...
2024-03-07 11:08:53,224 - rand_treasury_monitoring.utils.fireblocks - ERROR - [Fireblocks] Error occurred for function get_vault_assets_balance: Got an error from fireblocks server: 500

2024-03-07 11:08:53,327 - rand_treasury_monitoring.utils.fireblocks - INFO - [Fireblocks] Retrying get_vault_assets_balance...
2024-03-07 11:11:35,880 - rand_treasury_monitoring.utils.fireblocks - ERROR - [Fireblocks] Error occurred for function get_vault_assets_balance: Got an error from fireblocks server: 500

2024-03-07 11:11:35,984 - rand_treasury_monitoring.utils.fireblocks - INFO - [Fireblocks] Retrying get_vault_assets_balance...
2024-03-07 11:14:23,272 - rand_treasury_monitoring.utils.fireblocks - ERROR - [Fireblocks] Error occurred for function get_vault_assets_balance: Got an error from fireblocks server: 500

2024-03-07 11:14:23,378 - rand_treasury_monitoring.utils.fireblocks - INFO - [Fireblocks] Retrying get_vault_assets_balance...
2024-03-07 11:17:10,825 - rand_treasury_monitoring.utils.fireblocks - ERROR - [Fireblocks] Error occurred for function get_vault_assets_balance: Got an error from fireblocks server: 500

2024-03-07 11:17:10,932 - rand_treasury_monitoring.utils.fireblocks - INFO - [Fireblocks] Retrying get_vault_assets_balance...
2024-03-07 11:19:52,531 - rand_treasury_monitoring.utils.fireblocks - ERROR - [Fireblocks] Error occurred for function get_vault_assets_balance: Got an error from fireblocks server: 500

2024-03-07 11:19:52,640 - rand_treasury_monitoring.utils.fireblocks - INFO - [Fireblocks] Retrying get_vault_assets_balance...
2024-03-07 11:22:33,295 - rand_treasury_monitoring.utils.fireblocks - ERROR - [Fireblocks] Error occurred for function get_vault_assets_balance: Got an error from fireblocks server: 500

2024-03-07 11:22:33,401 - rand_treasury_monitoring.utils.fireblocks - INFO - [Fireblocks] Retrying get_vault_assets_balance...
2024-03-07 11:25:17,782 - rand_treasury_monitoring.utils.fireblocks - ERROR - [Fireblocks] Error occurred for function get_vault_assets_balance: Got an error from fireblocks server: 500

2024-03-07 11:25:17,890 - rand_treasury_monitoring.utils.fireblocks - INFO - [Fireblocks] Retrying get_vault_assets_balance...
2024-03-07 11:28:00,107 - rand_treasury_monitoring.utils.fireblocks - ERROR - [Fireblocks] Error occurred for function get_vault_assets_balance: Got an error from fireblocks server: 500

2024-03-07 11:28:00,211 - rand_treasury_monitoring.utils.fireblocks - INFO - [Fireblocks] Retrying get_vault_assets_balance...
Exception has occurred: ConnectionError       (note: full exception trace is shown but execution is paused at: <module>)
('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
http.client.RemoteDisconnected: Remote end closed connection without response

During handling of the above exception, another exception occurred:

urllib3.exceptions.ProtocolError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))

During handling of the above exception, another exception occurred:

  File "/Users/adr/Dev/Rand-Network/treasury-balance-service/rand_treasury_monitoring/utils/fireblocks.py", line 74, in get_vault_assets_balance
    return self.fireblocks.get_vault_assets_balance(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/adr/Dev/Rand-Network/treasury-balance-service/rand_treasury_monitoring/utils/fireblocks.py", line 17, in wrapper
    return func(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/adr/Dev/Rand-Network/treasury-balance-service/rand_treasury_monitoring/data/loaders.py", line 316, in fetch_accounts
    self.fireblocks.get_vault_assets_balance(vault_name)
  File "/Users/adr/Dev/Rand-Network/treasury-balance-service/rand_treasury_monitoring/data/loaders.py", line 357, in to_dataframe
    return self.fetch_accounts(
           ^^^^^^^^^^^^^^^^^^^^
  File "/var/folders/w6/dh79cy2x0sjdkb2y4vrs8kq00000gn/T/ipykernel_18952/3364672574.py", line 1, in <module> (Current frame)
    loader.to_dataframe()
requests.exceptions.ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))