From eb7a1055d99f5c3881620ccabd772e11c87ed7e0 Mon Sep 17 00:00:00 2001
From: Guillaume De Saint Martin <guillaumemdsm@gmail.com>
Date: Thu, 22 Aug 2024 21:41:25 +0200
Subject: [PATCH 1/7] [Requirements] bump

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index a79ea10ae..2f9d0c4dd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
 # Drakkar-Software requirements
 OctoBot-Commons==1.9.54
-OctoBot-Trading==2.4.100
+OctoBot-Trading==2.4.101
 OctoBot-Evaluators==1.9.5
 OctoBot-Tentacles-Manager==2.9.16
 OctoBot-Services==1.6.17

From 39d5635218039889b2a4db9d9e4f1f8561efd832 Mon Sep 17 00:00:00 2001
From: Guillaume De Saint Martin <guillaumemdsm@gmail.com>
Date: Thu, 22 Aug 2024 23:08:23 +0200
Subject: [PATCH 2/7] [Tracker] update error tracker log

---
 octobot/community/errors_upload/sentry_tracker.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/octobot/community/errors_upload/sentry_tracker.py b/octobot/community/errors_upload/sentry_tracker.py
index 0f289fe60..13348b95f 100644
--- a/octobot/community/errors_upload/sentry_tracker.py
+++ b/octobot/community/errors_upload/sentry_tracker.py
@@ -26,9 +26,9 @@ def init_sentry_tracker():
     """
     Will upload errors to octobot.constants.ERROR_TRACKER_DSN if its value is set
     """
-    logger = octobot_commons.logging.get_logger(__name__)
+    logger = octobot_commons.logging.get_logger("sentry_tracker")
     if not octobot.constants.ERROR_TRACKER_DSN:
-        logger.debug(f"Skipping error tracker: error tracker dsn is '{octobot.constants.ERROR_TRACKER_DSN}'")
+        logger.debug(f"Error tracker disabled")
         return
     environment = "cloud" if octobot.constants.IS_CLOUD_ENV else "self hosted"
     app_name = f"{octobot.constants.PROJECT_NAME} open source"
@@ -72,7 +72,7 @@ def init_sentry_tracker():
 def flush_tracker():
     if octobot.constants.ERROR_TRACKER_DSN:
         delay = 2
-        octobot_commons.logging.get_logger(__name__).info(f"Flushing trackers: shutting down in {delay} seconds ...")
+        octobot_commons.logging.get_logger("sentry_tracker").info(f"Flushing trackers: shutting down in {delay} seconds ...")
         sentry_sdk.flush()
         # let trackers upload errors
         time.sleep(delay)

From 6d1770b22431e2e791ad866b6e8e4e4413efb84b Mon Sep 17 00:00:00 2001
From: Guillaume De Saint Martin <guillaumemdsm@gmail.com>
Date: Sun, 25 Aug 2024 00:33:12 +0200
Subject: [PATCH 3/7] [Requirements] bump

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 2f9d0c4dd..dfc0d46d9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
 # Drakkar-Software requirements
 OctoBot-Commons==1.9.54
-OctoBot-Trading==2.4.101
+OctoBot-Trading==2.4.102
 OctoBot-Evaluators==1.9.5
 OctoBot-Tentacles-Manager==2.9.16
 OctoBot-Services==1.6.17

From c02ab0d88f662a32df743ce40e02f2b217703348 Mon Sep 17 00:00:00 2001
From: Guillaume De Saint Martin <guillaumemdsm@gmail.com>
Date: Sun, 25 Aug 2024 12:05:32 +0200
Subject: [PATCH 4/7] [Requirements] bump

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index dfc0d46d9..9029ec652 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
 # Drakkar-Software requirements
 OctoBot-Commons==1.9.54
-OctoBot-Trading==2.4.102
+OctoBot-Trading==2.4.103
 OctoBot-Evaluators==1.9.5
 OctoBot-Tentacles-Manager==2.9.16
 OctoBot-Services==1.6.17

From 85b4ad52a40449f7fd9d81aa60843f8bc8aed545 Mon Sep 17 00:00:00 2001
From: Guillaume De Saint Martin <guillaumemdsm@gmail.com>
Date: Sun, 25 Aug 2024 17:29:18 +0200
Subject: [PATCH 5/7] [Auth] add auth key authentication

---
 .../supabase_backend_tests/.env.template      |  4 ++
 .../supabase_backend_tests/__init__.py        |  4 ++
 .../test_user_elements.py                     | 50 ++++++++++++++++++-
 octobot/cli.py                                | 25 +++++++---
 octobot/community/authentication.py           | 18 +++++--
 .../community_supabase_client.py              | 19 ++++++-
 octobot/constants.py                          |  1 +
 7 files changed, 107 insertions(+), 14 deletions(-)

diff --git a/additional_tests/supabase_backend_tests/.env.template b/additional_tests/supabase_backend_tests/.env.template
index a523992ef..c2c065c3e 100644
--- a/additional_tests/supabase_backend_tests/.env.template
+++ b/additional_tests/supabase_backend_tests/.env.template
@@ -3,8 +3,12 @@ SUPABASE_BACKEND_KEY=
 
 SUPABASE_BACKEND_CLIENT_1_EMAIL=
 SUPABASE_BACKEND_CLIENT_1_PASSWORD=
+SUPABASE_BACKEND_CLIENT_1_AUTH_KEY=
 
 SUPABASE_BACKEND_CLIENT_2_EMAIL=
 SUPABASE_BACKEND_CLIENT_2_PASSWORD=
 
+SUPABASE_BACKEND_CLIENT_3_EMAIL=
+SUPABASE_BACKEND_CLIENT_3_PASSWORD=
+
 SUPABASE_BACKEND_SERVICE_KEY=
diff --git a/additional_tests/supabase_backend_tests/__init__.py b/additional_tests/supabase_backend_tests/__init__.py
index 11a439045..6c085f833 100644
--- a/additional_tests/supabase_backend_tests/__init__.py
+++ b/additional_tests/supabase_backend_tests/__init__.py
@@ -217,6 +217,10 @@ def get_backend_client_creds(identifier):
         os.getenv(f"SUPABASE_BACKEND_CLIENT_{identifier}_PASSWORD")
 
 
+def get_backend_client_auth_key(identifier):
+    return os.getenv(f"SUPABASE_BACKEND_CLIENT_{identifier}_AUTH_KEY")
+
+
 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 35c5c076a..2bd687af2 100644
--- a/additional_tests/supabase_backend_tests/test_user_elements.py
+++ b/additional_tests/supabase_backend_tests/test_user_elements.py
@@ -21,7 +21,8 @@
 import octobot.community as community
 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
+    admin_client, anon_client, get_backend_api_creds, skip_if_no_service_key, get_backend_client_creds, \
+    get_backend_client_auth_key
 
 
 # All test coroutines will be treated as marked.
@@ -114,3 +115,50 @@ async def test_sign_in_with_otp_token(authenticated_client_1, skip_if_no_service
     finally:
         if supabase_client:
             await supabase_client.aclose()
+
+
+async def test_sign_in_with_auth_token():
+    # create new client
+    backend_url, backend_key = get_backend_api_creds()
+    email, _ = get_backend_client_creds(1)
+
+    config = commons_configuration.Configuration("", "")
+    config.config = {}
+    supabase_client = None
+    try:
+        supabase_client = community.CommunitySupabaseClient(
+            backend_url,
+            backend_key,
+            community.ASyncConfigurationStorage(config)
+        )
+        saved_session = "saved_session"
+        await supabase_client.auth._storage.set_item(supabase_client.auth._storage_key, saved_session)
+        # wrong configs
+        with pytest.raises(authentication.AuthenticationError):
+            await supabase_client.get_otp_with_auth_key("", "")
+        with pytest.raises(authentication.AuthenticationError):
+            await supabase_client.get_otp_with_auth_key(None, "")
+        with pytest.raises(authentication.AuthenticationError):
+            await supabase_client.get_otp_with_auth_key(email, None)
+        with pytest.raises(authentication.AuthenticationError):
+            await supabase_client.get_otp_with_auth_key(email, "1234")
+        assert await supabase_client.auth._storage.get_item(supabase_client.auth._storage_key) == saved_session
+        token = await supabase_client.get_otp_with_auth_key(email, get_backend_client_auth_key(1))
+        # ensure token is valid
+
+        await supabase_client.sign_in_with_otp_token(token)
+        # save session has been updated
+        updated_session = await supabase_client.auth._storage.get_item(supabase_client.auth._storage_key)
+        assert updated_session != saved_session
+
+        # ensure new supabase_client is bound to the same user as the previous client
+        user = await supabase_client.get_user()
+        assert user[supabase_backend_enums.UserKeys.EMAIL.value] == email
+
+        # already consumed token
+        with pytest.raises(authentication.AuthenticationError):
+            await supabase_client.sign_in_with_otp_token(token)
+        assert await supabase_client.auth._storage.get_item(supabase_client.auth._storage_key) == updated_session
+    finally:
+        if supabase_client:
+            await supabase_client.aclose()
diff --git a/octobot/cli.py b/octobot/cli.py
index f9500ee1e..7062a667a 100644
--- a/octobot/cli.py
+++ b/octobot/cli.py
@@ -13,7 +13,6 @@
 #
 #  You should have received a copy of the GNU General Public
 #  License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
-import time
 import argparse
 import os
 import sys
@@ -189,13 +188,23 @@ async def _get_authenticated_community_if_possible(config, logger):
     community_auth = octobot_community.CommunityAuthentication.create(config)
     try:
         if not community_auth.is_initialized():
-            if constants.IS_CLOUD_ENV and constants.USER_ACCOUNT_EMAIL and constants.USER_PASSWORD_TOKEN:
-                try:
-                    await community_auth.login(
-                        constants.USER_ACCOUNT_EMAIL, None, password_token=constants.USER_PASSWORD_TOKEN
-                    )
-                except authentication.AuthenticationError as err:
-                    logger.debug(f"Password token auth failure ({err}). Trying with saved session.")
+            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.debug(f"Auth key auth failure ({err}). Trying other methods if available.")
+                if constants.USER_ACCOUNT_EMAIL and constants.USER_PASSWORD_TOKEN:
+                    try:
+                        logger.debug("Attempting password token authentication")
+                        await community_auth.login(
+                            constants.USER_ACCOUNT_EMAIL, None, password_token=constants.USER_PASSWORD_TOKEN
+                        )
+                    except authentication.AuthenticationError as err:
+                        logger.debug(f"Password token auth failure ({err}). Trying with saved session.")
             if not community_auth.is_initialized():
                 # try with saved credentials if any
                 has_tentacles = tentacles_manager_api.is_tentacles_architecture_valid()
diff --git a/octobot/community/authentication.py b/octobot/community/authentication.py
index 3d699564a..b06cbb984 100644
--- a/octobot/community/authentication.py
+++ b/octobot/community/authentication.py
@@ -81,6 +81,7 @@ def __init__(self, config=None, backend_url=None, backend_key=None, use_as_singl
         self.user_account = community_user_account.CommunityUserAccount()
         self.public_data = community_public_data.CommunityPublicData()
         self.successfully_fetched_tentacles_package_urls = False
+        self.silent_auth = False
         self._community_feed = None
 
         self.initialized_event = None
@@ -305,11 +306,20 @@ def can_authenticate(self):
     def must_be_authenticated_through_authenticator(self):
         return constants.IS_CLOUD_ENV
 
-    async def login(self, email, password, password_token=None, minimal=False):
+    async def login(
+        self,
+        email: str,
+        password: typing.Optional[str],
+        password_token: typing.Optional[str] = None,
+        auth_key: typing.Optional[str] = None,
+        minimal: bool = False
+    ):
         self._ensure_email(email)
         self._ensure_community_url()
         self._reset_tokens()
         with self._login_process():
+            if auth_key and not password_token:
+                password_token = await self.supabase_client.get_otp_with_auth_key(email, auth_key)
             if password_token:
                 await self.supabase_client.sign_in_with_otp_token(password_token)
             else:
@@ -331,7 +341,8 @@ async def register(self, email, password):
             await self.on_signed_in()
 
     async def on_signed_in(self, minimal=False):
-        self.logger.info(f"Signed in as {self.get_logged_in_email()}")
+        if not self.silent_auth:
+            self.logger.info(f"Signed in as {self.get_logged_in_email()}")
         await self._initialize_account(minimal=minimal)
 
     async def _update_account_metadata(self, metadata_update):
@@ -669,7 +680,8 @@ async def _restore_previous_session(self):
                 # will raise on failure
                 await self.supabase_client.restore_session()
                 await self._on_account_updated()
-                self.logger.info(f"Signed in as {self.get_logged_in_email()}")
+                if not self.silent_auth:
+                    self.logger.info(f"Signed in as {self.get_logged_in_email()}")
         return self.is_logged_in()
 
     @contextlib.asynccontextmanager
diff --git a/octobot/community/supabase_backend/community_supabase_client.py b/octobot/community/supabase_backend/community_supabase_client.py
index df49152f1..ec2984dbd 100644
--- a/octobot/community/supabase_backend/community_supabase_client.py
+++ b/octobot/community/supabase_backend/community_supabase_client.py
@@ -21,6 +21,7 @@
 import logging
 import httpx
 import uuid
+import json
 
 import aiohttp
 import gotrue.errors
@@ -211,8 +212,22 @@ async def get_user(self) -> dict:
                 raise errors.EmailValidationRequiredError(err) from err
             raise authentication.AuthenticationError(f"Please re-login to your OctoBot account: {err}") from err
 
-    def sync_get_user(self) -> dict:
-        return self.auth.get_user().user.model_dump()
+    async def get_otp_with_auth_key(self, user_email: str, auth_key: str) -> str:
+        try:
+            resp = await self.functions.invoke(
+                "create-auth-token",
+                {
+                    "headers": {
+                        "User-Auth-Token": auth_key
+                    },
+                    "body": {
+                        "user_email": user_email
+                    },
+                }
+            )
+            return json.loads(resp)["token"]
+        except Exception:
+            raise authentication.AuthenticationError(f"Invalid auth key authentication details")
 
     async def fetch_bot(self, bot_id) -> dict:
         try:
diff --git a/octobot/constants.py b/octobot/constants.py
index 84ae1e7f6..f240d6188 100644
--- a/octobot/constants.py
+++ b/octobot/constants.py
@@ -107,6 +107,7 @@
 USE_BETA_EARLY_ACCESS = os_util.parse_boolean_environment_var("USE_BETA_EARLY_ACCESS", "false")
 USER_ACCOUNT_EMAIL = os.getenv("USER_ACCOUNT_EMAIL", "")
 USER_PASSWORD_TOKEN = os.getenv("USER_PASSWORD_TOKEN", None)
+USER_AUTH_KEY = os.getenv("USER_AUTH_KEY", None)
 COMMUNITY_BOT_ID = os.getenv("COMMUNITY_BOT_ID", "")
 IS_DEMO = os_util.parse_boolean_environment_var("IS_DEMO", "False")
 IS_CLOUD_ENV = os_util.parse_boolean_environment_var("IS_CLOUD_ENV", "false")

From 1244896485a22ea60d8e8e4693d6bcdeedd1803c Mon Sep 17 00:00:00 2001
From: Guillaume De Saint Martin <guillaumemdsm@gmail.com>
Date: Sun, 25 Aug 2024 18:07:24 +0200
Subject: [PATCH 6/7] [Requirements] bump

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 9029ec652..a178de88f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 # Drakkar-Software requirements
-OctoBot-Commons==1.9.54
+OctoBot-Commons==1.9.55
 OctoBot-Trading==2.4.103
 OctoBot-Evaluators==1.9.5
 OctoBot-Tentacles-Manager==2.9.16

From 2bb3c49ae178bb5386a13d2985ce574f90c276f0 Mon Sep 17 00:00:00 2001
From: Guillaume De Saint Martin <guillaumemdsm@gmail.com>
Date: Sun, 25 Aug 2024 18:36:15 +0200
Subject: [PATCH 7/7] [Version] v2.0.4

---
 CHANGELOG.md        | 16 ++++++++++++++++
 README.md           |  2 +-
 octobot/__init__.py |  2 +-
 3 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 400d12492..e1cc79586 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 *It is strongly advised to perform an update of your tentacles after updating OctoBot. (start.py tentacles --install --all)*
 
+## [2.0.4] - 2024-08-25
+### Added
+- [BitMart] The BitMart exchange is now officially supported
+- [GridTrading] Fund redispatch delay config
+### Updated
+- [WebInterface] Improve portfolio history display and make it more flexible
+- [Webhook] Make error messages easier to understand
+- [CCXT] update to ccxt 4.3.85
+- [Community] Fix community authentication related issues
+### Fixed
+- [GridTrading] Fixed grid reset issues when funds redispatch is enabled
+- [MEXC] Fixed MEXC traded pairs fetching issues
+- [OKX] Fixed leveraging parsing issues
+- [WebInterface] Fixed order cancel UI issues
+- [Configuration] Fixed recovery file related iss
+
 ## [2.0.3] - 2024-08-03
 ### Added
 - [IndexTradingMode]: Default profile, intra-day, real time update option and custom content
diff --git a/README.md b/README.md
index f60a82512..bd11ab65b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# OctoBot [2.0.3](https://github.com/Drakkar-Software/OctoBot/blob/master/CHANGELOG.md)
+# OctoBot [2.0.4](https://github.com/Drakkar-Software/OctoBot/blob/master/CHANGELOG.md)
 [![PyPI](https://img.shields.io/pypi/v/OctoBot.svg?logo=pypi)](https://pypi.org/project/OctoBot)
 [![Downloads](https://pepy.tech/badge/octobot/month)](https://pepy.tech/project/octobot)
 [![Dockerhub](https://img.shields.io/docker/pulls/drakkarsoftware/octobot.svg?logo=docker)](https://hub.docker.com/r/drakkarsoftware/octobot)
diff --git a/octobot/__init__.py b/octobot/__init__.py
index bd2f79797..edb057f5d 100644
--- a/octobot/__init__.py
+++ b/octobot/__init__.py
@@ -16,5 +16,5 @@
 
 PROJECT_NAME = "OctoBot"
 AUTHOR = "Drakkar-Software"
-VERSION = "2.0.3"  # major.minor.revision
+VERSION = "2.0.4"  # major.minor.revision
 LONG_VERSION = f"{VERSION}"