olas-operate-middleware 0.10.19__py3-none-any.whl → 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/METADATA +3 -1
- {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/RECORD +30 -27
- operate/bridge/bridge_manager.py +10 -12
- operate/bridge/providers/lifi_provider.py +5 -4
- operate/bridge/providers/native_bridge_provider.py +6 -5
- operate/bridge/providers/provider.py +22 -87
- operate/bridge/providers/relay_provider.py +5 -4
- operate/cli.py +446 -168
- operate/constants.py +22 -2
- operate/keys.py +13 -0
- operate/ledger/__init__.py +107 -2
- operate/ledger/profiles.py +79 -11
- operate/operate_types.py +205 -2
- operate/quickstart/run_service.py +6 -10
- operate/services/agent_runner.py +5 -3
- operate/services/deployment_runner.py +3 -0
- operate/services/funding_manager.py +904 -0
- operate/services/health_checker.py +4 -4
- operate/services/manage.py +183 -310
- operate/services/protocol.py +392 -140
- operate/services/service.py +81 -5
- operate/settings.py +70 -0
- operate/utils/__init__.py +0 -29
- operate/utils/gnosis.py +79 -24
- operate/utils/single_instance.py +226 -0
- operate/wallet/master.py +221 -181
- operate/wallet/wallet_recovery_manager.py +5 -5
- {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/WHEEL +0 -0
- {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/entry_points.txt +0 -0
- {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/licenses/LICENSE +0 -0
operate/cli.py
CHANGED
|
@@ -22,6 +22,7 @@ import asyncio
|
|
|
22
22
|
import atexit
|
|
23
23
|
import multiprocessing
|
|
24
24
|
import os
|
|
25
|
+
import shutil
|
|
25
26
|
import signal
|
|
26
27
|
import traceback
|
|
27
28
|
import typing as t
|
|
@@ -30,14 +31,12 @@ from concurrent.futures import ThreadPoolExecutor
|
|
|
30
31
|
from contextlib import asynccontextmanager, suppress
|
|
31
32
|
from http import HTTPStatus
|
|
32
33
|
from pathlib import Path
|
|
34
|
+
from time import time
|
|
33
35
|
from types import FrameType
|
|
34
36
|
|
|
35
|
-
import
|
|
36
|
-
import requests
|
|
37
|
+
import autonomy.chain.tx
|
|
37
38
|
from aea.helpers.logging import setup_logger
|
|
38
39
|
from clea import group, params, run
|
|
39
|
-
from compose.project import ProjectError
|
|
40
|
-
from docker.errors import APIError
|
|
41
40
|
from fastapi import FastAPI, Request
|
|
42
41
|
from fastapi.middleware.cors import CORSMiddleware
|
|
43
42
|
from fastapi.responses import JSONResponse
|
|
@@ -49,22 +48,35 @@ from operate import __version__, services
|
|
|
49
48
|
from operate.account.user import UserAccount
|
|
50
49
|
from operate.bridge.bridge_manager import BridgeManager
|
|
51
50
|
from operate.constants import (
|
|
51
|
+
AGENT_RUNNER_PREFIX,
|
|
52
|
+
DEPLOYMENT_DIR,
|
|
52
53
|
KEYS_DIR,
|
|
53
54
|
MIN_PASSWORD_LENGTH,
|
|
55
|
+
MSG_INVALID_MNEMONIC,
|
|
56
|
+
MSG_INVALID_PASSWORD,
|
|
57
|
+
MSG_NEW_PASSWORD_MISSING,
|
|
58
|
+
OPERATE,
|
|
54
59
|
OPERATE_HOME,
|
|
55
60
|
SERVICES_DIR,
|
|
56
61
|
USER_JSON,
|
|
62
|
+
VERSION_FILE,
|
|
57
63
|
WALLETS_DIR,
|
|
58
64
|
WALLET_RECOVERY_DIR,
|
|
59
65
|
ZERO_ADDRESS,
|
|
60
66
|
)
|
|
61
67
|
from operate.ledger.profiles import (
|
|
62
|
-
|
|
68
|
+
DEFAULT_EOA_TOPUPS,
|
|
63
69
|
DEFAULT_NEW_SAFE_FUNDS,
|
|
64
70
|
ERC20_TOKENS,
|
|
65
71
|
)
|
|
66
72
|
from operate.migration import MigrationManager
|
|
67
|
-
from operate.operate_types import
|
|
73
|
+
from operate.operate_types import (
|
|
74
|
+
Chain,
|
|
75
|
+
ChainAmounts,
|
|
76
|
+
DeploymentStatus,
|
|
77
|
+
LedgerType,
|
|
78
|
+
Version,
|
|
79
|
+
)
|
|
68
80
|
from operate.quickstart.analyse_logs import analyse_logs
|
|
69
81
|
from operate.quickstart.claim_staking_rewards import claim_staking_rewards
|
|
70
82
|
from operate.quickstart.reset_configs import reset_configs
|
|
@@ -74,16 +86,38 @@ from operate.quickstart.run_service import run_service
|
|
|
74
86
|
from operate.quickstart.stop_service import stop_service
|
|
75
87
|
from operate.quickstart.terminate_on_chain_service import terminate_service
|
|
76
88
|
from operate.services.deployment_runner import stop_deployment_manager
|
|
89
|
+
from operate.services.funding_manager import FundingInProgressError, FundingManager
|
|
77
90
|
from operate.services.health_checker import HealthChecker
|
|
91
|
+
from operate.settings import Settings
|
|
78
92
|
from operate.utils import subtract_dicts
|
|
79
93
|
from operate.utils.gnosis import get_assets_balances
|
|
80
|
-
from operate.
|
|
94
|
+
from operate.utils.single_instance import AppSingleInstance, ParentWatchdog
|
|
95
|
+
from operate.wallet.master import InsufficientFundsException, MasterWalletManager
|
|
81
96
|
from operate.wallet.wallet_recovery_manager import (
|
|
82
97
|
WalletRecoveryError,
|
|
83
98
|
WalletRecoveryManager,
|
|
84
99
|
)
|
|
85
100
|
|
|
86
101
|
|
|
102
|
+
# TODO Backport to Open Autonomy
|
|
103
|
+
def should_rebuild(error: str) -> bool:
|
|
104
|
+
"""Check if we should rebuild the transaction."""
|
|
105
|
+
for _error in (
|
|
106
|
+
"wrong transaction nonce",
|
|
107
|
+
"OldNonce",
|
|
108
|
+
"nonce too low",
|
|
109
|
+
"replacement transaction underpriced",
|
|
110
|
+
):
|
|
111
|
+
if _error in error:
|
|
112
|
+
return True
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
autonomy.chain.tx.ERRORS_TO_RETRY += ("replacement transaction underpriced",)
|
|
117
|
+
autonomy.chain.tx.should_rebuild = should_rebuild
|
|
118
|
+
# End backport to Open Autonomy
|
|
119
|
+
|
|
120
|
+
|
|
87
121
|
DEFAULT_MAX_RETRIES = 3
|
|
88
122
|
USER_NOT_LOGGED_IN_ERROR = JSONResponse(
|
|
89
123
|
content={"error": "User not logged in."}, status_code=HTTPStatus.UNAUTHORIZED
|
|
@@ -117,17 +151,28 @@ class OperateApp:
|
|
|
117
151
|
home: t.Optional[Path] = None,
|
|
118
152
|
) -> None:
|
|
119
153
|
"""Initialize object."""
|
|
120
|
-
super().__init__()
|
|
121
154
|
self._path = (home or OPERATE_HOME).resolve()
|
|
122
155
|
self._services = self._path / SERVICES_DIR
|
|
123
156
|
self._keys = self._path / KEYS_DIR
|
|
124
157
|
self.setup()
|
|
158
|
+
self._backup_operate_if_new_version()
|
|
125
159
|
|
|
126
160
|
services.manage.KeysManager(
|
|
127
161
|
path=self._keys,
|
|
128
162
|
logger=logger,
|
|
129
163
|
)
|
|
130
|
-
self.
|
|
164
|
+
self._password: t.Optional[str] = os.environ.get("OPERATE_USER_PASSWORD")
|
|
165
|
+
self.settings = Settings(path=self._path)
|
|
166
|
+
|
|
167
|
+
self._wallet_manager = MasterWalletManager(
|
|
168
|
+
path=self._path / WALLETS_DIR,
|
|
169
|
+
password=self.password,
|
|
170
|
+
)
|
|
171
|
+
self._wallet_manager.setup()
|
|
172
|
+
self._funding_manager = FundingManager(
|
|
173
|
+
wallet_manager=self._wallet_manager,
|
|
174
|
+
logger=logger,
|
|
175
|
+
)
|
|
131
176
|
|
|
132
177
|
mm = MigrationManager(self._path, logger)
|
|
133
178
|
mm.migrate_user_account()
|
|
@@ -135,6 +180,59 @@ class OperateApp:
|
|
|
135
180
|
mm.migrate_wallets(self.wallet_manager)
|
|
136
181
|
mm.migrate_qs_configs()
|
|
137
182
|
|
|
183
|
+
@property
|
|
184
|
+
def password(self) -> t.Optional[str]:
|
|
185
|
+
"""Get the password."""
|
|
186
|
+
return self._password
|
|
187
|
+
|
|
188
|
+
@password.setter
|
|
189
|
+
def password(self, value: t.Optional[str]) -> None:
|
|
190
|
+
"""Set the password."""
|
|
191
|
+
self._password = value
|
|
192
|
+
self._wallet_manager.password = value
|
|
193
|
+
|
|
194
|
+
def _backup_operate_if_new_version(self) -> None:
|
|
195
|
+
"""Backup .operate directory if this is a new version."""
|
|
196
|
+
current_version = Version(__version__)
|
|
197
|
+
backup_required = False
|
|
198
|
+
version_file = self._path / VERSION_FILE
|
|
199
|
+
if not version_file.exists():
|
|
200
|
+
backup_required = True
|
|
201
|
+
found_version = "0.10.21" # first version with version file
|
|
202
|
+
else:
|
|
203
|
+
found_version = Version(version_file.read_text())
|
|
204
|
+
if current_version.major > found_version.major or (
|
|
205
|
+
current_version.major == found_version.major
|
|
206
|
+
and current_version.minor > found_version.minor
|
|
207
|
+
):
|
|
208
|
+
backup_required = True
|
|
209
|
+
|
|
210
|
+
if not backup_required:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
backup_path = self._path.parent / f"{OPERATE}_v{found_version}_bak"
|
|
214
|
+
if backup_path.exists():
|
|
215
|
+
logger.info(f"Backup directory {backup_path} already exists.")
|
|
216
|
+
backup_path = (
|
|
217
|
+
self._path.parent / f"{OPERATE}_v{found_version}_bak_{int(time())}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
logger.info(f"Backing up existing {OPERATE} directory to {backup_path}")
|
|
221
|
+
shutil.copytree(self._path, backup_path)
|
|
222
|
+
version_file.write_text(str(current_version))
|
|
223
|
+
|
|
224
|
+
# remove recoverable files from the backup to save space
|
|
225
|
+
service_dir = backup_path / SERVICES_DIR
|
|
226
|
+
for service_path in service_dir.iterdir():
|
|
227
|
+
deployment_dir = service_path / DEPLOYMENT_DIR
|
|
228
|
+
if deployment_dir.exists():
|
|
229
|
+
shutil.rmtree(deployment_dir)
|
|
230
|
+
|
|
231
|
+
for agent_runner_path in service_path.glob(f"{AGENT_RUNNER_PREFIX}*"):
|
|
232
|
+
agent_runner_path.unlink()
|
|
233
|
+
|
|
234
|
+
logger.info("Backup completed.")
|
|
235
|
+
|
|
138
236
|
def create_user_account(self, password: str) -> UserAccount:
|
|
139
237
|
"""Create a user account."""
|
|
140
238
|
self.password = password
|
|
@@ -147,13 +245,13 @@ class OperateApp:
|
|
|
147
245
|
"""Updates current password"""
|
|
148
246
|
|
|
149
247
|
if not new_password:
|
|
150
|
-
raise ValueError(
|
|
248
|
+
raise ValueError(MSG_NEW_PASSWORD_MISSING)
|
|
151
249
|
|
|
152
250
|
if not (
|
|
153
251
|
self.user_account.is_valid(old_password)
|
|
154
252
|
and self.wallet_manager.is_password_valid(old_password)
|
|
155
253
|
):
|
|
156
|
-
raise ValueError(
|
|
254
|
+
raise ValueError(MSG_INVALID_PASSWORD)
|
|
157
255
|
|
|
158
256
|
wallet_manager = self.wallet_manager
|
|
159
257
|
wallet_manager.password = old_password
|
|
@@ -164,11 +262,11 @@ class OperateApp:
|
|
|
164
262
|
"""Updates current password using the mnemonic"""
|
|
165
263
|
|
|
166
264
|
if not new_password:
|
|
167
|
-
raise ValueError(
|
|
265
|
+
raise ValueError(MSG_NEW_PASSWORD_MISSING)
|
|
168
266
|
|
|
169
267
|
mnemonic = mnemonic.strip().lower()
|
|
170
268
|
if not self.wallet_manager.is_mnemonic_valid(mnemonic):
|
|
171
|
-
raise ValueError(
|
|
269
|
+
raise ValueError(MSG_INVALID_MNEMONIC)
|
|
172
270
|
|
|
173
271
|
wallet_manager = self.wallet_manager
|
|
174
272
|
wallet_manager.update_password_with_mnemonic(mnemonic, new_password)
|
|
@@ -181,10 +279,16 @@ class OperateApp:
|
|
|
181
279
|
return services.manage.ServiceManager(
|
|
182
280
|
path=self._services,
|
|
183
281
|
wallet_manager=self.wallet_manager,
|
|
282
|
+
funding_manager=self.funding_manager,
|
|
184
283
|
logger=logger,
|
|
185
284
|
skip_dependency_check=skip_dependency_check,
|
|
186
285
|
)
|
|
187
286
|
|
|
287
|
+
@property
|
|
288
|
+
def funding_manager(self) -> FundingManager:
|
|
289
|
+
"""Load funding manager."""
|
|
290
|
+
return self._funding_manager
|
|
291
|
+
|
|
188
292
|
@property
|
|
189
293
|
def user_account(self) -> t.Optional[UserAccount]:
|
|
190
294
|
"""Load user account."""
|
|
@@ -195,13 +299,7 @@ class OperateApp:
|
|
|
195
299
|
@property
|
|
196
300
|
def wallet_manager(self) -> MasterWalletManager:
|
|
197
301
|
"""Load wallet manager."""
|
|
198
|
-
|
|
199
|
-
path=self._path / WALLETS_DIR,
|
|
200
|
-
password=self.password,
|
|
201
|
-
logger=logger,
|
|
202
|
-
)
|
|
203
|
-
manager.setup()
|
|
204
|
-
return manager
|
|
302
|
+
return self._wallet_manager
|
|
205
303
|
|
|
206
304
|
@property
|
|
207
305
|
def wallet_recoverey_manager(self) -> WalletRecoveryManager:
|
|
@@ -234,7 +332,7 @@ class OperateApp:
|
|
|
234
332
|
"""Json representation of the app."""
|
|
235
333
|
return {
|
|
236
334
|
"name": "Operate HTTP server",
|
|
237
|
-
"version":
|
|
335
|
+
"version": (__version__),
|
|
238
336
|
"home": str(self._path),
|
|
239
337
|
}
|
|
240
338
|
|
|
@@ -254,7 +352,7 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
254
352
|
logger.warning("Healthchecker is off!!!")
|
|
255
353
|
operate = OperateApp(home=home)
|
|
256
354
|
|
|
257
|
-
|
|
355
|
+
funding_job: t.Optional[asyncio.Task] = None
|
|
258
356
|
health_checker = HealthChecker(
|
|
259
357
|
operate.service_manager(), number_of_fails=number_of_fails, logger=logger
|
|
260
358
|
)
|
|
@@ -274,25 +372,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
274
372
|
raise exception
|
|
275
373
|
return res
|
|
276
374
|
|
|
277
|
-
def schedule_funding_job(
|
|
278
|
-
service_config_id: str,
|
|
279
|
-
from_safe: bool = True,
|
|
280
|
-
) -> None:
|
|
281
|
-
"""Schedule a funding job."""
|
|
282
|
-
logger.info(f"Starting funding job for {service_config_id}")
|
|
283
|
-
if service_config_id in funding_jobs:
|
|
284
|
-
logger.info(f"Cancelling existing funding job for {service_config_id}")
|
|
285
|
-
cancel_funding_job(service_config_id=service_config_id)
|
|
286
|
-
|
|
287
|
-
loop = asyncio.get_running_loop()
|
|
288
|
-
funding_jobs[service_config_id] = loop.create_task(
|
|
289
|
-
operate.service_manager().funding_job(
|
|
290
|
-
service_config_id=service_config_id,
|
|
291
|
-
loop=loop,
|
|
292
|
-
from_safe=from_safe,
|
|
293
|
-
)
|
|
294
|
-
)
|
|
295
|
-
|
|
296
375
|
def schedule_healthcheck_job(
|
|
297
376
|
service_config_id: str,
|
|
298
377
|
) -> None:
|
|
@@ -301,13 +380,29 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
301
380
|
# dont start health checker if it's switched off
|
|
302
381
|
health_checker.start_for_service(service_config_id)
|
|
303
382
|
|
|
304
|
-
def
|
|
383
|
+
def schedule_funding_job() -> None:
|
|
384
|
+
"""Schedule the funding job."""
|
|
385
|
+
cancel_funding_job() # cancel previous job if any
|
|
386
|
+
logger.info("Starting the funding job")
|
|
387
|
+
|
|
388
|
+
loop = asyncio.get_event_loop()
|
|
389
|
+
nonlocal funding_job
|
|
390
|
+
funding_job = loop.create_task(
|
|
391
|
+
operate.funding_manager.funding_job(
|
|
392
|
+
service_manager=operate.service_manager(),
|
|
393
|
+
loop=loop,
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def cancel_funding_job() -> None:
|
|
305
398
|
"""Cancel funding job."""
|
|
306
|
-
|
|
399
|
+
nonlocal funding_job
|
|
400
|
+
if funding_job is None:
|
|
307
401
|
return
|
|
308
|
-
|
|
402
|
+
|
|
403
|
+
status = funding_job.cancel()
|
|
309
404
|
if not status:
|
|
310
|
-
logger.info(
|
|
405
|
+
logger.info("Funding job cancellation failed")
|
|
311
406
|
|
|
312
407
|
def pause_all_services_on_startup() -> None:
|
|
313
408
|
logger.info("Stopping services on startup...")
|
|
@@ -346,7 +441,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
346
441
|
f"Deployment {service_config_id} stopping failed. but continue"
|
|
347
442
|
)
|
|
348
443
|
logger.info(f"Cancelling funding job for {service_config_id}")
|
|
349
|
-
cancel_funding_job(service_config_id=service_config_id)
|
|
350
444
|
health_checker.stop_for_service(service_config_id=service_config_id)
|
|
351
445
|
|
|
352
446
|
def pause_all_services_on_exit(signum: int, frame: t.Optional[FrameType]) -> None:
|
|
@@ -365,49 +459,25 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
365
459
|
|
|
366
460
|
@asynccontextmanager
|
|
367
461
|
async def lifespan(app: FastAPI):
|
|
368
|
-
# Load the ML model
|
|
369
|
-
watchdog_task = set_parent_watchdog(app)
|
|
370
|
-
yield
|
|
371
|
-
# Clean up the ML models and release the resources
|
|
372
|
-
|
|
373
|
-
with suppress(Exception):
|
|
374
|
-
watchdog_task.cancel()
|
|
375
|
-
|
|
376
|
-
with suppress(Exception):
|
|
377
|
-
await watchdog_task
|
|
378
|
-
|
|
379
|
-
app = FastAPI(lifespan=lifespan)
|
|
380
|
-
|
|
381
|
-
def set_parent_watchdog(app):
|
|
382
462
|
async def stop_app():
|
|
383
|
-
logger.info("Stopping services
|
|
384
|
-
|
|
385
|
-
stop_deployment_manager() # TODO: make it async?
|
|
463
|
+
logger.info("Stopping services due to parent death...")
|
|
464
|
+
stop_deployment_manager()
|
|
386
465
|
await run_in_executor(pause_all_services)
|
|
387
|
-
|
|
388
|
-
logger.info("Stopping services on demand done.")
|
|
389
466
|
app._server.should_exit = True # pylint: disable=protected-access
|
|
390
|
-
logger.info("
|
|
467
|
+
logger.info("App stopped due to parent death.")
|
|
391
468
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
logger.info(
|
|
395
|
-
f"Parent alive check task started: ppid is {os.getppid()} and own pid is {os.getpid()}"
|
|
396
|
-
)
|
|
397
|
-
while True:
|
|
398
|
-
parent = psutil.Process(os.getpid()).parent()
|
|
399
|
-
if not parent:
|
|
400
|
-
logger.info("Parent is not alive, going to stop")
|
|
401
|
-
await stop_app()
|
|
402
|
-
return
|
|
403
|
-
await asyncio.sleep(3)
|
|
469
|
+
watchdog = ParentWatchdog(on_parent_exit=stop_app)
|
|
470
|
+
watchdog.start()
|
|
404
471
|
|
|
405
|
-
|
|
406
|
-
|
|
472
|
+
yield # --- app is running ---
|
|
473
|
+
|
|
474
|
+
with suppress(Exception):
|
|
475
|
+
cancel_funding_job()
|
|
407
476
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
477
|
+
with suppress(Exception):
|
|
478
|
+
await watchdog.stop()
|
|
479
|
+
|
|
480
|
+
app = FastAPI(lifespan=lifespan)
|
|
411
481
|
|
|
412
482
|
app.add_middleware(
|
|
413
483
|
CORSMiddleware,
|
|
@@ -415,37 +485,17 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
415
485
|
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
416
486
|
)
|
|
417
487
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
logger.
|
|
424
|
-
retries = 0
|
|
425
|
-
while retries < DEFAULT_MAX_RETRIES:
|
|
426
|
-
try:
|
|
427
|
-
return await f(request)
|
|
428
|
-
except (APIError, ProjectError) as e:
|
|
429
|
-
logger.error(f"Error {e}\n{traceback.format_exc()}")
|
|
430
|
-
if "has active endpoints" in str(e):
|
|
431
|
-
error_msg = "Service is already running."
|
|
432
|
-
else:
|
|
433
|
-
error_msg = "Service deployment failed. Please check the logs."
|
|
434
|
-
return JSONResponse(
|
|
435
|
-
content={"error": error_msg},
|
|
436
|
-
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
437
|
-
)
|
|
438
|
-
except Exception as e: # pylint: disable=broad-except
|
|
439
|
-
logger.error(f"Error {str(e)}\n{traceback.format_exc()}")
|
|
440
|
-
retries += 1
|
|
488
|
+
@app.middleware("http")
|
|
489
|
+
async def handle_internal_server_error(request: Request, call_next):
|
|
490
|
+
try:
|
|
491
|
+
response = await call_next(request)
|
|
492
|
+
except Exception as e: # pylint: disable=broad-except
|
|
493
|
+
logger.error(f"Error {str(e)}\n{traceback.format_exc()}")
|
|
441
494
|
return JSONResponse(
|
|
442
|
-
content={
|
|
443
|
-
"error": "Operation failed after multiple attempts. Please try again later."
|
|
444
|
-
},
|
|
495
|
+
content={"error": str(e)},
|
|
445
496
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
446
497
|
)
|
|
447
|
-
|
|
448
|
-
return _call
|
|
498
|
+
return response
|
|
449
499
|
|
|
450
500
|
@app.get(f"/{shutdown_endpoint}")
|
|
451
501
|
async def _kill_server(request: Request) -> JSONResponse:
|
|
@@ -464,19 +514,21 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
464
514
|
return JSONResponse(content={"stopped": True})
|
|
465
515
|
|
|
466
516
|
@app.get("/api")
|
|
467
|
-
@with_retries
|
|
468
517
|
async def _get_api(request: Request) -> JSONResponse:
|
|
469
518
|
"""Get API info."""
|
|
470
519
|
return JSONResponse(content=operate.json)
|
|
471
520
|
|
|
521
|
+
@app.get("/api/settings")
|
|
522
|
+
async def _get_settings(request: Request) -> JSONResponse:
|
|
523
|
+
"""Get settings."""
|
|
524
|
+
return JSONResponse(content=operate.settings.json)
|
|
525
|
+
|
|
472
526
|
@app.get("/api/account")
|
|
473
|
-
@with_retries
|
|
474
527
|
async def _get_account(request: Request) -> t.Dict:
|
|
475
528
|
"""Get account information."""
|
|
476
529
|
return {"is_setup": operate.user_account is not None}
|
|
477
530
|
|
|
478
531
|
@app.post("/api/account")
|
|
479
|
-
@with_retries
|
|
480
532
|
async def _setup_account(request: Request) -> t.Dict:
|
|
481
533
|
"""Setup account."""
|
|
482
534
|
if operate.user_account is not None:
|
|
@@ -498,7 +550,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
498
550
|
return JSONResponse(content={"error": None})
|
|
499
551
|
|
|
500
552
|
@app.put("/api/account")
|
|
501
|
-
@with_retries
|
|
502
553
|
async def _update_password( # pylint: disable=too-many-return-statements
|
|
503
554
|
request: Request,
|
|
504
555
|
) -> t.Dict:
|
|
@@ -568,7 +619,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
568
619
|
)
|
|
569
620
|
|
|
570
621
|
@app.post("/api/account/login")
|
|
571
|
-
@with_retries
|
|
572
622
|
async def _validate_password(request: Request) -> t.Dict:
|
|
573
623
|
"""Validate password."""
|
|
574
624
|
if operate.user_account is None:
|
|
@@ -577,18 +627,18 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
577
627
|
data = await request.json()
|
|
578
628
|
if not operate.user_account.is_valid(password=data["password"]):
|
|
579
629
|
return JSONResponse(
|
|
580
|
-
content={"error":
|
|
630
|
+
content={"error": MSG_INVALID_PASSWORD},
|
|
581
631
|
status_code=HTTPStatus.UNAUTHORIZED,
|
|
582
632
|
)
|
|
583
633
|
|
|
584
634
|
operate.password = data["password"]
|
|
635
|
+
schedule_funding_job()
|
|
585
636
|
return JSONResponse(
|
|
586
637
|
content={"message": "Login successful."},
|
|
587
638
|
status_code=HTTPStatus.OK,
|
|
588
639
|
)
|
|
589
640
|
|
|
590
641
|
@app.get("/api/wallet")
|
|
591
|
-
@with_retries
|
|
592
642
|
async def _get_wallets(request: Request) -> t.List[t.Dict]:
|
|
593
643
|
"""Get wallets."""
|
|
594
644
|
wallets = []
|
|
@@ -597,7 +647,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
597
647
|
return JSONResponse(content=wallets)
|
|
598
648
|
|
|
599
649
|
@app.post("/api/wallet")
|
|
600
|
-
@with_retries
|
|
601
650
|
async def _create_wallet(request: Request) -> t.List[t.Dict]:
|
|
602
651
|
"""Create wallet"""
|
|
603
652
|
if operate.user_account is None:
|
|
@@ -620,7 +669,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
620
669
|
return JSONResponse(content={"wallet": wallet.json, "mnemonic": mnemonic})
|
|
621
670
|
|
|
622
671
|
@app.post("/api/wallet/private_key")
|
|
623
|
-
@with_retries
|
|
624
672
|
async def _get_private_key(request: Request) -> t.List[t.Dict]:
|
|
625
673
|
"""Get Master EOA private key."""
|
|
626
674
|
if operate.user_account is None:
|
|
@@ -632,16 +680,51 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
632
680
|
return USER_NOT_LOGGED_IN_ERROR
|
|
633
681
|
if operate.password != password:
|
|
634
682
|
return JSONResponse(
|
|
635
|
-
content={"error":
|
|
683
|
+
content={"error": MSG_INVALID_PASSWORD},
|
|
636
684
|
status_code=HTTPStatus.UNAUTHORIZED,
|
|
637
685
|
)
|
|
638
686
|
|
|
687
|
+
# TODO Should fail if not provided
|
|
639
688
|
ledger_type = data.get("ledger_type", LedgerType.ETHEREUM.value)
|
|
640
689
|
wallet = operate.wallet_manager.load(ledger_type=LedgerType(ledger_type))
|
|
641
690
|
return JSONResponse(content={"private_key": wallet.crypto.private_key})
|
|
642
691
|
|
|
692
|
+
@app.post("/api/wallet/mnemonic")
|
|
693
|
+
async def _get_mnemonic(request: Request) -> t.List[t.Dict]:
|
|
694
|
+
"""Get Master EOA mnemonic."""
|
|
695
|
+
if operate.user_account is None:
|
|
696
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
697
|
+
|
|
698
|
+
data = await request.json()
|
|
699
|
+
password = data.get("password")
|
|
700
|
+
if operate.password is None:
|
|
701
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
702
|
+
if operate.password != password:
|
|
703
|
+
return JSONResponse(
|
|
704
|
+
content={"error": "Password is not valid."},
|
|
705
|
+
status_code=HTTPStatus.UNAUTHORIZED,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
try:
|
|
709
|
+
ledger_type = LedgerType(data.get("ledger_type"))
|
|
710
|
+
wallet = operate.wallet_manager.load(ledger_type=ledger_type)
|
|
711
|
+
mnemonic = wallet.decrypt_mnemonic(password=password)
|
|
712
|
+
if mnemonic is None:
|
|
713
|
+
return JSONResponse(
|
|
714
|
+
content={"error": "Mnemonic file does not exist."},
|
|
715
|
+
status_code=HTTPStatus.NOT_FOUND,
|
|
716
|
+
)
|
|
717
|
+
return JSONResponse(content={"mnemonic": mnemonic})
|
|
718
|
+
except Exception as e: # pylint: disable=broad-except
|
|
719
|
+
logger.error(f"Failed to retrieve mnemonic: {e}\n{traceback.format_exc()}")
|
|
720
|
+
return JSONResponse(
|
|
721
|
+
content={
|
|
722
|
+
"error": "Failed to retrieve mnemonic. Please check the logs."
|
|
723
|
+
},
|
|
724
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
725
|
+
)
|
|
726
|
+
|
|
643
727
|
@app.get("/api/extended/wallet")
|
|
644
|
-
@with_retries
|
|
645
728
|
async def _get_wallet_safe(request: Request) -> t.List[t.Dict]:
|
|
646
729
|
"""Get wallets."""
|
|
647
730
|
wallets = []
|
|
@@ -650,7 +733,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
650
733
|
return JSONResponse(content=wallets)
|
|
651
734
|
|
|
652
735
|
@app.get("/api/wallet/safe")
|
|
653
|
-
@with_retries
|
|
654
736
|
async def _get_safes(request: Request) -> t.List[t.Dict]:
|
|
655
737
|
"""Create wallet safe"""
|
|
656
738
|
all_safes = []
|
|
@@ -662,7 +744,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
662
744
|
return JSONResponse(content=all_safes)
|
|
663
745
|
|
|
664
746
|
@app.get("/api/wallet/safe/{chain}")
|
|
665
|
-
@with_retries
|
|
666
747
|
async def _get_safe(request: Request) -> t.List[t.Dict]:
|
|
667
748
|
"""Get safe address"""
|
|
668
749
|
chain = Chain.from_string(request.path_params["chain"])
|
|
@@ -742,14 +823,16 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
742
823
|
)
|
|
743
824
|
|
|
744
825
|
if transfer_excess_assets:
|
|
745
|
-
asset_addresses = {ZERO_ADDRESS} | {
|
|
826
|
+
asset_addresses = {ZERO_ADDRESS} | {
|
|
827
|
+
token[chain] for token in ERC20_TOKENS.values()
|
|
828
|
+
}
|
|
746
829
|
balances = get_assets_balances(
|
|
747
830
|
ledger_api=ledger_api,
|
|
748
831
|
addresses={wallet.address},
|
|
749
832
|
asset_addresses=asset_addresses,
|
|
750
833
|
raise_on_invalid_address=False,
|
|
751
834
|
)[wallet.address]
|
|
752
|
-
initial_funds = subtract_dicts(balances,
|
|
835
|
+
initial_funds = subtract_dicts(balances, DEFAULT_EOA_TOPUPS[chain])
|
|
753
836
|
|
|
754
837
|
logger.info(f"_create_safe Computed {initial_funds=}")
|
|
755
838
|
|
|
@@ -763,10 +846,13 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
763
846
|
|
|
764
847
|
transfer_txs = {}
|
|
765
848
|
for asset, amount in initial_funds.items():
|
|
849
|
+
if amount <= 0:
|
|
850
|
+
continue
|
|
851
|
+
|
|
766
852
|
logger.info(
|
|
767
853
|
f"_create_safe Transfer to={safe_address} {amount=} {chain} {asset=}"
|
|
768
854
|
)
|
|
769
|
-
tx_hash = wallet.
|
|
855
|
+
tx_hash = wallet.transfer(
|
|
770
856
|
to=safe_address,
|
|
771
857
|
amount=int(amount),
|
|
772
858
|
chain=chain,
|
|
@@ -792,7 +878,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
792
878
|
)
|
|
793
879
|
|
|
794
880
|
@app.put("/api/wallet/safe")
|
|
795
|
-
@with_retries
|
|
796
881
|
async def _update_safe(request: Request) -> t.List[t.Dict]:
|
|
797
882
|
"""Update wallet safe"""
|
|
798
883
|
# TODO: Extract login check as decorator
|
|
@@ -844,14 +929,88 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
844
929
|
}
|
|
845
930
|
)
|
|
846
931
|
|
|
932
|
+
@app.post("/api/wallet/withdraw")
|
|
933
|
+
async def _wallet_withdraw(request: Request) -> JSONResponse:
|
|
934
|
+
"""Withdraw from Master Safe / master eoa"""
|
|
935
|
+
|
|
936
|
+
if operate.password is None:
|
|
937
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
938
|
+
|
|
939
|
+
data = await request.json()
|
|
940
|
+
if not operate.user_account.is_valid(password=data["password"]):
|
|
941
|
+
return JSONResponse(
|
|
942
|
+
content={"error": MSG_INVALID_PASSWORD},
|
|
943
|
+
status_code=HTTPStatus.UNAUTHORIZED,
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
try:
|
|
947
|
+
withdraw_assets = data.get("withdraw_assets", {})
|
|
948
|
+
to = data["to"]
|
|
949
|
+
wallet_manager = operate.wallet_manager
|
|
950
|
+
transfer_txs: t.Dict[str, t.Dict[str, t.List[str]]] = {}
|
|
951
|
+
|
|
952
|
+
# TODO: Ensure master wallet has enough funding.
|
|
953
|
+
for chain_str, tokens in withdraw_assets.items():
|
|
954
|
+
chain = Chain(chain_str)
|
|
955
|
+
wallet = wallet_manager.load(chain.ledger_type)
|
|
956
|
+
transfer_txs[chain_str] = {}
|
|
957
|
+
|
|
958
|
+
# Process ERC20 first
|
|
959
|
+
for asset, amount in tokens.items():
|
|
960
|
+
if asset != ZERO_ADDRESS:
|
|
961
|
+
txs = wallet.transfer_from_safe_then_eoa(
|
|
962
|
+
to=to,
|
|
963
|
+
amount=int(amount),
|
|
964
|
+
chain=chain,
|
|
965
|
+
asset=asset,
|
|
966
|
+
)
|
|
967
|
+
transfer_txs[chain_str][asset] = txs
|
|
968
|
+
|
|
969
|
+
# Process native last
|
|
970
|
+
if ZERO_ADDRESS in tokens:
|
|
971
|
+
asset = ZERO_ADDRESS
|
|
972
|
+
amount = tokens[asset]
|
|
973
|
+
txs = wallet.transfer_from_safe_then_eoa(
|
|
974
|
+
to=to,
|
|
975
|
+
amount=int(amount),
|
|
976
|
+
chain=chain,
|
|
977
|
+
asset=asset,
|
|
978
|
+
)
|
|
979
|
+
transfer_txs[chain_str][asset] = txs
|
|
980
|
+
|
|
981
|
+
except InsufficientFundsException as e:
|
|
982
|
+
logger.error(f"Insufficient funds: {e}\n{traceback.format_exc()}")
|
|
983
|
+
return JSONResponse(
|
|
984
|
+
content={
|
|
985
|
+
"error": f"Failed to withdraw funds. Insufficient funds: {e}",
|
|
986
|
+
"transfer_txs": transfer_txs,
|
|
987
|
+
},
|
|
988
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
989
|
+
)
|
|
990
|
+
except Exception as e: # pylint: disable=broad-except
|
|
991
|
+
logger.error(f"Failed to withdraw funds: {e}\n{traceback.format_exc()}")
|
|
992
|
+
return JSONResponse(
|
|
993
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
994
|
+
content={
|
|
995
|
+
"error": "Failed to withdraw funds. Please check the logs.",
|
|
996
|
+
"transfer_txs": transfer_txs,
|
|
997
|
+
},
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
return JSONResponse(
|
|
1001
|
+
content={
|
|
1002
|
+
"error": None,
|
|
1003
|
+
"message": "Funds withdrawn successfully.",
|
|
1004
|
+
"transfer_txs": transfer_txs,
|
|
1005
|
+
}
|
|
1006
|
+
)
|
|
1007
|
+
|
|
847
1008
|
@app.get("/api/v2/services")
|
|
848
|
-
@with_retries
|
|
849
1009
|
async def _get_services(request: Request) -> JSONResponse:
|
|
850
1010
|
"""Get all services."""
|
|
851
1011
|
return JSONResponse(content=operate.service_manager().json)
|
|
852
1012
|
|
|
853
1013
|
@app.get("/api/v2/services/validate")
|
|
854
|
-
@with_retries
|
|
855
1014
|
async def _validate_services(request: Request) -> JSONResponse:
|
|
856
1015
|
"""Validate all services."""
|
|
857
1016
|
service_manager = operate.service_manager()
|
|
@@ -866,7 +1025,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
866
1025
|
)
|
|
867
1026
|
|
|
868
1027
|
@app.get("/api/v2/services/deployment")
|
|
869
|
-
@with_retries
|
|
870
1028
|
async def _get_services_deployment(request: Request) -> JSONResponse:
|
|
871
1029
|
"""Get a service deployment."""
|
|
872
1030
|
service_manager = operate.service_manager()
|
|
@@ -879,7 +1037,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
879
1037
|
return JSONResponse(content=output)
|
|
880
1038
|
|
|
881
1039
|
@app.get("/api/v2/service/{service_config_id}")
|
|
882
|
-
@with_retries
|
|
883
1040
|
async def _get_service(request: Request) -> JSONResponse:
|
|
884
1041
|
"""Get a service."""
|
|
885
1042
|
service_config_id = request.path_params["service_config_id"]
|
|
@@ -897,7 +1054,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
897
1054
|
)
|
|
898
1055
|
|
|
899
1056
|
@app.get("/api/v2/service/{service_config_id}/deployment")
|
|
900
|
-
@with_retries
|
|
901
1057
|
async def _get_service_deployment(request: Request) -> JSONResponse:
|
|
902
1058
|
"""Get a service deployment."""
|
|
903
1059
|
service_config_id = request.path_params["service_config_id"]
|
|
@@ -911,7 +1067,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
911
1067
|
return JSONResponse(content=deployment_json)
|
|
912
1068
|
|
|
913
1069
|
@app.get("/api/v2/service/{service_config_id}/agent_performance")
|
|
914
|
-
@with_retries
|
|
915
1070
|
async def _get_agent_performance(request: Request) -> JSONResponse:
|
|
916
1071
|
"""Get the service refill requirements."""
|
|
917
1072
|
service_config_id = request.path_params["service_config_id"]
|
|
@@ -925,8 +1080,22 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
925
1080
|
.get_agent_performance()
|
|
926
1081
|
)
|
|
927
1082
|
|
|
1083
|
+
@app.get("/api/v2/service/{service_config_id}/funding_requirements")
|
|
1084
|
+
async def _get_funding_requirements(request: Request) -> JSONResponse:
|
|
1085
|
+
"""Get the service refill requirements."""
|
|
1086
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1087
|
+
|
|
1088
|
+
if not operate.service_manager().exists(service_config_id=service_config_id):
|
|
1089
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1090
|
+
|
|
1091
|
+
return JSONResponse(
|
|
1092
|
+
content=operate.service_manager().funding_requirements(
|
|
1093
|
+
service_config_id=service_config_id
|
|
1094
|
+
)
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
# TODO deprecate
|
|
928
1098
|
@app.get("/api/v2/service/{service_config_id}/refill_requirements")
|
|
929
|
-
@with_retries
|
|
930
1099
|
async def _get_refill_requirements(request: Request) -> JSONResponse:
|
|
931
1100
|
"""Get the service refill requirements."""
|
|
932
1101
|
service_config_id = request.path_params["service_config_id"]
|
|
@@ -941,7 +1110,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
941
1110
|
)
|
|
942
1111
|
|
|
943
1112
|
@app.post("/api/v2/service")
|
|
944
|
-
@with_retries
|
|
945
1113
|
async def _create_services_v2(request: Request) -> JSONResponse:
|
|
946
1114
|
"""Create a service."""
|
|
947
1115
|
if operate.password is None:
|
|
@@ -953,7 +1121,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
953
1121
|
return JSONResponse(content=output.json)
|
|
954
1122
|
|
|
955
1123
|
@app.post("/api/v2/service/{service_config_id}")
|
|
956
|
-
@with_retries
|
|
957
1124
|
async def _deploy_and_run_service(request: Request) -> JSONResponse:
|
|
958
1125
|
"""Deploy a service."""
|
|
959
1126
|
if operate.password is None:
|
|
@@ -971,11 +1138,9 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
971
1138
|
manager.deploy_service_onchain_from_safe(
|
|
972
1139
|
service_config_id=service_config_id
|
|
973
1140
|
)
|
|
974
|
-
manager.fund_service(service_config_id=service_config_id)
|
|
975
1141
|
manager.deploy_service_locally(service_config_id=service_config_id)
|
|
976
1142
|
|
|
977
1143
|
await run_in_executor(_fn)
|
|
978
|
-
schedule_funding_job(service_config_id=service_config_id)
|
|
979
1144
|
schedule_healthcheck_job(service_config_id=service_config_id)
|
|
980
1145
|
|
|
981
1146
|
return JSONResponse(
|
|
@@ -986,7 +1151,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
986
1151
|
|
|
987
1152
|
@app.put("/api/v2/service/{service_config_id}")
|
|
988
1153
|
@app.patch("/api/v2/service/{service_config_id}")
|
|
989
|
-
@with_retries
|
|
990
1154
|
async def _update_service(request: Request) -> JSONResponse:
|
|
991
1155
|
"""Update a service."""
|
|
992
1156
|
if operate.password is None:
|
|
@@ -1022,7 +1186,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
1022
1186
|
return JSONResponse(content=output.json)
|
|
1023
1187
|
|
|
1024
1188
|
@app.post("/api/v2/service/{service_config_id}/deployment/stop")
|
|
1025
|
-
@with_retries
|
|
1026
1189
|
async def _stop_service_locally(request: Request) -> JSONResponse:
|
|
1027
1190
|
"""Stop a service deployment."""
|
|
1028
1191
|
|
|
@@ -1041,11 +1204,10 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
1041
1204
|
|
|
1042
1205
|
await run_in_executor(deployment.stop)
|
|
1043
1206
|
logger.info(f"Cancelling funding job for {service_config_id}")
|
|
1044
|
-
cancel_funding_job(service_config_id=service_config_id)
|
|
1045
1207
|
return JSONResponse(content=deployment.json)
|
|
1046
1208
|
|
|
1209
|
+
# TODO Deprecate
|
|
1047
1210
|
@app.post("/api/v2/service/{service_config_id}/onchain/withdraw")
|
|
1048
|
-
@with_retries
|
|
1049
1211
|
async def _withdraw_onchain(request: Request) -> JSONResponse:
|
|
1050
1212
|
"""Withdraw all the funds from a service."""
|
|
1051
1213
|
|
|
@@ -1074,16 +1236,20 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
1074
1236
|
service_manager.terminate_service_on_chain_from_safe(
|
|
1075
1237
|
service_config_id=service_config_id,
|
|
1076
1238
|
chain=chain,
|
|
1239
|
+
)
|
|
1240
|
+
service_manager.drain(
|
|
1241
|
+
service_config_id=service_config_id,
|
|
1242
|
+
chain_str=chain,
|
|
1077
1243
|
withdrawal_address=withdrawal_address,
|
|
1078
1244
|
)
|
|
1079
1245
|
|
|
1080
|
-
# drain the
|
|
1246
|
+
# drain the Master Safe and master signer for the home chain
|
|
1081
1247
|
chain = Chain(service.home_chain)
|
|
1082
1248
|
master_wallet = service_manager.wallet_manager.load(
|
|
1083
1249
|
ledger_type=chain.ledger_type
|
|
1084
1250
|
)
|
|
1085
1251
|
|
|
1086
|
-
# drain the
|
|
1252
|
+
# drain the Master Safe
|
|
1087
1253
|
logger.info(
|
|
1088
1254
|
f"Draining the Master Safe {master_wallet.safes[chain]} on chain {chain.value} (withdrawal address {withdrawal_address})."
|
|
1089
1255
|
)
|
|
@@ -1113,8 +1279,137 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
1113
1279
|
|
|
1114
1280
|
return JSONResponse(content={"error": None, "message": "Withdrawal successful"})
|
|
1115
1281
|
|
|
1282
|
+
@app.post("/api/v2/service/{service_config_id}/terminate_and_withdraw")
|
|
1283
|
+
async def _terminate_and_withdraw(request: Request) -> JSONResponse:
|
|
1284
|
+
"""Terminate the service and withdraw all the funds to Master Safe"""
|
|
1285
|
+
|
|
1286
|
+
if operate.password is None:
|
|
1287
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
1288
|
+
|
|
1289
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1290
|
+
service_manager = operate.service_manager()
|
|
1291
|
+
wallet_manager = operate.wallet_manager
|
|
1292
|
+
|
|
1293
|
+
if not service_manager.exists(service_config_id=service_config_id):
|
|
1294
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1295
|
+
|
|
1296
|
+
try:
|
|
1297
|
+
pause_all_services()
|
|
1298
|
+
service = service_manager.load(service_config_id=service_config_id)
|
|
1299
|
+
for chain in service.chain_configs:
|
|
1300
|
+
wallet = wallet_manager.load(Chain(chain).ledger_type)
|
|
1301
|
+
master_safe = wallet.safes[Chain(chain)]
|
|
1302
|
+
service_manager.terminate_service_on_chain_from_safe(
|
|
1303
|
+
service_config_id=service_config_id,
|
|
1304
|
+
chain=chain,
|
|
1305
|
+
)
|
|
1306
|
+
service_manager.drain(
|
|
1307
|
+
service_config_id=service_config_id,
|
|
1308
|
+
chain_str=chain,
|
|
1309
|
+
withdrawal_address=master_safe,
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
except InsufficientFundsException as e:
|
|
1313
|
+
logger.error(
|
|
1314
|
+
f"Failed to terminate service and withdraw funds. Insufficient funds: {e}\n{traceback.format_exc()}"
|
|
1315
|
+
)
|
|
1316
|
+
return JSONResponse(
|
|
1317
|
+
content={
|
|
1318
|
+
"error": f"Failed to terminate service and withdraw funds. Insufficient funds: {e}"
|
|
1319
|
+
},
|
|
1320
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1321
|
+
)
|
|
1322
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1323
|
+
logger.error(
|
|
1324
|
+
f"Terminate service and withdraw funds failed: {e}\n{traceback.format_exc()}"
|
|
1325
|
+
)
|
|
1326
|
+
return JSONResponse(
|
|
1327
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1328
|
+
content={
|
|
1329
|
+
"error": "Failed to terminate service and withdraw funds. Please check the logs."
|
|
1330
|
+
},
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
return JSONResponse(
|
|
1334
|
+
content={
|
|
1335
|
+
"error": None,
|
|
1336
|
+
"message": "Terminate service and withdraw funds successful",
|
|
1337
|
+
}
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
@app.post("/api/v2/service/{service_config_id}/fund")
|
|
1341
|
+
async def fund_service( # pylint: disable=too-many-return-statements
|
|
1342
|
+
request: Request,
|
|
1343
|
+
) -> JSONResponse:
|
|
1344
|
+
"""Fund agent or service safe via Master Safe"""
|
|
1345
|
+
|
|
1346
|
+
if operate.password is None:
|
|
1347
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
1348
|
+
|
|
1349
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1350
|
+
service_manager = operate.service_manager()
|
|
1351
|
+
|
|
1352
|
+
if not service_manager.exists(service_config_id=service_config_id):
|
|
1353
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1354
|
+
|
|
1355
|
+
try:
|
|
1356
|
+
data = await request.json()
|
|
1357
|
+
service_manager.fund_service(
|
|
1358
|
+
service_config_id=service_config_id,
|
|
1359
|
+
amounts=ChainAmounts(
|
|
1360
|
+
{
|
|
1361
|
+
chain_str: {
|
|
1362
|
+
address: {
|
|
1363
|
+
asset: int(amount) for asset, amount in assets.items()
|
|
1364
|
+
}
|
|
1365
|
+
for address, assets in addresses.items()
|
|
1366
|
+
}
|
|
1367
|
+
for chain_str, addresses in data.items()
|
|
1368
|
+
}
|
|
1369
|
+
),
|
|
1370
|
+
)
|
|
1371
|
+
except ValueError as e:
|
|
1372
|
+
logger.error(
|
|
1373
|
+
f"Failed to fund from Master Safe: {e}\n{traceback.format_exc()}"
|
|
1374
|
+
)
|
|
1375
|
+
return JSONResponse(
|
|
1376
|
+
content={"error": str(e)},
|
|
1377
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1378
|
+
)
|
|
1379
|
+
except InsufficientFundsException as e:
|
|
1380
|
+
logger.error(
|
|
1381
|
+
f"Failed to fund from Master Safe. Insufficient funds: {e}\n{traceback.format_exc()}"
|
|
1382
|
+
)
|
|
1383
|
+
return JSONResponse(
|
|
1384
|
+
content={
|
|
1385
|
+
"error": f"Failed to fund from Master Safe. Insufficient funds: {e}"
|
|
1386
|
+
},
|
|
1387
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1388
|
+
)
|
|
1389
|
+
except FundingInProgressError as e:
|
|
1390
|
+
logger.error(
|
|
1391
|
+
f"Failed to fund from Master Safe: {e}\n{traceback.format_exc()}"
|
|
1392
|
+
)
|
|
1393
|
+
return JSONResponse(
|
|
1394
|
+
content={"error": str(e)},
|
|
1395
|
+
status_code=HTTPStatus.CONFLICT,
|
|
1396
|
+
)
|
|
1397
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1398
|
+
logger.error(
|
|
1399
|
+
f"Failed to fund from Master Safe: {e}\n{traceback.format_exc()}"
|
|
1400
|
+
)
|
|
1401
|
+
return JSONResponse(
|
|
1402
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1403
|
+
content={
|
|
1404
|
+
"error": "Failed to fund from Master Safe. Please check the logs."
|
|
1405
|
+
},
|
|
1406
|
+
)
|
|
1407
|
+
|
|
1408
|
+
return JSONResponse(
|
|
1409
|
+
content={"error": None, "message": "Funded from Master Safe successfully"}
|
|
1410
|
+
)
|
|
1411
|
+
|
|
1116
1412
|
@app.post("/api/bridge/bridge_refill_requirements")
|
|
1117
|
-
@with_retries
|
|
1118
1413
|
async def _bridge_refill_requirements(request: Request) -> JSONResponse:
|
|
1119
1414
|
"""Get the bridge refill requirements."""
|
|
1120
1415
|
if operate.password is None:
|
|
@@ -1149,7 +1444,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
1149
1444
|
)
|
|
1150
1445
|
|
|
1151
1446
|
@app.post("/api/bridge/execute")
|
|
1152
|
-
@with_retries
|
|
1153
1447
|
async def _bridge_execute(request: Request) -> JSONResponse:
|
|
1154
1448
|
"""Execute bridge transaction."""
|
|
1155
1449
|
if operate.password is None:
|
|
@@ -1179,14 +1473,12 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
1179
1473
|
)
|
|
1180
1474
|
|
|
1181
1475
|
@app.get("/api/bridge/last_executed_bundle_id")
|
|
1182
|
-
@with_retries
|
|
1183
1476
|
async def _bridge_last_executed_bundle_id(request: Request) -> t.List[t.Dict]:
|
|
1184
1477
|
"""Get last executed bundle id."""
|
|
1185
1478
|
content = {"id": operate.bridge_manager.last_executed_bundle_id()}
|
|
1186
1479
|
return JSONResponse(content=content, status_code=HTTPStatus.OK)
|
|
1187
1480
|
|
|
1188
1481
|
@app.get("/api/bridge/status/{id}")
|
|
1189
|
-
@with_retries
|
|
1190
1482
|
async def _bridge_status(request: Request) -> JSONResponse:
|
|
1191
1483
|
"""Get bridge transaction status."""
|
|
1192
1484
|
|
|
@@ -1215,7 +1507,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
1215
1507
|
)
|
|
1216
1508
|
|
|
1217
1509
|
@app.post("/api/wallet/recovery/initiate")
|
|
1218
|
-
@with_retries
|
|
1219
1510
|
async def _wallet_recovery_initiate(request: Request) -> JSONResponse:
|
|
1220
1511
|
"""Initiate wallet recovery."""
|
|
1221
1512
|
if operate.user_account is None:
|
|
@@ -1259,7 +1550,6 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
1259
1550
|
)
|
|
1260
1551
|
|
|
1261
1552
|
@app.post("/api/wallet/recovery/complete")
|
|
1262
|
-
@with_retries
|
|
1263
1553
|
async def _wallet_recovery_complete(request: Request) -> JSONResponse:
|
|
1264
1554
|
"""Complete wallet recovery."""
|
|
1265
1555
|
if operate.user_account is None:
|
|
@@ -1326,6 +1616,11 @@ def _daemon(
|
|
|
1326
1616
|
] = None,
|
|
1327
1617
|
) -> None:
|
|
1328
1618
|
"""Launch operate daemon."""
|
|
1619
|
+
# try automatically shutdown previous instance before creating the app
|
|
1620
|
+
if TRY_TO_SHUTDOWN_PREVIOUS_INSTANCE:
|
|
1621
|
+
app_single_instance = AppSingleInstance(port)
|
|
1622
|
+
app_single_instance.shutdown_previous_instance()
|
|
1623
|
+
|
|
1329
1624
|
app = create_app(home=home)
|
|
1330
1625
|
|
|
1331
1626
|
config_kwargs = {
|
|
@@ -1345,23 +1640,6 @@ def _daemon(
|
|
|
1345
1640
|
}
|
|
1346
1641
|
)
|
|
1347
1642
|
|
|
1348
|
-
# try automatically shutdown previous instance
|
|
1349
|
-
if TRY_TO_SHUTDOWN_PREVIOUS_INSTANCE:
|
|
1350
|
-
url = f"http{'s' if ssl_keyfile and ssl_certfile else ''}://{host}:{port}/shutdown"
|
|
1351
|
-
logger.info(f"trying to stop previous instance with {url}")
|
|
1352
|
-
try:
|
|
1353
|
-
requests.get(
|
|
1354
|
-
f"https://{host}:{port}/shutdown", timeout=3, verify=False # nosec
|
|
1355
|
-
)
|
|
1356
|
-
except requests.exceptions.SSLError:
|
|
1357
|
-
logger.warning("SSL failed, trying HTTP fallback...")
|
|
1358
|
-
try:
|
|
1359
|
-
requests.get(f"http://{host}:{port}/shutdown", timeout=3)
|
|
1360
|
-
except Exception: # pylint: disable=broad-except
|
|
1361
|
-
logger.exception("Failed to stop previous instance")
|
|
1362
|
-
except Exception: # pylint: disable=broad-except
|
|
1363
|
-
logger.exception("Failed to stop previous instance")
|
|
1364
|
-
|
|
1365
1643
|
server = Server(Config(**config_kwargs))
|
|
1366
1644
|
app._server = server # pylint: disable=protected-access
|
|
1367
1645
|
server.run()
|