diff --git a/additional_tests/exchanges_tests/__init__.py b/additional_tests/exchanges_tests/__init__.py index 65ad3e142..f351a2809 100644 --- a/additional_tests/exchanges_tests/__init__.py +++ b/additional_tests/exchanges_tests/__init__.py @@ -22,6 +22,7 @@ import octobot_commons.constants as commons_constants import octobot_commons.asyncio_tools as asyncio_tools import octobot_commons.os_util as os_util +import octobot_commons.configuration as configuration import octobot_commons.tests.test_config as test_config import octobot_trading.api as trading_api import octobot_trading.exchanges as exchanges @@ -61,15 +62,18 @@ def get_name(self): @contextlib.asynccontextmanager -async def get_authenticated_exchange_manager(exchange_name, exchange_tentacle_name, config=None, - credentials_exchange_name=None, market_filter=None): +async def get_authenticated_exchange_manager( + exchange_name, exchange_tentacle_name, config=None, + credentials_exchange_name=None, market_filter=None, + use_invalid_creds=False +): credentials_exchange_name = credentials_exchange_name or exchange_name _load_exchange_creds_env_variables_if_necessary() config = {**test_config.load_test_config(), **config} if config else test_config.load_test_config() if exchange_name not in config[commons_constants.CONFIG_EXCHANGES]: config[commons_constants.CONFIG_EXCHANGES][exchange_name] = {} config[commons_constants.CONFIG_EXCHANGES][exchange_name].update(_get_exchange_auth_details( - credentials_exchange_name + credentials_exchange_name, use_invalid_creds )) exchange_type = config[commons_constants.CONFIG_EXCHANGES][exchange_name].get( commons_constants.CONFIG_EXCHANGE_TYPE, exchanges.get_default_exchange_type(exchange_name)) @@ -129,15 +133,35 @@ def _load_exchange_creds_env_variables_if_necessary(): LOADED_EXCHANGE_CREDS_ENV_VARIABLES = True -def _get_exchange_auth_details(exchange_name): - return { +def _get_exchange_auth_details(exchange_name, use_invalid_creds): + config = { commons_constants.CONFIG_EXCHANGE_KEY: _get_exchange_credential_from_env(exchange_name, commons_constants.CONFIG_EXCHANGE_KEY), - commons_constants.CONFIG_EXCHANGE_SECRET: - _get_exchange_credential_from_env(exchange_name, commons_constants.CONFIG_EXCHANGE_SECRET), + commons_constants.CONFIG_EXCHANGE_SECRET: _get_exchange_credential_from_env( + exchange_name, commons_constants.CONFIG_EXCHANGE_SECRET + ), commons_constants.CONFIG_EXCHANGE_PASSWORD: _get_exchange_credential_from_env(exchange_name, commons_constants.CONFIG_EXCHANGE_PASSWORD), } + if use_invalid_creds: + _invalidate(config) + return config + + +def _invalidate(exchange_config: dict): + # try to invalidate key while returning a "plausible" value to avoid signature / parsing issues + decoded = configuration.decrypt_element_if_possible( + commons_constants.CONFIG_EXCHANGE_KEY, exchange_config, None + ) + updated_decoded = decoded + for numb in range(9): + if str(numb) in decoded: + number_index = decoded.index(str(numb)) + updated_decoded = f"{decoded[:number_index]}{numb + 1}{decoded[number_index+1:]}" + break + if updated_decoded == decoded: + raise ValueError("No number to invalid api key") + exchange_config[commons_constants.CONFIG_EXCHANGE_KEY] = configuration.encrypt(updated_decoded).decode() def _get_exchange_credential_from_env(exchange_name, cred_suffix): diff --git a/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py b/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py index fb8131397..666d93c17 100644 --- a/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py +++ b/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py @@ -143,6 +143,17 @@ async def inner_test_get_account_id(self): with pytest.raises(NotImplementedError): await self.exchange_manager.exchange.get_account_id() + async def test_invalid_api_key_error(self): + with pytest.raises(trading_errors.AuthenticationError): + created_exchange = mock.Mock() + async with self.local_exchange_manager(use_invalid_creds=True): + created_exchange() + # should fail + await self.get_portfolio() + raise AssertionError("Did not raise") + # ensure self.local_exchange_manager did not raise + created_exchange.assert_called_once() + async def test_get_api_key_permissions(self): async with self.local_exchange_manager(): await self.inner_test_get_api_key_permissions() @@ -735,7 +746,7 @@ async def get_price(self, symbol=None): )) @contextlib.asynccontextmanager - async def local_exchange_manager(self, market_filter=None, identifiers_suffix=None): + async def local_exchange_manager(self, market_filter=None, identifiers_suffix=None, use_invalid_creds=False): try: exchange_tentacle_name = self.EXCHANGE_TENTACLE_NAME or self.EXCHANGE_NAME.capitalize() credentials_exchange_name = self.CREDENTIALS_EXCHANGE_NAME or self.EXCHANGE_NAME @@ -746,7 +757,8 @@ async def local_exchange_manager(self, market_filter=None, identifiers_suffix=No exchange_tentacle_name, self.get_config(), credentials_exchange_name=credentials_exchange_name, - market_filter=market_filter + market_filter=market_filter, + use_invalid_creds=use_invalid_creds, ) as exchange_manager: self.exchange_manager = exchange_manager yield diff --git a/additional_tests/exchanges_tests/test_ascendex.py b/additional_tests/exchanges_tests/test_ascendex.py index 4fbb5311b..847477df1 100644 --- a/additional_tests/exchanges_tests/test_ascendex.py +++ b/additional_tests/exchanges_tests/test_ascendex.py @@ -44,6 +44,9 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_binance.py b/additional_tests/exchanges_tests/test_binance.py index bb12ba7d6..2e4363c40 100644 --- a/additional_tests/exchanges_tests/test_binance.py +++ b/additional_tests/exchanges_tests/test_binance.py @@ -47,6 +47,9 @@ async def test_get_portfolio_with_market_filter(self): async def test_get_account_id(self): await super().test_get_account_id() + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): await super().test_get_api_key_permissions() diff --git a/additional_tests/exchanges_tests/test_binance_futures.py b/additional_tests/exchanges_tests/test_binance_futures.py index 5cedfdd9c..4cc7e4a72 100644 --- a/additional_tests/exchanges_tests/test_binance_futures.py +++ b/additional_tests/exchanges_tests/test_binance_futures.py @@ -51,6 +51,9 @@ async def test_get_portfolio_with_market_filter(self): async def test_get_account_id(self): await super().test_get_account_id() + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_bingx.py b/additional_tests/exchanges_tests/test_bingx.py index 6d87a2054..257479205 100644 --- a/additional_tests/exchanges_tests/test_bingx.py +++ b/additional_tests/exchanges_tests/test_bingx.py @@ -46,6 +46,9 @@ async def test_get_portfolio_with_market_filter(self): async def test_get_account_id(self): await super().test_get_account_id() + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): await super().test_get_api_key_permissions() diff --git a/additional_tests/exchanges_tests/test_bitget.py b/additional_tests/exchanges_tests/test_bitget.py index 7e90c1360..628be48cf 100644 --- a/additional_tests/exchanges_tests/test_bitget.py +++ b/additional_tests/exchanges_tests/test_bitget.py @@ -46,6 +46,9 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_bitmart.py b/additional_tests/exchanges_tests/test_bitmart.py index f42ba8684..82ae6d604 100644 --- a/additional_tests/exchanges_tests/test_bitmart.py +++ b/additional_tests/exchanges_tests/test_bitmart.py @@ -44,6 +44,9 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_bybit.py b/additional_tests/exchanges_tests/test_bybit.py index f2aceae70..0a5782030 100644 --- a/additional_tests/exchanges_tests/test_bybit.py +++ b/additional_tests/exchanges_tests/test_bybit.py @@ -48,6 +48,9 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_bybit_futures.py b/additional_tests/exchanges_tests/test_bybit_futures.py index d447609f8..e30453855 100644 --- a/additional_tests/exchanges_tests/test_bybit_futures.py +++ b/additional_tests/exchanges_tests/test_bybit_futures.py @@ -51,6 +51,9 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_coinbase.py b/additional_tests/exchanges_tests/test_coinbase.py index fb9cee9df..01532a1ab 100644 --- a/additional_tests/exchanges_tests/test_coinbase.py +++ b/additional_tests/exchanges_tests/test_coinbase.py @@ -44,6 +44,9 @@ async def test_get_portfolio_with_market_filter(self): async def test_get_account_id(self): await super().test_get_account_id() + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): await super().test_get_api_key_permissions() diff --git a/additional_tests/exchanges_tests/test_coinex.py b/additional_tests/exchanges_tests/test_coinex.py index 5fe7a67c9..536697e85 100644 --- a/additional_tests/exchanges_tests/test_coinex.py +++ b/additional_tests/exchanges_tests/test_coinex.py @@ -43,6 +43,9 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_cryptocom.py b/additional_tests/exchanges_tests/test_cryptocom.py index 6f0e9bbe1..43c0e7795 100644 --- a/additional_tests/exchanges_tests/test_cryptocom.py +++ b/additional_tests/exchanges_tests/test_cryptocom.py @@ -44,6 +44,9 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_gateio.py b/additional_tests/exchanges_tests/test_gateio.py index 94899e31c..bf466f5f5 100644 --- a/additional_tests/exchanges_tests/test_gateio.py +++ b/additional_tests/exchanges_tests/test_gateio.py @@ -45,6 +45,10 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + # await super().test_invalid_api_key_error() # raises Request timeout + pass + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_hollaex.py b/additional_tests/exchanges_tests/test_hollaex.py index 56311f7b0..c445ed702 100644 --- a/additional_tests/exchanges_tests/test_hollaex.py +++ b/additional_tests/exchanges_tests/test_hollaex.py @@ -46,6 +46,9 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_htx.py b/additional_tests/exchanges_tests/test_htx.py index 77a7f5cf8..ac9b56106 100644 --- a/additional_tests/exchanges_tests/test_htx.py +++ b/additional_tests/exchanges_tests/test_htx.py @@ -45,6 +45,9 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_kucoin.py b/additional_tests/exchanges_tests/test_kucoin.py index da70b469f..8aa92be46 100644 --- a/additional_tests/exchanges_tests/test_kucoin.py +++ b/additional_tests/exchanges_tests/test_kucoin.py @@ -47,6 +47,9 @@ async def test_create_and_cancel_limit_orders(self): async def test_get_account_id(self): await super().test_get_account_id() + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): await super().test_get_api_key_permissions() diff --git a/additional_tests/exchanges_tests/test_kucoin_futures.py b/additional_tests/exchanges_tests/test_kucoin_futures.py index ef4eddb15..943b35a30 100644 --- a/additional_tests/exchanges_tests/test_kucoin_futures.py +++ b/additional_tests/exchanges_tests/test_kucoin_futures.py @@ -48,6 +48,9 @@ async def test_get_portfolio_with_market_filter(self): async def test_get_account_id(self): await super().test_get_account_id() + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): await super().test_get_api_key_permissions() diff --git a/additional_tests/exchanges_tests/test_mexc.py b/additional_tests/exchanges_tests/test_mexc.py index d2c09ccbb..ad95718c7 100644 --- a/additional_tests/exchanges_tests/test_mexc.py +++ b/additional_tests/exchanges_tests/test_mexc.py @@ -45,6 +45,9 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/exchanges_tests/test_okx.py b/additional_tests/exchanges_tests/test_okx.py index 9facd1215..16831d6f3 100644 --- a/additional_tests/exchanges_tests/test_okx.py +++ b/additional_tests/exchanges_tests/test_okx.py @@ -47,6 +47,9 @@ async def test_create_and_cancel_limit_orders(self): async def test_get_account_id(self): await super().test_get_account_id() + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): await super().test_get_api_key_permissions() diff --git a/additional_tests/exchanges_tests/test_okx_futures.py b/additional_tests/exchanges_tests/test_okx_futures.py index ddc336412..9897bb189 100644 --- a/additional_tests/exchanges_tests/test_okx_futures.py +++ b/additional_tests/exchanges_tests/test_okx_futures.py @@ -46,6 +46,9 @@ async def test_get_portfolio_with_market_filter(self): async def test_get_account_id(self): await super().test_get_account_id() + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): await super().test_get_api_key_permissions() diff --git a/additional_tests/exchanges_tests/test_phemex.py b/additional_tests/exchanges_tests/test_phemex.py index 052280d03..44df6e6ee 100644 --- a/additional_tests/exchanges_tests/test_phemex.py +++ b/additional_tests/exchanges_tests/test_phemex.py @@ -43,6 +43,9 @@ async def test_get_account_id(self): # pass if not implemented pass + async def test_invalid_api_key_error(self): + await super().test_invalid_api_key_error() + async def test_get_api_key_permissions(self): # pass if not implemented pass diff --git a/additional_tests/supabase_backend_tests/.env.template b/additional_tests/supabase_backend_tests/.env.template index c2c065c3e..35cda0699 100644 --- a/additional_tests/supabase_backend_tests/.env.template +++ b/additional_tests/supabase_backend_tests/.env.template @@ -4,6 +4,7 @@ SUPABASE_BACKEND_KEY= SUPABASE_BACKEND_CLIENT_1_EMAIL= SUPABASE_BACKEND_CLIENT_1_PASSWORD= SUPABASE_BACKEND_CLIENT_1_AUTH_KEY= +SUPABASE_BACKEND_CLIENT_1_EXPIRED_JWT_TOKEN= SUPABASE_BACKEND_CLIENT_2_EMAIL= SUPABASE_BACKEND_CLIENT_2_PASSWORD= diff --git a/additional_tests/supabase_backend_tests/__init__.py b/additional_tests/supabase_backend_tests/__init__.py index 6c085f833..4534fa512 100644 --- a/additional_tests/supabase_backend_tests/__init__.py +++ b/additional_tests/supabase_backend_tests/__init__.py @@ -221,6 +221,10 @@ def get_backend_client_auth_key(identifier): return os.getenv(f"SUPABASE_BACKEND_CLIENT_{identifier}_AUTH_KEY") +def get_backend_client_expired_jwt_token(identifier): + return os.getenv(f"SUPABASE_BACKEND_CLIENT_{identifier}_EXPIRED_JWT_TOKEN") + + def _get_backend_service_key(): return os.getenv(f"SUPABASE_BACKEND_SERVICE_KEY") diff --git a/additional_tests/supabase_backend_tests/test_user_elements.py b/additional_tests/supabase_backend_tests/test_user_elements.py index 2bd687af2..dfe3a198c 100644 --- a/additional_tests/supabase_backend_tests/test_user_elements.py +++ b/additional_tests/supabase_backend_tests/test_user_elements.py @@ -14,15 +14,20 @@ # You should have received a copy of the GNU General Public # License along with OctoBot. If not, see . import time + +import mock +import postgrest import pytest import octobot_commons.configuration as commons_configuration import octobot_commons.authentication as authentication import octobot.community as community +import octobot.community.supabase_backend as supabase_backend +import octobot.community.errors as community_errors import octobot.community.supabase_backend.enums as supabase_backend_enums from additional_tests.supabase_backend_tests import authenticated_client_1, authenticated_client_2, \ admin_client, anon_client, get_backend_api_creds, skip_if_no_service_key, get_backend_client_creds, \ - get_backend_client_auth_key + get_backend_client_auth_key, get_backend_client_expired_jwt_token # All test coroutines will be treated as marked. @@ -162,3 +167,34 @@ async def test_sign_in_with_auth_token(): finally: if supabase_client: await supabase_client.aclose() + + +async def test_expired_jwt_token(authenticated_client_1): + initial_email = (await authenticated_client_1.get_user())[supabase_backend_enums.UserKeys.EMAIL.value] + + # refreshing session is working + await authenticated_client_1.refresh_session() + # does not raise + bots = await authenticated_client_1.fetch_bots() + assert (await authenticated_client_1.get_user())[supabase_backend_enums.UserKeys.EMAIL.value] == initial_email + + # use expired jwt token + expired_jwt_token = get_backend_client_expired_jwt_token(1) + + # simulate auth using this token + session = mock.Mock(access_token=expired_jwt_token) + authenticated_client_1._listen_to_auth_events( + "SIGNED_IN", session + ) + + # now raising "APIError: JWT expired" + with pytest.raises(postgrest.APIError): + await authenticated_client_1.fetch_bots() + + # now raising "APIError: JWT expired" which is converted into community_errors.SessionTokenExpiredError + with pytest.raises(community_errors.SessionTokenExpiredError): + with supabase_backend.error_describer(): + await authenticated_client_1.fetch_bots() + + + diff --git a/octobot/cli.py b/octobot/cli.py index 83ea92c4e..8bf7f162f 100644 --- a/octobot/cli.py +++ b/octobot/cli.py @@ -151,14 +151,22 @@ async def _apply_db_bot_config(logger, config, community_auth) -> bool: try: # async loop may have changed if community_auth was already used before await community_auth.ensure_async_loop() - profile_data = await community_auth.supabase_client.fetch_bot_tentacles_data_based_config(constants.COMMUNITY_BOT_ID) + profile_data, auth_data = await community_auth.fetch_bot_tentacles_data_based_config( + constants.COMMUNITY_BOT_ID, + constants.USER_AUTH_KEY, + ) profile = await profiles.import_profile_data_as_profile( profile_data, constants.PROFILE_FILE_SCHEMA, None, name=profile_data.profile_details.name, - auto_update=False + auto_update=False, + force_simulator=False, ) + for auth_data_element in auth_data: + logger.info(f"Applying {auth_data_element.internal_name} exchange auth details") + auth_data_element.apply_to_exchange_config(config) + config.load_profiles() except octobot.community.errors.BotNotFoundError: raise errors.RemoteConfigError( @@ -189,15 +197,13 @@ async def _get_authenticated_community_if_possible(config, logger): try: if not community_auth.is_initialized(): if constants.IS_CLOUD_ENV: - if constants.USER_ACCOUNT_EMAIL and constants.USER_AUTH_KEY: - try: - logger.debug("Attempting auth key authentication") - await community_auth.login( - constants.USER_ACCOUNT_EMAIL, None, auth_key=constants.USER_AUTH_KEY - ) - except authentication.AuthenticationError as err: - logger.info(f"Auth key auth failure ({err}). Trying other methods if available.") - if constants.USER_ACCOUNT_EMAIL and constants.USER_PASSWORD_TOKEN: + authenticated = False + try: + logger.debug("Attempting auth key authentication") + authenticated = await community_auth.auto_reauthenticate() + except authentication.AuthenticationError as err: + logger.info(f"Auth key auth failure ({err}). Trying other methods if available.") + if not authenticated and constants.USER_ACCOUNT_EMAIL and constants.USER_PASSWORD_TOKEN: try: logger.debug("Attempting password token authentication") await community_auth.login( diff --git a/octobot/community/authentication.py b/octobot/community/authentication.py index 6bf57c1f3..f041a3ff3 100644 --- a/octobot/community/authentication.py +++ b/octobot/community/authentication.py @@ -40,7 +40,31 @@ import octobot_trading.enums as trading_enums +def expired_session_retrier(func): + async def expired_session_retrier_wrapper(*args, **kwargs): + self = args[0] + try: + with supabase_backend.error_describer(): + return await func(*args, **kwargs) + except errors.SessionTokenExpiredError: + try: + with supabase_backend.error_describer(): + self.logger.info(f"Expired session, trying to refresh token.") + await self.supabase_client.refresh_session() + return await func(*args, **kwargs) + except errors.SessionTokenExpiredError as err: + if await self.auto_reauthenticate(): + self.logger.error( + f"Impossible to use default refresh token, using saved auth details instead." + ) + return await func(*args, **kwargs) + # can't refresh token: logout + self.logger.warning(f"Expired session, please re-authenticate. {err}") + await self.logout() + return expired_session_retrier_wrapper + def _bot_data_update(func): + @expired_session_retrier async def bot_data_update_wrapper(*args, raise_errors=False, **kwargs): self = args[0] if not self.is_logged_in_and_has_selected_bot(): @@ -49,6 +73,9 @@ async def bot_data_update_wrapper(*args, raise_errors=False, **kwargs): try: self.logger.debug(f"bot_data_update: {func.__name__} initiated.") return await func(*args, **kwargs) + except errors.SessionTokenExpiredError: + # requried by expired_session_retrier + raise except Exception as err: if raise_errors: raise err @@ -218,6 +245,7 @@ async def _re_create_client(self): self.logger.debug(f"Refreshing user session") self.supabase_client.event_loop = asyncio.get_event_loop() await self.supabase_client.refresh_session() + await self._on_account_updated() async def ensure_async_loop(self): # elements should be bound to the current loop @@ -328,6 +356,15 @@ async def login( if self.is_logged_in(): await self.on_signed_in(minimal=minimal) + async def auto_reauthenticate(self) -> bool: + if constants.IS_CLOUD_ENV and constants.USER_ACCOUNT_EMAIL and constants.USER_AUTH_KEY: + self.logger.debug("Attempting auth key authentication") + await self.login( + constants.USER_ACCOUNT_EMAIL, None, auth_key=constants.USER_AUTH_KEY + ) + return self.is_logged_in() + return False + async def register(self, email, password): if self.must_be_authenticated_through_authenticator(): raise authentication.AuthenticationError("Creating a new account is not authorized on this environment.") @@ -548,6 +585,13 @@ def _get_compatible_strategy_categories(self) -> list[str]: category_types.append("index") return category_types + async def fetch_bot_tentacles_data_based_config( + self, bot_id: str, auth_key: typing.Optional[str] + ) -> (commons_profiles.ProfileData, list[commons_profiles.ExchangeAuthData]): + return await self.supabase_client.fetch_bot_tentacles_data_based_config( + bot_id, self, auth_key + ) + async def fetch_private_data(self, reset=False): try: mqtt_uuid = None diff --git a/octobot/community/errors.py b/octobot/community/errors.py index 66a58aaaa..e73b74857 100644 --- a/octobot/community/errors.py +++ b/octobot/community/errors.py @@ -24,6 +24,10 @@ class StatusCodeRequestError(RequestError): pass +class SessionTokenExpiredError(commons_authentication.AuthenticationError): + pass + + class BotError(commons_authentication.UnavailableError): pass diff --git a/octobot/community/supabase_backend/__init__.py b/octobot/community/supabase_backend/__init__.py index 563a3c84c..0f555bdb1 100644 --- a/octobot/community/supabase_backend/__init__.py +++ b/octobot/community/supabase_backend/__init__.py @@ -25,6 +25,7 @@ ) from octobot.community.supabase_backend import community_supabase_client from octobot.community.supabase_backend.community_supabase_client import ( + error_describer, CommunitySupabaseClient, HTTP_RETRY_COUNT, ) @@ -33,6 +34,7 @@ "SyncConfigurationStorage", "ASyncConfigurationStorage", "AuthenticatedAsyncSupabaseClient", + "error_describer", "CommunitySupabaseClient", "HTTP_RETRY_COUNT", ] diff --git a/octobot/community/supabase_backend/community_supabase_client.py b/octobot/community/supabase_backend/community_supabase_client.py index ec2984dbd..f1aac67aa 100644 --- a/octobot/community/supabase_backend/community_supabase_client.py +++ b/octobot/community/supabase_backend/community_supabase_client.py @@ -22,8 +22,9 @@ import httpx import uuid import json - +import contextlib import aiohttp + import gotrue.errors import gotrue.types import postgrest @@ -56,6 +57,16 @@ HTTP_RETRY_COUNT = 5 +@contextlib.contextmanager +def error_describer(): + try: + yield + except postgrest.APIError as err: + if "jwt expired" in str(err).lower(): + raise errors.SessionTokenExpiredError(err) from err + raise + + def _httpx_retrier(f): async def httpx_retrier_wrapper(*args, **kwargs): resp = None @@ -160,9 +171,9 @@ async def restore_session(self): if not self.is_signed_in(): raise authentication.FailedAuthentication() - async def refresh_session(self): + async def refresh_session(self, refresh_token: typing.Union[str, None] = None): try: - await self.auth.refresh_session() + await self.auth.refresh_session(refresh_token=refresh_token) except gotrue.errors.AuthError as err: raise authentication.AuthenticationError(err) from err @@ -355,7 +366,9 @@ async def update_bot_orders(self, bot_id, formatted_orders) -> dict: bot_id, bot_update ) - async def fetch_bot_tentacles_data_based_config(self, bot_id: str) -> commons_profiles.ProfileData: + async def fetch_bot_tentacles_data_based_config( + self, bot_id: str, authenticator, auth_key: typing.Optional[str] + ) -> (commons_profiles.ProfileData, list[commons_profiles.ExchangeAuthData]): if not bot_id: raise errors.BotNotFoundError(f"bot_id is '{bot_id}'") commons_logging.get_logger(__name__).debug(f"Fetching {bot_id} bot config") @@ -383,16 +396,26 @@ async def fetch_bot_tentacles_data_based_config(self, bot_id: str) -> commons_pr [], commons_profile_data.TradingData(commons_constants.USD_LIKE_COINS[0]) ) + auth_data = [] # apply specific options - self._apply_options_based_config(profile_data, bot_config["bot_config"]) - return profile_data + await self._apply_options_based_config( + profile_data, auth_data, bot_config["bot_config"], authenticator, auth_key + ) + return profile_data, auth_data + - def _apply_options_based_config(self, profile_data: commons_profiles.ProfileData, bot_config: dict): + async def _apply_options_based_config( + self, profile_data: commons_profiles.ProfileData, + auth_data: list[commons_profiles.ExchangeAuthData], bot_config: dict, + authenticator, auth_key: typing.Optional[str] + ): if tentacles_data := [ commons_profile_data.TentaclesData.from_dict(td) for td in bot_config[enums.BotConfigKeys.OPTIONS.value].get("tentacles", []) ]: - commons_profiles.TentaclesProfileDataTranslator(profile_data).translate(tentacles_data, bot_config) + await commons_profiles.TentaclesProfileDataTranslator(profile_data, auth_data).translate( + tentacles_data, bot_config, authenticator, auth_key + ) async def fetch_bot_profile_data(self, bot_config_id: str) -> commons_profiles.ProfileData: if not bot_config_id: diff --git a/requirements.txt b/requirements.txt index 5c29a02e5..1d3e9c078 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Drakkar-Software requirements -OctoBot-Commons==1.9.55 -OctoBot-Trading==2.4.107 +OctoBot-Commons==1.9.58 +OctoBot-Trading==2.4.110 OctoBot-Evaluators==1.9.6 OctoBot-Tentacles-Manager==2.9.16 OctoBot-Services==1.6.17 @@ -19,6 +19,7 @@ setuptools==69.0.3 # Community websockets gmqtt==0.6.16 +pgpy==0.6.0 # Error tracking sentry-sdk==2.13.0 # always make sure sentry_aiohttp_transport.py keep working