From 80cf5177a22adcfb0afb7fcb16482cebc172caaf Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Tue, 16 Jul 2024 09:54:15 +0300 Subject: [PATCH 01/19] reimplemented healthcheck --- operate/cli.py | 26 +----- operate/services/health_checker.py | 144 +++++++++++++++++++++++++++++ operate/services/manage.py | 38 -------- 3 files changed, 149 insertions(+), 59 deletions(-) create mode 100644 operate/services/health_checker.py diff --git a/operate/cli.py b/operate/cli.py index 80dfa36bf..07f87227b 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -42,6 +42,7 @@ from operate.account.user import UserAccount from operate.constants import KEY, KEYS, OPERATE, SERVICES from operate.ledger import get_ledger_type_from_chain_type +from operate.services.health_checker import HealthChecker from operate.types import ChainType, DeploymentStatus from operate.wallet.master import MasterWalletManager @@ -145,8 +146,7 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st logger = setup_logger(name="operate") operate = OperateApp(home=home, logger=logger) funding_jobs: t.Dict[str, asyncio.Task] = {} - healthcheck_jobs: t.Dict[str, asyncio.Task] = {} - + health_checker = HealthChecker(operate.service_manager()) # Create shutdown endpoint shutdown_endpoint = uuid.uuid4().hex (operate._path / "operate.kill").write_text( # pylint: disable=protected-access @@ -176,17 +176,7 @@ def schedule_healthcheck_job( service: str, ) -> None: """Schedule a healthcheck job.""" - logger.info(f"Starting healthcheck job for {service}") - if service in healthcheck_jobs: - logger.info(f"Cancelling existing healthcheck_jobs job for {service}") - cancel_healthcheck_job(service=service) - - loop = asyncio.get_running_loop() - healthcheck_jobs[service] = loop.create_task( - operate.service_manager().healthcheck_job( - hash=service, - ) - ) + health_checker.start_for_service(service) def cancel_funding_job(service: str) -> None: """Cancel funding job.""" @@ -210,16 +200,9 @@ def pause_all_services_on_startup() -> None: deployment.stop(force=True) logger.info(f"Cancelling funding job for {service}") cancel_funding_job(service=service) + health_checker.stop_for_service(service=service) logger.info("Stopping services on startup done.") - def cancel_healthcheck_job(service: str) -> None: - """Cancel healthcheck job.""" - if service not in healthcheck_jobs: - return - status = healthcheck_jobs[service].cancel() - if not status: - logger.info(f"Healthcheck job cancellation for {service} failed") - # on backend app started we assume there are now started agents, so we force to pause all pause_all_services_on_startup() @@ -695,6 +678,7 @@ async def _stop_service_locally(request: Request) -> JSONResponse: return service_not_found_error(service=request.path_params["service"]) service = request.path_params["service"] deployment = operate.service_manager().load_or_create(service).deployment + health_checker.stop_for_service(service=service) deployment.stop() logger.info(f"Cancelling funding job for {service}") cancel_funding_job(service=service) diff --git a/operate/services/health_checker.py b/operate/services/health_checker.py new file mode 100644 index 000000000..a297f9b44 --- /dev/null +++ b/operate/services/health_checker.py @@ -0,0 +1,144 @@ +import asyncio +import time +import traceback +import typing as t + +import aiohttp +from aea.helpers.logging import setup_logger + +from operate.services.manage import ServiceManager # type: ignore + + +HTTP_OK = 200 + + +class HealthChecker: + SLEEP_PERIOD = 30 + PORT_UP_TIMEOUT = 120 # seconds + + def __init__(self, service_manager: ServiceManager): + self._jobs: t.Dict[str, asyncio.Task] = {} + self.logger = setup_logger(name="operate.health_checker") + self.logger.info("[HEALTCHECKER]: created") + self._service_manager = service_manager + + def start_for_service(self, service: str): + self.logger.info(f"[HEALTCHECKER]: Starting healthcheck job for {service}") + if service in self._jobs: + self.stop_for_service(service=service) + + loop = asyncio.get_running_loop() + self._jobs[service] = loop.create_task( + self.healthcheck_job( + hash=service, + ) + ) + + def stop_for_service(self, service: str): + if service not in self._jobs: + return + self.logger.info( + f"[HEALTCHECKER]: Cancelling existing healthcheck_jobs job for {service}" + ) + status = self._jobs[service].cancel() + if not status: + self.logger.info( + f"[HEALTCHECKER]: Healthcheck job cancellation for {service} failed" + ) + + async def check_service_health(self, service: str) -> bool: + """Check the service health""" + del service + async with aiohttp.ClientSession() as session: + async with session.get("http://localhost:8716/healthcheck") as resp: + status = resp.status + response_json = await resp.json() + # self.logger.info(f"[HEALTCHECKER]: check {status}, {response_json}") + return status == HTTP_OK and response_json.get( + "is_transitioning_fast", False + ) + + async def healthcheck_job( + self, + hash: str, + ) -> None: + """Start a background funding job.""" + + try: + service = hash + + self.logger.info( + f"[HEALTCHECKER] Start healthcheck job for service: {service}" + ) + + async def _wait_for_port(sleep_period=15): + self.logger.info("[HEALTCHECKER]: wait port is up") + while True: + try: + await self.check_service_health(service) + self.logger.info("[HEALTCHECKER]: port is UP") + return + except aiohttp.ClientConnectionError: + self.logger.error("[HEALTCHECKER]: error connecting http port") + await asyncio.sleep(sleep_period) + + async def _check_port_ready(timeout=self.PORT_UP_TIMEOUT, sleep_period=15): + try: + await asyncio.wait_for( + _wait_for_port(sleep_period=sleep_period), timeout=timeout + ) + return True + except asyncio.TimeoutError: + return False + + async def _check_health(number_of_fails=5, sleep_period=self.SLEEP_PERIOD): + fails = 0 + while True: + try: + # Check the service health + healthy = await self.check_service_health(service) + except aiohttp.ClientConnectionError: + self.logger.info("[HEALTCHECKER] port read failed. restart") + return + self.logger.info(f"[HEALTCHECKER] is HEALTHY") + + if not healthy: + fails += 1 + self.logger.info( + f"[HEALTCHECKER] not healthy for {fails} time in a row" + ) + else: + # reset fails if comes healty + fails = 0 + + if fails >= number_of_fails: + # too much fails, exit + self.logger.error( + f"[HEALTCHECKER] failed {fails} times in a row. restart" + ) + return + await asyncio.sleep(sleep_period) + + async def _restart(service_manager, service): + service_manager.stop_service_locally(hash=service) + service_manager.deploy_service_locally(hash=service) + + # upper cycle + while True: + self.logger.info("[HEALTCHECKER] wait for port ready") + if not (await _check_port_ready(timeout=self.PORT_UP_TIMEOUT)): + self.logger.info( + "[HEALTCHECKER] port not ready within timeout. restart deploymen" + ) + else: + # blocking till restart needed + self.logger.info( + f"[HEALTCHECKER] port is ready, checking health every {self.SLEEP_PERIOD}" + ) + await _check_health(sleep_period=self.SLEEP_PERIOD) + + # perform restart + # TODO: blocking!!!!!!! + await _restart(self._service_manager, service) + except Exception as e: + self.logger.exception("oops") diff --git a/operate/services/manage.py b/operate/services/manage.py index 927fff7cc..f1cb481a0 100644 --- a/operate/services/manage.py +++ b/operate/services/manage.py @@ -27,7 +27,6 @@ from concurrent.futures import ThreadPoolExecutor from pathlib import Path -import aiohttp # type: ignore from aea.helpers.base import IPFSHash from aea.helpers.logging import setup_logger from autonomy.chain.base import registry_contracts @@ -62,17 +61,6 @@ HTTP_OK = 200 -async def check_service_health() -> bool: - """Check the service health""" - async with aiohttp.ClientSession() as session: - async with session.get("http://localhost:8716/healthcheck") as resp: - status = resp.status - response_json = await resp.json() - return status == HTTP_OK and response_json.get( - "is_transitioning_fast", False - ) - - class ServiceManager: """Service manager.""" @@ -922,32 +910,6 @@ async def funding_job( ) await asyncio.sleep(60) - async def healthcheck_job( - self, - hash: str, - ) -> None: - """Start a background funding job.""" - failed_health_checks = 0 - - while True: - try: - # Check the service health - healthy = await check_service_health() - # Restart the service if the health failed 5 times in a row - if not healthy: - failed_health_checks += 1 - else: - failed_health_checks = 0 - if failed_health_checks >= 4: - self.stop_service_locally(hash=hash) - self.deploy_service_locally(hash=hash) - - except Exception: # pylint: disable=broad-except - logging.info( - f"Error occured while checking the service health\n{traceback.format_exc()}" - ) - await asyncio.sleep(30) - def deploy_service_locally(self, hash: str, force: bool = True) -> Deployment: """ Deploy service locally From 1d4c44e8a10623d1149e6934de3c9d6fd513e45f Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Tue, 16 Jul 2024 14:27:34 +0300 Subject: [PATCH 02/19] linting --- operate/services/health_checker.py | 101 ++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 31 deletions(-) diff --git a/operate/services/health_checker.py b/operate/services/health_checker.py index a297f9b44..e1979be13 100644 --- a/operate/services/health_checker.py +++ b/operate/services/health_checker.py @@ -1,9 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""Source code for checking aea is alive..""" import asyncio -import time -import traceback import typing as t +from concurrent.futures import ThreadPoolExecutor -import aiohttp +import aiohttp # type: ignore from aea.helpers.logging import setup_logger from operate.services.manage import ServiceManager # type: ignore @@ -13,16 +32,20 @@ class HealthChecker: + """Health checker manager.""" + SLEEP_PERIOD = 30 PORT_UP_TIMEOUT = 120 # seconds - def __init__(self, service_manager: ServiceManager): + def __init__(self, service_manager: ServiceManager) -> None: + """Init the healtch checker.""" self._jobs: t.Dict[str, asyncio.Task] = {} self.logger = setup_logger(name="operate.health_checker") self.logger.info("[HEALTCHECKER]: created") self._service_manager = service_manager - def start_for_service(self, service: str): + def start_for_service(self, service: str) -> None: + """Start for a specific service.""" self.logger.info(f"[HEALTCHECKER]: Starting healthcheck job for {service}") if service in self._jobs: self.stop_for_service(service=service) @@ -30,11 +53,12 @@ def start_for_service(self, service: str): loop = asyncio.get_running_loop() self._jobs[service] = loop.create_task( self.healthcheck_job( - hash=service, + service=service, ) ) - def stop_for_service(self, service: str): + def stop_for_service(self, service: str) -> None: + """Stop for a specific service.""" if service not in self._jobs: return self.logger.info( @@ -46,32 +70,30 @@ def stop_for_service(self, service: str): f"[HEALTCHECKER]: Healthcheck job cancellation for {service} failed" ) - async def check_service_health(self, service: str) -> bool: + @staticmethod + async def check_service_health(service: str) -> bool: """Check the service health""" del service async with aiohttp.ClientSession() as session: async with session.get("http://localhost:8716/healthcheck") as resp: status = resp.status response_json = await resp.json() - # self.logger.info(f"[HEALTCHECKER]: check {status}, {response_json}") return status == HTTP_OK and response_json.get( "is_transitioning_fast", False ) async def healthcheck_job( self, - hash: str, + service: str, ) -> None: - """Start a background funding job.""" + """Start a background health check job.""" try: - service = hash - self.logger.info( f"[HEALTCHECKER] Start healthcheck job for service: {service}" ) - async def _wait_for_port(sleep_period=15): + async def _wait_for_port(sleep_period: int = 15) -> None: self.logger.info("[HEALTCHECKER]: wait port is up") while True: try: @@ -82,7 +104,9 @@ async def _wait_for_port(sleep_period=15): self.logger.error("[HEALTCHECKER]: error connecting http port") await asyncio.sleep(sleep_period) - async def _check_port_ready(timeout=self.PORT_UP_TIMEOUT, sleep_period=15): + async def _check_port_ready( + timeout: int = self.PORT_UP_TIMEOUT, sleep_period: int = 15 + ) -> bool: try: await asyncio.wait_for( _wait_for_port(sleep_period=sleep_period), timeout=timeout @@ -91,54 +115,69 @@ async def _check_port_ready(timeout=self.PORT_UP_TIMEOUT, sleep_period=15): except asyncio.TimeoutError: return False - async def _check_health(number_of_fails=5, sleep_period=self.SLEEP_PERIOD): + async def _check_health( + number_of_fails: int = 5, sleep_period: int = self.SLEEP_PERIOD + ) -> None: fails = 0 while True: try: # Check the service health healthy = await self.check_service_health(service) except aiohttp.ClientConnectionError: - self.logger.info("[HEALTCHECKER] port read failed. restart") + self.logger.info( + f"[HEALTCHECKER] {service} port read failed. restart" + ) return - self.logger.info(f"[HEALTCHECKER] is HEALTHY") if not healthy: fails += 1 self.logger.info( - f"[HEALTCHECKER] not healthy for {fails} time in a row" + f"[HEALTCHECKER] {service} not healthy for {fails} time in a row" ) else: + self.logger.info(f"[HEALTCHECKER] {service} is HEALTHY") # reset fails if comes healty fails = 0 if fails >= number_of_fails: # too much fails, exit self.logger.error( - f"[HEALTCHECKER] failed {fails} times in a row. restart" + f"[HEALTCHECKER] {service} failed {fails} times in a row. restart" ) return await asyncio.sleep(sleep_period) - async def _restart(service_manager, service): - service_manager.stop_service_locally(hash=service) - service_manager.deploy_service_locally(hash=service) + async def _restart(service_manager: ServiceManager, service: str) -> None: + def _do_restart() -> None: + service_manager.stop_service_locally(hash=service) + service_manager.deploy_service_locally(hash=service) + + loop = asyncio.get_event_loop() + with ThreadPoolExecutor() as executor: + future = loop.run_in_executor(executor, _do_restart) + await future + exception = future.exception() + if exception is not None: + raise exception # upper cycle while True: - self.logger.info("[HEALTCHECKER] wait for port ready") - if not (await _check_port_ready(timeout=self.PORT_UP_TIMEOUT)): + self.logger.info(f"[HEALTCHECKER] {service} wait for port ready") + if await _check_port_ready(timeout=self.PORT_UP_TIMEOUT): + # blocking till restart needed self.logger.info( - "[HEALTCHECKER] port not ready within timeout. restart deploymen" + f"[HEALTCHECKER] {service} port is ready, checking health every {self.SLEEP_PERIOD}" ) + await _check_health(sleep_period=self.SLEEP_PERIOD) + else: - # blocking till restart needed self.logger.info( - f"[HEALTCHECKER] port is ready, checking health every {self.SLEEP_PERIOD}" + "[HEALTCHECKER] port not ready within timeout. restart deployment" ) - await _check_health(sleep_period=self.SLEEP_PERIOD) # perform restart # TODO: blocking!!!!!!! await _restart(self._service_manager, service) - except Exception as e: - self.logger.exception("oops") + except Exception: + self.logger.exception(f"problems running healthcheckr for {service}") + raise From bd393eecd91cbeb20b8a1e7aa3c106df917cb8fb Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Wed, 17 Jul 2024 13:43:54 +0300 Subject: [PATCH 03/19] version bump --- electron/install.js | 2 +- package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electron/install.js b/electron/install.js index ba13ac9f8..54d6b2c2e 100644 --- a/electron/install.js +++ b/electron/install.js @@ -13,7 +13,7 @@ const { paths } = require('./constants'); * - use "" (nothing as a suffix) for latest release candidate, for example "0.1.0rc26" * - use "alpha" for alpha release, for example "0.1.0rc26-alpha" */ -const OlasMiddlewareVersion = '0.1.0rc71'; +const OlasMiddlewareVersion = '0.1.0rc71-hc-test-1'; const Env = { ...process.env, diff --git a/package.json b/package.json index e41167849..8ddab125a 100644 --- a/package.json +++ b/package.json @@ -56,5 +56,5 @@ "start:frontend": "cd frontend && yarn start", "test:frontend": "cd frontend && yarn test" }, - "version": "0.1.0-rc71" + "version": "0.1.0-rc71-hc-test-1" } diff --git a/pyproject.toml b/pyproject.toml index 8ac78d250..caed51245 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "olas-operate-middleware" -version = "0.1.0-rc71" +version = "0.1.0-rc71-hc-test-1" description = "" authors = ["David Vilela ", "Viraj Patel "] readme = "README.md" From f89882cea10caea3f4a0e83f8b0572e8c84809ae Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Wed, 17 Jul 2024 13:45:53 +0300 Subject: [PATCH 04/19] version bump --- electron/install.js | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electron/install.js b/electron/install.js index 54d6b2c2e..ba13ac9f8 100644 --- a/electron/install.js +++ b/electron/install.js @@ -13,7 +13,7 @@ const { paths } = require('./constants'); * - use "" (nothing as a suffix) for latest release candidate, for example "0.1.0rc26" * - use "alpha" for alpha release, for example "0.1.0rc26-alpha" */ -const OlasMiddlewareVersion = '0.1.0rc71-hc-test-1'; +const OlasMiddlewareVersion = '0.1.0rc71'; const Env = { ...process.env, diff --git a/pyproject.toml b/pyproject.toml index caed51245..8ac78d250 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "olas-operate-middleware" -version = "0.1.0-rc71-hc-test-1" +version = "0.1.0-rc71" description = "" authors = ["David Vilela ", "Viraj Patel "] readme = "README.md" From 699ba3855739ca75f47a6fc80e143824b3c9e6d6 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Wed, 17 Jul 2024 13:55:15 +0300 Subject: [PATCH 05/19] version bump --- electron/install.js | 2 +- package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electron/install.js b/electron/install.js index ba13ac9f8..85b6fa587 100644 --- a/electron/install.js +++ b/electron/install.js @@ -13,7 +13,7 @@ const { paths } = require('./constants'); * - use "" (nothing as a suffix) for latest release candidate, for example "0.1.0rc26" * - use "alpha" for alpha release, for example "0.1.0rc26-alpha" */ -const OlasMiddlewareVersion = '0.1.0rc71'; +const OlasMiddlewareVersion = '0.1.0rc73'; const Env = { ...process.env, diff --git a/package.json b/package.json index 8ddab125a..2ad85e89a 100644 --- a/package.json +++ b/package.json @@ -56,5 +56,5 @@ "start:frontend": "cd frontend && yarn start", "test:frontend": "cd frontend && yarn test" }, - "version": "0.1.0-rc71-hc-test-1" + "version": "0.1.0-rc73" } diff --git a/pyproject.toml b/pyproject.toml index 8ac78d250..77a460d42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "olas-operate-middleware" -version = "0.1.0-rc71" +version = "0.1.0-rc73" description = "" authors = ["David Vilela ", "Viraj Patel "] readme = "README.md" From 279ebf9b49cb138fcc95b1f0f4d4f6efcb754ad0 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Wed, 17 Jul 2024 13:56:52 +0300 Subject: [PATCH 06/19] version bump --- electron/install.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/install.js b/electron/install.js index 85b6fa587..3c1436b47 100644 --- a/electron/install.js +++ b/electron/install.js @@ -13,7 +13,7 @@ const { paths } = require('./constants'); * - use "" (nothing as a suffix) for latest release candidate, for example "0.1.0rc26" * - use "alpha" for alpha release, for example "0.1.0rc26-alpha" */ -const OlasMiddlewareVersion = '0.1.0rc73'; +const OlasMiddlewareVersion = '0.1.0rc74'; const Env = { ...process.env, From b3162a3456b11d0aacacda07192f16c3049d7ce4 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Wed, 17 Jul 2024 14:32:40 +0300 Subject: [PATCH 07/19] version bump --- electron/install.js | 2 +- package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electron/install.js b/electron/install.js index 3c1436b47..c6356214e 100644 --- a/electron/install.js +++ b/electron/install.js @@ -13,7 +13,7 @@ const { paths } = require('./constants'); * - use "" (nothing as a suffix) for latest release candidate, for example "0.1.0rc26" * - use "alpha" for alpha release, for example "0.1.0rc26-alpha" */ -const OlasMiddlewareVersion = '0.1.0rc74'; +const OlasMiddlewareVersion = '0.1.0rc75'; const Env = { ...process.env, diff --git a/package.json b/package.json index 2ad85e89a..8f2d1deef 100644 --- a/package.json +++ b/package.json @@ -56,5 +56,5 @@ "start:frontend": "cd frontend && yarn start", "test:frontend": "cd frontend && yarn test" }, - "version": "0.1.0-rc73" + "version": "0.1.0-rc75" } diff --git a/pyproject.toml b/pyproject.toml index 77a460d42..746f8b62d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "olas-operate-middleware" -version = "0.1.0-rc73" +version = "0.1.0-rc75" description = "" authors = ["David Vilela ", "Viraj Patel "] readme = "README.md" From 20bc26cd9480e3e80a3d422f544c1cfa8c396d96 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Mon, 22 Jul 2024 15:02:36 +0300 Subject: [PATCH 08/19] fix for blocking call in async endpoints --- operate/cli.py | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/operate/cli.py b/operate/cli.py index 07f87227b..818b497af 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -20,6 +20,7 @@ """Operate app CLI module.""" import asyncio +from concurrent.futures import ThreadPoolExecutor import logging import os import signal @@ -153,6 +154,17 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st shutdown_endpoint ) + thread_pool_executor = ThreadPoolExecutor() + + async def run_in_executor(fn, *args): + loop = asyncio.get_event_loop() + future = loop.run_in_executor(thread_pool_executor, fn, *args) + res = await future + exception = future.exception() + if exception is not None: + raise exception + return res + def schedule_funding_job( service: str, from_safe: bool = True, @@ -528,10 +540,12 @@ async def _create_services(request: Request) -> JSONResponse: ) if template.get("deploy", False): - manager.deploy_service_onchain_from_safe(hash=service.hash, update=update) - manager.stake_service_on_chain_from_safe(hash=service.hash) - manager.fund_service(hash=service.hash) - manager.deploy_service_locally(hash=service.hash) + def _fn(): + manager.deploy_service_onchain_from_safe(hash=service.hash, update=update) + manager.stake_service_on_chain_from_safe(hash=service.hash) + manager.fund_service(hash=service.hash) + manager.deploy_service_locally(hash=service.hash) + await run_in_executor(_fn) schedule_funding_job(service=service.hash) schedule_healthcheck_job(service=service.hash) @@ -651,7 +665,11 @@ async def _build_service_locally(request: Request) -> JSONResponse: ) .deployment ) - deployment.build(force=True) + + def _fn(): + deployment.build(force=True) + + await run_in_executor(_fn) return JSONResponse(content=deployment.json) @app.post("/api/services/{service}/deployment/start") @@ -662,10 +680,14 @@ async def _start_service_locally(request: Request) -> JSONResponse: return service_not_found_error(service=request.path_params["service"]) service = request.path_params["service"] manager = operate.service_manager() - manager.deploy_service_onchain(hash=service) - manager.stake_service_on_chain(hash=service) - manager.fund_service(hash=service) - manager.deploy_service_locally(hash=service, force=True) + + def _fn(): + manager.deploy_service_onchain(hash=service) + manager.stake_service_on_chain(hash=service) + manager.fund_service(hash=service) + manager.deploy_service_locally(hash=service, force=True) + + await run_in_executor(_fn) schedule_funding_job(service=service) schedule_healthcheck_job(service=service.hash) return JSONResponse(content=manager.load_or_create(service).deployment) @@ -679,7 +701,8 @@ async def _stop_service_locally(request: Request) -> JSONResponse: service = request.path_params["service"] deployment = operate.service_manager().load_or_create(service).deployment health_checker.stop_for_service(service=service) - deployment.stop() + + await run_in_executor(deployment.stop) logger.info(f"Cancelling funding job for {service}") cancel_funding_job(service=service) return JSONResponse(content=deployment.json) From 94e7a903ef2366fc9be0671b2e5d8f57a77440f2 Mon Sep 17 00:00:00 2001 From: truemiller Date: Tue, 23 Jul 2024 15:32:13 +0100 Subject: [PATCH 09/19] feat: Refactor CannotStartAgent component to use updated isEligibleForStaking variable --- .../Main/MainHeader/CannotStartAgent.tsx | 2 +- frontend/components/Main/MainHeader/index.tsx | 11 +-- .../context/StakingContractInfoProvider.tsx | 85 +++---------------- frontend/hooks/useBalance.ts | 32 +------ frontend/hooks/useStakingContractInfo.ts | 69 ++++++++++++++- 5 files changed, 81 insertions(+), 118 deletions(-) diff --git a/frontend/components/Main/MainHeader/CannotStartAgent.tsx b/frontend/components/Main/MainHeader/CannotStartAgent.tsx index 5cfc0a6d7..b28bf05ec 100644 --- a/frontend/components/Main/MainHeader/CannotStartAgent.tsx +++ b/frontend/components/Main/MainHeader/CannotStartAgent.tsx @@ -67,7 +67,7 @@ const NoJobsAvailablePopover = () => ( export const CannotStartAgent = () => { const { - isEligibleForStakingAction, + isEligibleForStaking: isEligibleForStakingAction, hasEnoughServiceSlots, isRewardsAvailable, isAgentEvicted, diff --git a/frontend/components/Main/MainHeader/index.tsx b/frontend/components/Main/MainHeader/index.tsx index 8942363f8..9990fd68a 100644 --- a/frontend/components/Main/MainHeader/index.tsx +++ b/frontend/components/Main/MainHeader/index.tsx @@ -94,11 +94,8 @@ export const MainHeader = () => { const [isModalOpen, setIsModalOpen] = useState(false); const handleModalClose = useCallback(() => setIsModalOpen(false), []); - const { - isInitialStakingLoad, - isEligibleForStakingAction, - canStartEvictedAgent, - } = useStakingContractInfo(); + const { isInitialStakingLoad, isEligibleForStaking, canStartEvictedAgent } = + useStakingContractInfo(); // hook to setup tray icon useSetupTrayIcon(); @@ -247,7 +244,7 @@ export const MainHeader = () => { ); } - if (!isEligibleForStakingAction) return ; + if (!isEligibleForStaking) return ; if (!isBalanceLoaded) { return ( @@ -301,7 +298,7 @@ export const MainHeader = () => { services, storeState?.isInitialFunded, totalEthBalance, - isEligibleForStakingAction, + isEligibleForStaking, canStartEvictedAgent, ]); diff --git a/frontend/context/StakingContractInfoProvider.tsx b/frontend/context/StakingContractInfoProvider.tsx index dc2b65c2c..20d56ac2f 100644 --- a/frontend/context/StakingContractInfoProvider.tsx +++ b/frontend/context/StakingContractInfoProvider.tsx @@ -10,28 +10,19 @@ import { useInterval } from 'usehooks-ts'; import { FIVE_SECONDS_INTERVAL } from '@/constants/intervals'; import { AutonolasService } from '@/service/Autonolas'; +import { StakingContractInfo } from '@/types/Autonolas'; import { ServicesContext } from './ServicesProvider'; type StakingContractInfoContextProps = { updateStakingContractInfo: () => Promise; - isInitialStakingLoad: boolean; - isRewardsAvailable: boolean; - hasEnoughServiceSlots: boolean; - isAgentEvicted: boolean; - isEligibleForStakingAction: boolean; - canStartEvictedAgent: boolean; + stakingContractInfo?: StakingContractInfo; }; export const StakingContractInfoContext = createContext({ updateStakingContractInfo: async () => {}, - isInitialStakingLoad: true, - isRewardsAvailable: false, - hasEnoughServiceSlots: false, - isAgentEvicted: false, - isEligibleForStakingAction: false, - canStartEvictedAgent: false, + stakingContractInfo: undefined, }); export const StakingContractInfoProvider = ({ @@ -40,67 +31,16 @@ export const StakingContractInfoProvider = ({ const { services } = useContext(ServicesContext); const serviceId = useMemo(() => services?.[0]?.chain_data?.token, [services]); - const [stakingLoadCount, setStakingLoadCount] = useState(0); - const [isRewardsAvailable, setIsRewardsAvailable] = useState(false); - const [hasEnoughServiceSlots, setHasEnoughServiceSlots] = useState(false); - const [isAgentEvicted, setIsAgentEvicted] = useState(false); - const [isEligibleForStakingAction, setCanStartAgent] = useState(false); + const [stakingContractInfo, setStakingContractInfo] = + useState(); const updateStakingContractInfo = useCallback(async () => { - try { - if (!serviceId) return; + if (!serviceId) return; - const info = await AutonolasService.getStakingContractInfo(serviceId); - if (!info) return; + const info = await AutonolasService.getStakingContractInfo(serviceId); + if (!info) return; - const { - availableRewards, - maxNumServices, - serviceIds, - serviceStakingStartTime, - serviceStakingState, - minimumStakingDuration, - } = info; - const isRewardsAvailable = availableRewards > 0; - const hasEnoughServiceSlots = serviceIds.length < maxNumServices; - const hasEnoughRewardsAndSlots = - isRewardsAvailable && hasEnoughServiceSlots; - - const isAgentEvicted = serviceStakingState === 2; - - /** - * For example: minStakingDuration = 3 days - * - * - Service starts staking 1st June 00:01 - * - Service stops being active on 1st June 02:01 (after 2 hours) - * - Contract will evict the service at 3rd June 02:02 - * - Now, cannot unstake the service until 4th June 00:01, because it hasn’t met the minStakingDuration of 3 days. - * - IMPORTANT: Between 3rd June 02:02 and 4th June 00:01 the service is EVICTED and without the possibility of unstake and re-stake - * - That is, user should not be able to run/start your agent if this condition is met. - * - */ - const isServiceStakedForMinimumDuration = - Math.round(Date.now() / 1000) - serviceStakingStartTime >= - minimumStakingDuration; - - /** - * user can start the agent iff, - * - rewards are available - * - service has enough slots - * - if agent is evicted, then service should be staked for minimum duration - */ - const isEligibleForStakingAction = - hasEnoughRewardsAndSlots && - (isAgentEvicted ? isServiceStakedForMinimumDuration : true); - - setIsRewardsAvailable(isRewardsAvailable); - setHasEnoughServiceSlots(hasEnoughServiceSlots); - setCanStartAgent(isEligibleForStakingAction); - setIsAgentEvicted(isAgentEvicted); - setStakingLoadCount((prev) => prev + 1); - } catch (error) { - console.error('Failed to fetch staking contract info', error); - } + setStakingContractInfo(info); }, [serviceId]); useInterval(updateStakingContractInfo, FIVE_SECONDS_INTERVAL); @@ -109,12 +49,7 @@ export const StakingContractInfoProvider = ({ {children} diff --git a/frontend/hooks/useBalance.ts b/frontend/hooks/useBalance.ts index 4132572a1..c8d21df91 100644 --- a/frontend/hooks/useBalance.ts +++ b/frontend/hooks/useBalance.ts @@ -2,34 +2,4 @@ import { useContext } from 'react'; import { BalanceContext } from '@/context/BalanceProvider'; -export const useBalance = () => { - const { - isLoaded, - setIsLoaded, - isBalanceLoaded, - eoaBalance, - safeBalance, - totalEthBalance, - totalOlasBalance, - wallets, - walletBalances, - updateBalances, - setIsPaused, - totalOlasStakedBalance, - } = useContext(BalanceContext); - - return { - isLoaded, - setIsLoaded, - isBalanceLoaded, - eoaBalance, - safeBalance, - totalEthBalance, - totalOlasBalance, - wallets, - walletBalances, - updateBalances, - setIsPaused, - totalOlasStakedBalance, - }; -}; +export const useBalance = () => useContext(BalanceContext); diff --git a/frontend/hooks/useStakingContractInfo.ts b/frontend/hooks/useStakingContractInfo.ts index e3fc10c91..91e6a11ee 100644 --- a/frontend/hooks/useStakingContractInfo.ts +++ b/frontend/hooks/useStakingContractInfo.ts @@ -1,6 +1,67 @@ -import { useContext } from 'react'; +import { ServicesContext } from "@/context/ServicesProvider"; +import { StakingContractInfoContext } from "@/context/StakingContractInfoProvider"; +import { AutonolasService } from "@/service/Autonolas"; +import { StakingContractInfo } from "@/types/Autonolas"; +import _ from "lodash"; +import { useContext } from "react"; -import { StakingContractInfoContext } from '@/context/StakingContractInfoProvider'; +export const useStakingContractInfo = () => { + const { stakingContractInfo } = useContext(StakingContractInfoContext); + const { services } = useContext(ServicesContext); -export const useStakingContractInfo = () => - useContext(StakingContractInfoContext); + if (!services) return {}; + if (!stakingContractInfo) return {}; + + const { serviceStakingState, serviceStakingStartTime, availableRewards, serviceIds, maxNumServices, minimumStakingDuration } = stakingContractInfo; + + + const isRewardsAvailable = availableRewards > 0; + const hasEnoughServiceSlots = serviceIds.length < maxNumServices; + const hasEnoughRewardsAndSlots = + isRewardsAvailable && hasEnoughServiceSlots; + + const isAgentEvicted = serviceStakingState === 2; + + if (!serviceId) return; + + const info = await AutonolasService.getStakingContractInfo(serviceId); + if (!info) return; + + const = info; + + + + /** + * For example: minStakingDuration = 3 days + * + * - Service starts staking 1st June 00:01 + * - Service stops being active on 1st June 02:01 (after 2 hours) + * - Contract will evict the service at 3rd June 02:02 + * - Now, cannot unstake the service until 4th June 00:01, because it hasn’t met the minStakingDuration of 3 days. + * - IMPORTANT: Between 3rd June 02:02 and 4th June 00:01 the service is EVICTED and without the possibility of unstake and re-stake + * - That is, user should not be able to run/start your agent if this condition is met. + * + */ + const isServiceStakedForMinimumDuration = + Math.round(Date.now() / 1000) - serviceStakingStartTime >= + minimumStakingDuration; + + /** + * user can start the agent iff, + * - rewards are available + * - service has enough slots + * - if agent is evicted, then service should be staked for minimum duration + */ + const isEligibleForStaking = + hasEnoughRewardsAndSlots && + (isAgentEvicted ? isServiceStakedForMinimumDuration : true); + + setIsRewardsAvailable(isRewardsAvailable); + setHasEnoughServiceSlots(hasEnoughServiceSlots); + setIsEligibleForStaking(isEligibleForStaking); + setIsAgentEvicted(isAgentEvicted); + } catch (error) { + console.error('Failed to fetch staking contract info', error); + } + }, [serviceId]); +}; From 5a99a926b7734efd96afd2260a418789f2ee8536 Mon Sep 17 00:00:00 2001 From: truemiller Date: Wed, 24 Jul 2024 16:32:35 +0100 Subject: [PATCH 10/19] Refactored main header components --- .../Main/MainHeader/AgentButton/index.tsx | 220 +++++++++++++ .../Main/MainHeader/AgentHead/index.tsx | 36 +++ .../Main/MainHeader/CannotStartAgent.tsx | 4 +- .../{constants.tsx => constants.ts} | 2 + frontend/components/Main/MainHeader/index.tsx | 291 +----------------- frontend/hooks/useServiceTemplates.ts | 1 + frontend/hooks/useServices.ts | 1 + frontend/hooks/useStakingContractInfo.ts | 102 +++--- frontend/utils/service.ts | 15 +- 9 files changed, 328 insertions(+), 344 deletions(-) create mode 100644 frontend/components/Main/MainHeader/AgentButton/index.tsx create mode 100644 frontend/components/Main/MainHeader/AgentHead/index.tsx rename frontend/components/Main/MainHeader/{constants.tsx => constants.ts} (89%) diff --git a/frontend/components/Main/MainHeader/AgentButton/index.tsx b/frontend/components/Main/MainHeader/AgentButton/index.tsx new file mode 100644 index 000000000..347db2bc2 --- /dev/null +++ b/frontend/components/Main/MainHeader/AgentButton/index.tsx @@ -0,0 +1,220 @@ +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Button, ButtonProps, Flex, Popover, Typography } from 'antd'; +import { useCallback, useMemo } from 'react'; + +import { Chain, DeploymentStatus } from '@/client'; +import { COLOR } from '@/constants/colors'; +import { useBalance } from '@/hooks/useBalance'; +import { useElectronApi } from '@/hooks/useElectronApi'; +import { useServices } from '@/hooks/useServices'; +import { useServiceTemplates } from '@/hooks/useServiceTemplates'; +import { useStakingContractInfo } from '@/hooks/useStakingContractInfo'; +import { useStore } from '@/hooks/useStore'; +import { useWallet } from '@/hooks/useWallet'; +import { ServicesService } from '@/service/Services'; +import { WalletService } from '@/service/Wallet'; +import { getMinimumStakedAmountRequired } from '@/utils/service'; + +import { CannotStartAgent } from '../CannotStartAgent'; +import { requiredGas, requiredOlas } from '../constants'; + +const { Text } = Typography; + +const LOADING_MESSAGE = + "Starting the agent may take a while, so feel free to minimize the app. We'll notify you once it's running. Please, don't quit the app."; + +const AgentStartingButton = () => ( + + + + + {LOADING_MESSAGE} + + } + > + + +); + +const AgentStoppingButton = () => ( + +); + +const AgentRunningButton = () => { + const { service } = useServices(); + + const handlePause = useCallback(async () => { + if (!service) return; + ServicesService.stopDeployment(service.hash); + }, [service]); + + return ( + + + + Agent is working + + + ); +}; + +const AgentNotRunningButton = () => { + const { wallets, masterSafeAddress } = useWallet(); + const { service, serviceStatus } = useServices(); + const { serviceTemplate } = useServiceTemplates(); + const { showNotification } = useElectronApi(); + const { + setIsPaused: setIsBalancePollingPaused, + safeBalance, + totalOlasStakedBalance, + totalEthBalance, + } = useBalance(); + const { storeState } = useStore(); + const { isEligibleForStaking, isAgentEvicted } = useStakingContractInfo(); + + const safeOlasBalance = safeBalance?.OLAS; + const safeOlasBalanceWithStaked = + safeOlasBalance === undefined || totalOlasStakedBalance === undefined + ? undefined + : safeOlasBalance + totalOlasStakedBalance; + + const handleStart = useCallback(async () => { + if (!wallets?.[0]) return; + + // Paused to stop confusing balance transitions while starting the agent + setIsBalancePollingPaused(true); + + try { + if (!masterSafeAddress) { + await WalletService.createSafe(Chain.GNOSIS); + } + + await ServicesService.createService({ + serviceTemplate, + deploy: true, + }); + + if (!service) { + showNotification?.('Your agent is now running!'); + } else { + const minimumStakedAmountRequired = + getMinimumStakedAmountRequired(serviceTemplate); + + showNotification?.( + `Your agent is running and you've staked ${minimumStakedAmountRequired} OLAS!`, + ); + + // setIsModalOpen(true); + } + } catch (error) { + console.error(error); + } finally { + setIsBalancePollingPaused(false); + } + }, [ + masterSafeAddress, + serviceTemplate, + service, + setIsBalancePollingPaused, + showNotification, + wallets, + ]); + + const isDeployable = useMemo(() => { + if (serviceStatus === DeploymentStatus.DEPLOYED) return false; + if (serviceStatus === DeploymentStatus.DEPLOYING) return false; + if (serviceStatus === DeploymentStatus.STOPPING) return false; + + // case where service exists & user has initial funded + if (service && storeState?.isInitialFunded) { + if (!safeOlasBalanceWithStaked) return false; + // at present agent will always require staked/bonded OLAS (or the ability to stake/bond) + return safeOlasBalanceWithStaked >= requiredOlas; + } + + // case if agent is evicted and user has met the staking criteria + if (isEligibleForStaking && isAgentEvicted) return true; + + const hasEnoughOlas = (safeOlasBalanceWithStaked ?? 0) >= requiredOlas; + const hasEnoughEth = (totalEthBalance ?? 0) > requiredGas; + + return hasEnoughOlas && hasEnoughEth; + }, [ + isAgentEvicted, + isEligibleForStaking, + safeOlasBalanceWithStaked, + service, + serviceStatus, + storeState?.isInitialFunded, + totalEthBalance, + ]); + + const buttonProps: ButtonProps = { + type: 'primary', + size: 'large', + disabled: !isDeployable, + onClick: isDeployable ? handleStart : undefined, + }; + + const buttonText = `Start agent ${service ? '' : '& stake'}`; + + return ; +}; + +export const AgentButton = () => { + const { service, serviceStatus, hasInitialLoaded } = useServices(); + const { isEligibleForStaking, isAgentEvicted } = useStakingContractInfo(); + + return useMemo(() => { + if (!hasInitialLoaded) { + return + ); + }, [ + hasInitialLoaded, + serviceStatus, + isEligibleForStaking, + isAgentEvicted, + service, + ]); +}; diff --git a/frontend/components/Main/MainHeader/AgentHead/index.tsx b/frontend/components/Main/MainHeader/AgentHead/index.tsx new file mode 100644 index 000000000..a291ef4f3 --- /dev/null +++ b/frontend/components/Main/MainHeader/AgentHead/index.tsx @@ -0,0 +1,36 @@ +import { Badge } from 'antd'; +import Image from 'next/image'; + +import { DeploymentStatus } from '@/client'; +import { useServices } from '@/hooks/useServices'; + +const badgeOffset: [number, number] = [-5, 32.5]; + +const TransitionalAgentHead = () => ( + + Happy Robot + +); + +const DeployedAgentHead = () => ( + + Happy Robot + +); + +const StoppedAgentHead = () => ( + + Sad Robot + +); + +export const AgentHead = () => { + const { serviceStatus } = useServices(); + if ( + serviceStatus === DeploymentStatus.DEPLOYING || + serviceStatus === DeploymentStatus.STOPPING + ) + return ; + if (serviceStatus === DeploymentStatus.DEPLOYED) return ; + return ; +}; diff --git a/frontend/components/Main/MainHeader/CannotStartAgent.tsx b/frontend/components/Main/MainHeader/CannotStartAgent.tsx index b28bf05ec..9206d6fa0 100644 --- a/frontend/components/Main/MainHeader/CannotStartAgent.tsx +++ b/frontend/components/Main/MainHeader/CannotStartAgent.tsx @@ -67,13 +67,13 @@ const NoJobsAvailablePopover = () => ( export const CannotStartAgent = () => { const { - isEligibleForStaking: isEligibleForStakingAction, + isEligibleForStaking, hasEnoughServiceSlots, isRewardsAvailable, isAgentEvicted, } = useStakingContractInfo(); - if (isEligibleForStakingAction) return null; + if (isEligibleForStaking) return null; if (!hasEnoughServiceSlots) return ; if (!isRewardsAvailable) return ; if (isAgentEvicted) return ; diff --git a/frontend/components/Main/MainHeader/constants.tsx b/frontend/components/Main/MainHeader/constants.ts similarity index 89% rename from frontend/components/Main/MainHeader/constants.tsx rename to frontend/components/Main/MainHeader/constants.ts index 1487211ce..87083563d 100644 --- a/frontend/components/Main/MainHeader/constants.tsx +++ b/frontend/components/Main/MainHeader/constants.ts @@ -2,6 +2,8 @@ import { formatUnits } from 'ethers/lib/utils'; import { SERVICE_TEMPLATES } from '@/constants/serviceTemplates'; +// TODO: Move this to more appropriate location (root /constants) + const olasCostOfBond = Number( formatUnits(`${SERVICE_TEMPLATES[0].configuration.olas_cost_of_bond}`, 18), ); diff --git a/frontend/components/Main/MainHeader/index.tsx b/frontend/components/Main/MainHeader/index.tsx index 9990fd68a..c24b0d430 100644 --- a/frontend/components/Main/MainHeader/index.tsx +++ b/frontend/components/Main/MainHeader/index.tsx @@ -1,64 +1,16 @@ -import { InfoCircleOutlined } from '@ant-design/icons'; -import { - Badge, - Button, - ButtonProps, - Flex, - Popover, - Skeleton, - Typography, -} from 'antd'; -import Image from 'next/image'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Flex } from 'antd'; +import { useCallback, useEffect, useState } from 'react'; -import { Chain, DeploymentStatus } from '@/client'; -import { COLOR } from '@/constants/colors'; +import { DeploymentStatus } from '@/client'; import { LOW_BALANCE } from '@/constants/thresholds'; import { useBalance } from '@/hooks/useBalance'; import { useElectronApi } from '@/hooks/useElectronApi'; import { useServices } from '@/hooks/useServices'; -import { useServiceTemplates } from '@/hooks/useServiceTemplates'; -import { useStakingContractInfo } from '@/hooks/useStakingContractInfo'; -import { useStore } from '@/hooks/useStore'; -import { useWallet } from '@/hooks/useWallet'; -import { ServicesService } from '@/service/Services'; -import { WalletService } from '@/service/Wallet'; -import { getMinimumStakedAmountRequired } from '@/utils/service'; -import { CannotStartAgent } from './CannotStartAgent'; -import { requiredGas, requiredOlas } from './constants'; +import { AgentButton } from './AgentButton'; +import { AgentHead } from './AgentHead'; import { FirstRunModal } from './FirstRunModal'; -const { Text } = Typography; - -const LOADING_MESSAGE = - 'Starting the agent may take a while, so feel free to minimize the app. We’ll notify you once it’s running. Please, don’t quit the app.'; -const StartingButtonPopover = () => ( - - - - - {LOADING_MESSAGE} - - } - > - - -); - -enum ServiceButtonLoadingState { - Starting, - Pausing, - NotLoading, -} - const useSetupTrayIcon = () => { const { safeBalance } = useBalance(); const { serviceStatus } = useServices(); @@ -78,239 +30,16 @@ const useSetupTrayIcon = () => { }; export const MainHeader = () => { - const { storeState } = useStore(); - const { services, serviceStatus, setServiceStatus } = useServices(); - const { showNotification } = useElectronApi(); - const { getServiceTemplates } = useServiceTemplates(); - const { wallets, masterSafeAddress } = useWallet(); - const { - safeBalance, - totalOlasStakedBalance, - totalEthBalance, - isBalanceLoaded, - setIsPaused: setIsBalancePollingPaused, - } = useBalance(); - - const [isModalOpen, setIsModalOpen] = useState(false); - const handleModalClose = useCallback(() => setIsModalOpen(false), []); + const [isFirstRunModalOpen, setIsFirstRunModalOpen] = useState(false); + const handleModalClose = useCallback(() => setIsFirstRunModalOpen(false), []); - const { isInitialStakingLoad, isEligibleForStaking, canStartEvictedAgent } = - useStakingContractInfo(); - - // hook to setup tray icon useSetupTrayIcon(); - const safeOlasBalanceWithStaked = useMemo(() => { - if (safeBalance?.OLAS === undefined) return; - if (totalOlasStakedBalance === undefined) return; - return totalOlasStakedBalance + safeBalance.OLAS; - }, [safeBalance?.OLAS, totalOlasStakedBalance]); - - const [serviceButtonState, setServiceButtonState] = - useState(ServiceButtonLoadingState.NotLoading); - - const serviceTemplate = useMemo( - () => getServiceTemplates()[0], - [getServiceTemplates], - ); - - const agentHead = useMemo(() => { - if ( - serviceButtonState === ServiceButtonLoadingState.Starting || - serviceButtonState === ServiceButtonLoadingState.Pausing - ) - return ( - - Happy Robot - - ); - if (serviceStatus === DeploymentStatus.DEPLOYED) - return ( - - Happy Robot - - ); - return ( - - Sad Robot - - ); - }, [serviceButtonState, serviceStatus]); - - const handleStart = useCallback(async () => { - if (!wallets?.[0]) return; - - setServiceButtonState(ServiceButtonLoadingState.Starting); - setIsBalancePollingPaused(true); - - try { - if (!masterSafeAddress) { - await WalletService.createSafe(Chain.GNOSIS); - } - // TODO: Replace with proper upload logic - // if (services.length > 0) { - // return ServicesService.startDeployment(services[0].hash).then(() => { - // setServiceStatus(DeploymentStatus.DEPLOYED); - // setIsBalancePollingPaused(false); - // setServiceButtonState({ isLoading: false }); - // }); - // } - - const serviceExists = !!services?.[0]; - - // For now POST /api/services will take care of creating, starting and updating the service - return ServicesService.createService({ - serviceTemplate, - deploy: true, - }) - .then(() => { - setServiceStatus(DeploymentStatus.DEPLOYED); - if (serviceExists) { - showNotification?.('Your agent is now running!'); - } else { - const minimumStakedAmountRequired = - getMinimumStakedAmountRequired(serviceTemplate); - - showNotification?.( - `Your agent is running and you've staked ${minimumStakedAmountRequired} OLAS!`, - ); - setIsModalOpen(true); - } - }) - .finally(() => { - setIsBalancePollingPaused(false); - setServiceButtonState(ServiceButtonLoadingState.NotLoading); - }); - } catch (error) { - setIsBalancePollingPaused(false); - setServiceButtonState(ServiceButtonLoadingState.NotLoading); - } - }, [ - masterSafeAddress, - serviceTemplate, - services, - setIsBalancePollingPaused, - setServiceStatus, - showNotification, - wallets, - ]); - - const handlePause = useCallback(() => { - if (!services) return; - if (services.length === 0) return; - setServiceButtonState(ServiceButtonLoadingState.Pausing); - ServicesService.stopDeployment(services[0].hash).then(() => { - setServiceStatus(DeploymentStatus.STOPPED); - setServiceButtonState(ServiceButtonLoadingState.NotLoading); - }); - }, [services, setServiceStatus]); - - const serviceToggleButton = useMemo(() => { - if (serviceButtonState === ServiceButtonLoadingState.Pausing) { - return ( - - ); - } - - if (serviceButtonState === ServiceButtonLoadingState.Starting) { - return ; - } - - if (serviceStatus === DeploymentStatus.DEPLOYED) { - return ( - - - - Agent is working - - - ); - } - - if (!isEligibleForStaking) return ; - - if (!isBalanceLoaded) { - return ( - - ); - } - - const isDeployable = (() => { - // case where required values are undefined (not fetched from the server) - if (totalEthBalance === undefined) return false; - if (safeOlasBalanceWithStaked === undefined) return false; - if (!services) return false; - - // deployment statuses where agent should not be deployed - // if (serviceStatus === DeploymentStatus.DEPLOYED) return false; // condition already checked above - if (serviceStatus === DeploymentStatus.DEPLOYING) return false; - if (serviceStatus === DeploymentStatus.STOPPING) return false; - - // case where service exists & user has initial funded - if (services[0] && storeState?.isInitialFunded) - return safeOlasBalanceWithStaked >= requiredOlas; // at present agent will always require staked/bonded OLAS (or the ability to stake/bond) - - // case if agent is evicted and user has met the staking criteria - if (canStartEvictedAgent) return true; - - return ( - safeOlasBalanceWithStaked >= requiredOlas && - totalEthBalance > requiredGas - ); - })(); - const serviceExists = !!services?.[0]; - - const buttonProps: ButtonProps = { - type: 'primary', - size: 'large', - disabled: !isDeployable, - onClick: isDeployable ? handleStart : undefined, - }; - const buttonText = `Start agent ${serviceExists ? '' : '& stake'}`; - - return ; - }, [ - handlePause, - handleStart, - isBalanceLoaded, - safeOlasBalanceWithStaked, - serviceButtonState, - serviceStatus, - services, - storeState?.isInitialFunded, - totalEthBalance, - isEligibleForStaking, - canStartEvictedAgent, - ]); - return ( - {agentHead} - {isInitialStakingLoad ? ( - - ) : ( - serviceToggleButton - )} - + + + ); }; diff --git a/frontend/hooks/useServiceTemplates.ts b/frontend/hooks/useServiceTemplates.ts index 0eb803f94..7e4e733b3 100644 --- a/frontend/hooks/useServiceTemplates.ts +++ b/frontend/hooks/useServiceTemplates.ts @@ -9,5 +9,6 @@ export const useServiceTemplates = () => { return { getServiceTemplate, getServiceTemplates, + serviceTemplate: SERVICE_TEMPLATES[0], // Default to the first template }; }; diff --git a/frontend/hooks/useServices.ts b/frontend/hooks/useServices.ts index d78a563fc..16b249e75 100644 --- a/frontend/hooks/useServices.ts +++ b/frontend/hooks/useServices.ts @@ -80,6 +80,7 @@ export const useServices = () => { setServices((prev) => prev?.filter((s) => s.hash !== serviceHash)); return { + service: services?.[0], services, serviceStatus, setServiceStatus, diff --git a/frontend/hooks/useStakingContractInfo.ts b/frontend/hooks/useStakingContractInfo.ts index 91e6a11ee..5392b7199 100644 --- a/frontend/hooks/useStakingContractInfo.ts +++ b/frontend/hooks/useStakingContractInfo.ts @@ -1,67 +1,59 @@ -import { ServicesContext } from "@/context/ServicesProvider"; -import { StakingContractInfoContext } from "@/context/StakingContractInfoProvider"; -import { AutonolasService } from "@/service/Autonolas"; -import { StakingContractInfo } from "@/types/Autonolas"; -import _ from "lodash"; -import { useContext } from "react"; +import { useContext } from 'react'; + +import { StakingContractInfoContext } from '@/context/StakingContractInfoProvider'; + +import { useServices } from './useServices'; export const useStakingContractInfo = () => { const { stakingContractInfo } = useContext(StakingContractInfoContext); - const { services } = useContext(ServicesContext); - - if (!services) return {}; - if (!stakingContractInfo) return {}; + const { services } = useServices(); - const { serviceStakingState, serviceStakingStartTime, availableRewards, serviceIds, maxNumServices, minimumStakingDuration } = stakingContractInfo; + if (!services?.[0] || !stakingContractInfo) return {}; + const { + serviceStakingState, + serviceStakingStartTime, + availableRewards, + serviceIds, + maxNumServices, + minimumStakingDuration, + } = stakingContractInfo; const isRewardsAvailable = availableRewards > 0; const hasEnoughServiceSlots = serviceIds.length < maxNumServices; - const hasEnoughRewardsAndSlots = - isRewardsAvailable && hasEnoughServiceSlots; + const hasEnoughRewardsAndSlots = isRewardsAvailable && hasEnoughServiceSlots; const isAgentEvicted = serviceStakingState === 2; - if (!serviceId) return; - - const info = await AutonolasService.getStakingContractInfo(serviceId); - if (!info) return; - - const = info; - - - - /** - * For example: minStakingDuration = 3 days - * - * - Service starts staking 1st June 00:01 - * - Service stops being active on 1st June 02:01 (after 2 hours) - * - Contract will evict the service at 3rd June 02:02 - * - Now, cannot unstake the service until 4th June 00:01, because it hasn’t met the minStakingDuration of 3 days. - * - IMPORTANT: Between 3rd June 02:02 and 4th June 00:01 the service is EVICTED and without the possibility of unstake and re-stake - * - That is, user should not be able to run/start your agent if this condition is met. - * - */ - const isServiceStakedForMinimumDuration = - Math.round(Date.now() / 1000) - serviceStakingStartTime >= - minimumStakingDuration; - - /** - * user can start the agent iff, - * - rewards are available - * - service has enough slots - * - if agent is evicted, then service should be staked for minimum duration - */ - const isEligibleForStaking = - hasEnoughRewardsAndSlots && - (isAgentEvicted ? isServiceStakedForMinimumDuration : true); - - setIsRewardsAvailable(isRewardsAvailable); - setHasEnoughServiceSlots(hasEnoughServiceSlots); - setIsEligibleForStaking(isEligibleForStaking); - setIsAgentEvicted(isAgentEvicted); - } catch (error) { - console.error('Failed to fetch staking contract info', error); - } - }, [serviceId]); + /** + * For example: minStakingDuration = 3 days + * + * - Service starts staking 1st June 00:01 + * - Service stops being active on 1st June 02:01 (after 2 hours) + * - Contract will evict the service at 3rd June 02:02 + * - Now, cannot unstake the service until 4th June 00:01, because it hasn’t met the minStakingDuration of 3 days. + * - IMPORTANT: Between 3rd June 02:02 and 4th June 00:01 the service is EVICTED and without the possibility of unstake and re-stake + * - That is, user should not be able to run/start your agent if this condition is met. + * + */ + const isServiceStakedForMinimumDuration = + Math.round(Date.now() / 1000) - serviceStakingStartTime >= + minimumStakingDuration; + + /** + * user can start the agent iff, + * - rewards are available + * - service has enough slots + * - if agent is evicted, then service should be staked for minimum duration + */ + const isEligibleForStaking = + hasEnoughRewardsAndSlots && + (isAgentEvicted ? isServiceStakedForMinimumDuration : true); + + return { + hasEnoughServiceSlots, + isEligibleForStaking, + isRewardsAvailable, + isAgentEvicted, + }; }; diff --git a/frontend/utils/service.ts b/frontend/utils/service.ts index 0b77f9f8f..160a9fee5 100644 --- a/frontend/utils/service.ts +++ b/frontend/utils/service.ts @@ -1,13 +1,16 @@ -import { ServiceTemplate } from "@/client"; -import { formatUnits } from "ethers/lib/utils"; +import { formatUnits } from 'ethers/lib/utils'; -export const getMinimumStakedAmountRequired = (serviceTemplate: ServiceTemplate) => { +import { ServiceTemplate } from '@/client'; + +export const getMinimumStakedAmountRequired = ( + serviceTemplate: ServiceTemplate, +) => { const olasCostOfBond = Number( - formatUnits(`${serviceTemplate.configuration.olas_cost_of_bond}`, 18) + formatUnits(`${serviceTemplate.configuration.olas_cost_of_bond}`, 18), ); const olasRequiredToStake = Number( - formatUnits(`${serviceTemplate.configuration.olas_required_to_stake}`, 18) + formatUnits(`${serviceTemplate.configuration.olas_required_to_stake}`, 18), ); return olasCostOfBond + olasRequiredToStake; -}; \ No newline at end of file +}; From eab7895d278952c6159dc0439f061069f64069ee Mon Sep 17 00:00:00 2001 From: truemiller Date: Thu, 25 Jul 2024 14:53:30 +0100 Subject: [PATCH 11/19] feat: added service polling pauses --- frontend/context/ServicesProvider.tsx | 6 +++++- frontend/hooks/useServices.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/context/ServicesProvider.tsx b/frontend/context/ServicesProvider.tsx index d11b4a0e4..8b44e2612 100644 --- a/frontend/context/ServicesProvider.tsx +++ b/frontend/context/ServicesProvider.tsx @@ -28,6 +28,7 @@ type ServicesContextProps = { updateServiceStatus: () => Promise; hasInitialLoaded: boolean; setHasInitialLoaded: Dispatch>; + setIsPaused: Dispatch>; }; export const ServicesContext = createContext({ @@ -40,6 +41,7 @@ export const ServicesContext = createContext({ updateServiceStatus: async () => {}, hasInitialLoaded: false, setHasInitialLoaded: () => {}, + setIsPaused: () => {}, }); export const ServicesProvider = ({ children }: PropsWithChildren) => { @@ -51,6 +53,7 @@ export const ServicesProvider = ({ children }: PropsWithChildren) => { DeploymentStatus | undefined >(); const [hasInitialLoaded, setHasInitialLoaded] = useState(false); + const [isPaused, setIsPaused] = useState(false); const serviceAddresses = useMemo( () => @@ -92,7 +95,7 @@ export const ServicesProvider = ({ children }: PropsWithChildren) => { updateServicesState() .then(() => updateServiceStatus()) .catch((e) => message.error(e.message)), - isOnline ? FIVE_SECONDS_INTERVAL : null, + isOnline && !isPaused ? FIVE_SECONDS_INTERVAL : null, ); return ( @@ -107,6 +110,7 @@ export const ServicesProvider = ({ children }: PropsWithChildren) => { serviceStatus, setServiceStatus, setHasInitialLoaded, + setIsPaused, }} > {children} diff --git a/frontend/hooks/useServices.ts b/frontend/hooks/useServices.ts index 16b249e75..2de306fcb 100644 --- a/frontend/hooks/useServices.ts +++ b/frontend/hooks/useServices.ts @@ -47,6 +47,7 @@ export const useServices = () => { serviceStatus, setServiceStatus, updateServiceStatus, + setIsPaused, } = useContext(ServicesContext); // STATE METHODS @@ -92,5 +93,6 @@ export const useServices = () => { updateServiceStatus, deleteServiceState, hasInitialLoaded, + setIsServicePollingPaused: setIsPaused, }; }; From f35f21923bad5bb5af4d0afee976ff3213322ba7 Mon Sep 17 00:00:00 2001 From: truemiller Date: Thu, 25 Jul 2024 14:53:53 +0100 Subject: [PATCH 12/19] fix: mock service status until completion, then restart polling --- .../Main/MainHeader/AgentButton/index.tsx | 71 ++++++++++++++++--- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/frontend/components/Main/MainHeader/AgentButton/index.tsx b/frontend/components/Main/MainHeader/AgentButton/index.tsx index 347db2bc2..886d45e18 100644 --- a/frontend/components/Main/MainHeader/AgentButton/index.tsx +++ b/frontend/components/Main/MainHeader/AgentButton/index.tsx @@ -50,12 +50,23 @@ const AgentStoppingButton = () => ( ); const AgentRunningButton = () => { - const { service } = useServices(); + const { showNotification } = useElectronApi(); + const { service, setIsServicePollingPaused, setServiceStatus } = + useServices(); const handlePause = useCallback(async () => { if (!service) return; - ServicesService.stopDeployment(service.hash); - }, [service]); + setIsServicePollingPaused(true); + setServiceStatus(DeploymentStatus.STOPPING); + try { + await ServicesService.stopDeployment(service.hash); + } catch (error) { + console.error(error); + showNotification?.('Error while stopping service'); + } finally { + setIsServicePollingPaused(false); + } + }, [service, setIsServicePollingPaused, setServiceStatus, showNotification]); return ( @@ -71,7 +82,12 @@ const AgentRunningButton = () => { const AgentNotRunningButton = () => { const { wallets, masterSafeAddress } = useWallet(); - const { service, serviceStatus } = useServices(); + const { + service, + serviceStatus, + setServiceStatus, + setIsServicePollingPaused, + } = useServices(); const { serviceTemplate } = useServiceTemplates(); const { showNotification } = useElectronApi(); const { @@ -90,21 +106,49 @@ const AgentNotRunningButton = () => { : safeOlasBalance + totalOlasStakedBalance; const handleStart = useCallback(async () => { + // Must have a wallet to start the agent if (!wallets?.[0]) return; + // Paused to stop overlapping service poll while wallet is created or service is built + setIsServicePollingPaused(true); + // Paused to stop confusing balance transitions while starting the agent setIsBalancePollingPaused(true); + // Mock "DEPLOYING" status (service polling will update this once resumed) + setServiceStatus(DeploymentStatus.DEPLOYING); + + // Create master safe if it doesn't exist try { if (!masterSafeAddress) { await WalletService.createSafe(Chain.GNOSIS); } + } catch (error) { + console.error(error); + setServiceStatus(undefined); + showNotification?.('Error while creating safe'); + setIsServicePollingPaused(false); + setIsBalancePollingPaused(false); + return; + } + // Then create / deploy the service + try { await ServicesService.createService({ serviceTemplate, deploy: true, }); + } catch (error) { + console.error(error); + setServiceStatus(undefined); + showNotification?.('Error while deploying service'); + setIsServicePollingPaused(false); + setIsBalancePollingPaused(false); + return; + } + // Show success notification based on whether there was a service prior to starting + try { if (!service) { showNotification?.('Your agent is now running!'); } else { @@ -114,21 +158,26 @@ const AgentNotRunningButton = () => { showNotification?.( `Your agent is running and you've staked ${minimumStakedAmountRequired} OLAS!`, ); - - // setIsModalOpen(true); } } catch (error) { console.error(error); - } finally { - setIsBalancePollingPaused(false); + showNotification?.('Error while showing "running" notification'); } + + // Can assume successful deployment + // resume polling, optimistically update service status (poll will update, if needed) + setIsServicePollingPaused(false); + setIsBalancePollingPaused(false); + setServiceStatus(DeploymentStatus.DEPLOYED); }, [ + wallets, + setIsServicePollingPaused, + setIsBalancePollingPaused, + setServiceStatus, masterSafeAddress, + showNotification, serviceTemplate, service, - setIsBalancePollingPaused, - showNotification, - wallets, ]); const isDeployable = useMemo(() => { From 7f7852d1c372a863e3eeb83288de1a1918b2c8d0 Mon Sep 17 00:00:00 2001 From: truemiller Date: Thu, 25 Jul 2024 14:55:03 +0100 Subject: [PATCH 13/19] chore: better commenting around agent pausing handler --- frontend/components/Main/MainHeader/AgentButton/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/components/Main/MainHeader/AgentButton/index.tsx b/frontend/components/Main/MainHeader/AgentButton/index.tsx index 886d45e18..33437068c 100644 --- a/frontend/components/Main/MainHeader/AgentButton/index.tsx +++ b/frontend/components/Main/MainHeader/AgentButton/index.tsx @@ -56,7 +56,10 @@ const AgentRunningButton = () => { const handlePause = useCallback(async () => { if (!service) return; + // Paused to stop overlapping service poll while waiting for response setIsServicePollingPaused(true); + + // Optimistically update service status setServiceStatus(DeploymentStatus.STOPPING); try { await ServicesService.stopDeployment(service.hash); @@ -64,6 +67,7 @@ const AgentRunningButton = () => { console.error(error); showNotification?.('Error while stopping service'); } finally { + // Resumt polling, will update to correct status regardless of success setIsServicePollingPaused(false); } }, [service, setIsServicePollingPaused, setServiceStatus, showNotification]); From 1247a72e7e57b9880d6ff99d1083cdc86e69c037 Mon Sep 17 00:00:00 2001 From: truemiller Date: Thu, 25 Jul 2024 15:46:14 +0100 Subject: [PATCH 14/19] chore: bump to rc90 --- electron/install.js | 2 +- package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electron/install.js b/electron/install.js index c6356214e..1e1a9e839 100644 --- a/electron/install.js +++ b/electron/install.js @@ -13,7 +13,7 @@ const { paths } = require('./constants'); * - use "" (nothing as a suffix) for latest release candidate, for example "0.1.0rc26" * - use "alpha" for alpha release, for example "0.1.0rc26-alpha" */ -const OlasMiddlewareVersion = '0.1.0rc75'; +const OlasMiddlewareVersion = '0.1.0rc90'; const Env = { ...process.env, diff --git a/package.json b/package.json index 8f2d1deef..ff88c414f 100644 --- a/package.json +++ b/package.json @@ -56,5 +56,5 @@ "start:frontend": "cd frontend && yarn start", "test:frontend": "cd frontend && yarn test" }, - "version": "0.1.0-rc75" + "version": "0.1.0-rc90" } diff --git a/pyproject.toml b/pyproject.toml index 746f8b62d..990ce3b81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "olas-operate-middleware" -version = "0.1.0-rc75" +version = "0.1.0-rc90" description = "" authors = ["David Vilela ", "Viraj Patel "] readme = "README.md" From 545ac3127c4186e150ddec7b5b7a78db0fc1df03 Mon Sep 17 00:00:00 2001 From: truemiller Date: Thu, 25 Jul 2024 20:05:27 +0100 Subject: [PATCH 15/19] chore: bump to 91 --- electron/install.js | 2 +- package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electron/install.js b/electron/install.js index 9d0dd8057..afa2f3e00 100644 --- a/electron/install.js +++ b/electron/install.js @@ -14,7 +14,7 @@ const { paths } = require('./constants'); * - use "" (nothing as a suffix) for latest release candidate, for example "0.1.0rc26" * - use "alpha" for alpha release, for example "0.1.0rc26-alpha" */ -const OlasMiddlewareVersion = '0.1.0rc90'; +const OlasMiddlewareVersion = '0.1.0rc91'; const Env = { ...process.env, diff --git a/package.json b/package.json index ff88c414f..e7b6c5634 100644 --- a/package.json +++ b/package.json @@ -56,5 +56,5 @@ "start:frontend": "cd frontend && yarn start", "test:frontend": "cd frontend && yarn test" }, - "version": "0.1.0-rc90" + "version": "0.1.0-rc91" } diff --git a/pyproject.toml b/pyproject.toml index 990ce3b81..90785a054 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "olas-operate-middleware" -version = "0.1.0-rc90" +version = "0.1.0-rc91" description = "" authors = ["David Vilela ", "Viraj Patel "] readme = "README.md" From 451d54a74f096f5785139bf943ff66fed95e8512 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Fri, 26 Jul 2024 13:29:23 +0300 Subject: [PATCH 16/19] lint fixes --- operate/cli.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/operate/cli.py b/operate/cli.py index 818b497af..7d3292760 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -20,13 +20,13 @@ """Operate app CLI module.""" import asyncio -from concurrent.futures import ThreadPoolExecutor import logging import os import signal import traceback import typing as t import uuid +from concurrent.futures import ThreadPoolExecutor from pathlib import Path from aea.helpers.logging import setup_logger @@ -156,7 +156,7 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st thread_pool_executor = ThreadPoolExecutor() - async def run_in_executor(fn, *args): + async def run_in_executor(fn: t.Callable, *args: t.Any) -> t.Any: loop = asyncio.get_event_loop() future = loop.run_in_executor(thread_pool_executor, fn, *args) res = await future @@ -540,11 +540,15 @@ async def _create_services(request: Request) -> JSONResponse: ) if template.get("deploy", False): - def _fn(): - manager.deploy_service_onchain_from_safe(hash=service.hash, update=update) + + def _fn() -> None: + manager.deploy_service_onchain_from_safe( + hash=service.hash, update=update + ) manager.stake_service_on_chain_from_safe(hash=service.hash) manager.fund_service(hash=service.hash) manager.deploy_service_locally(hash=service.hash) + await run_in_executor(_fn) schedule_funding_job(service=service.hash) schedule_healthcheck_job(service=service.hash) @@ -666,7 +670,7 @@ async def _build_service_locally(request: Request) -> JSONResponse: .deployment ) - def _fn(): + def _fn() -> None: deployment.build(force=True) await run_in_executor(_fn) @@ -681,7 +685,7 @@ async def _start_service_locally(request: Request) -> JSONResponse: service = request.path_params["service"] manager = operate.service_manager() - def _fn(): + def _fn() -> None: manager.deploy_service_onchain(hash=service) manager.stake_service_on_chain(hash=service) manager.fund_service(hash=service) From 7dfa9238149ccbadaef9da8288d10d7f6dd20f8c Mon Sep 17 00:00:00 2001 From: truemiller Date: Fri, 26 Jul 2024 15:33:15 +0100 Subject: [PATCH 17/19] chore: bump to rc92 for release on main --- electron/install.js | 2 +- package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electron/install.js b/electron/install.js index afa2f3e00..671a1578d 100644 --- a/electron/install.js +++ b/electron/install.js @@ -14,7 +14,7 @@ const { paths } = require('./constants'); * - use "" (nothing as a suffix) for latest release candidate, for example "0.1.0rc26" * - use "alpha" for alpha release, for example "0.1.0rc26-alpha" */ -const OlasMiddlewareVersion = '0.1.0rc91'; +const OlasMiddlewareVersion = '0.1.0rc92'; const Env = { ...process.env, diff --git a/package.json b/package.json index e7b6c5634..82c0054e3 100644 --- a/package.json +++ b/package.json @@ -56,5 +56,5 @@ "start:frontend": "cd frontend && yarn start", "test:frontend": "cd frontend && yarn test" }, - "version": "0.1.0-rc91" + "version": "0.1.0-rc92" } diff --git a/pyproject.toml b/pyproject.toml index 90785a054..42d8a8e3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "olas-operate-middleware" -version = "0.1.0-rc91" +version = "0.1.0-rc92" description = "" authors = ["David Vilela ", "Viraj Patel "] readme = "README.md" From c21e1f98f2ffc9439c8d0e9bc2b5e843c6048485 Mon Sep 17 00:00:00 2001 From: Josh Miller <31908788+truemiller@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:49:43 +0100 Subject: [PATCH 18/19] Update frontend/components/Main/MainHeader/AgentButton/index.tsx --- frontend/components/Main/MainHeader/AgentButton/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/Main/MainHeader/AgentButton/index.tsx b/frontend/components/Main/MainHeader/AgentButton/index.tsx index 33437068c..ba5db6ae7 100644 --- a/frontend/components/Main/MainHeader/AgentButton/index.tsx +++ b/frontend/components/Main/MainHeader/AgentButton/index.tsx @@ -65,7 +65,7 @@ const AgentRunningButton = () => { await ServicesService.stopDeployment(service.hash); } catch (error) { console.error(error); - showNotification?.('Error while stopping service'); + showNotification?.('Error while stopping agent'); } finally { // Resumt polling, will update to correct status regardless of success setIsServicePollingPaused(false); From 6f0895e7ad3737f6b4f769f4aa57a45822d34966 Mon Sep 17 00:00:00 2001 From: Josh Miller <31908788+truemiller@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:50:00 +0100 Subject: [PATCH 19/19] Update frontend/components/Main/MainHeader/AgentButton/index.tsx --- frontend/components/Main/MainHeader/AgentButton/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/Main/MainHeader/AgentButton/index.tsx b/frontend/components/Main/MainHeader/AgentButton/index.tsx index ba5db6ae7..cd6f72c59 100644 --- a/frontend/components/Main/MainHeader/AgentButton/index.tsx +++ b/frontend/components/Main/MainHeader/AgentButton/index.tsx @@ -67,7 +67,7 @@ const AgentRunningButton = () => { console.error(error); showNotification?.('Error while stopping agent'); } finally { - // Resumt polling, will update to correct status regardless of success + // Resume polling, will update to correct status regardless of success setIsServicePollingPaused(false); } }, [service, setIsServicePollingPaused, setServiceStatus, showNotification]);