Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

random 500 internal error and RemoteDisconnected #174

Open
adradr opened this issue Mar 7, 2024 · 0 comments
Open

random 500 internal error and RemoteDisconnected #174

adradr opened this issue Mar 7, 2024 · 0 comments
Assignees
Labels
bug Something isn't working

Comments

@adradr
Copy link

adradr commented Mar 7, 2024

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):

  • Python Version: 3.12.1
  • fireblocks-sdk version: fireblocks_sdk==2.5.1

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'))
@adradr adradr added the bug Something isn't working label Mar 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants