olas-operate-middleware 0.1.0rc59__py3-none-any.whl → 0.13.2__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.13.2.dist-info/METADATA +75 -0
- olas_operate_middleware-0.13.2.dist-info/RECORD +101 -0
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/WHEEL +1 -1
- operate/__init__.py +17 -0
- operate/account/user.py +35 -9
- operate/bridge/bridge_manager.py +470 -0
- operate/bridge/providers/lifi_provider.py +377 -0
- operate/bridge/providers/native_bridge_provider.py +677 -0
- operate/bridge/providers/provider.py +469 -0
- operate/bridge/providers/relay_provider.py +457 -0
- operate/cli.py +1565 -417
- operate/constants.py +60 -12
- operate/data/README.md +19 -0
- operate/data/contracts/{service_staking_token → dual_staking_token}/__init__.py +2 -2
- operate/data/contracts/dual_staking_token/build/DualStakingToken.json +443 -0
- operate/data/contracts/dual_staking_token/contract.py +132 -0
- operate/data/contracts/dual_staking_token/contract.yaml +23 -0
- operate/{ledger/base.py → data/contracts/foreign_omnibridge/__init__.py} +2 -19
- operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json +1372 -0
- operate/data/contracts/foreign_omnibridge/contract.py +130 -0
- operate/data/contracts/foreign_omnibridge/contract.yaml +23 -0
- operate/{ledger/solana.py → data/contracts/home_omnibridge/__init__.py} +2 -20
- operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +1421 -0
- operate/data/contracts/home_omnibridge/contract.py +80 -0
- operate/data/contracts/home_omnibridge/contract.yaml +23 -0
- operate/data/contracts/l1_standard_bridge/__init__.py +20 -0
- operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json +831 -0
- operate/data/contracts/l1_standard_bridge/contract.py +158 -0
- operate/data/contracts/l1_standard_bridge/contract.yaml +23 -0
- operate/data/contracts/l2_standard_bridge/__init__.py +20 -0
- operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json +626 -0
- operate/data/contracts/l2_standard_bridge/contract.py +130 -0
- operate/data/contracts/l2_standard_bridge/contract.yaml +23 -0
- operate/data/contracts/mech_activity/__init__.py +20 -0
- operate/data/contracts/mech_activity/build/MechActivity.json +111 -0
- operate/data/contracts/mech_activity/contract.py +44 -0
- operate/data/contracts/mech_activity/contract.yaml +23 -0
- operate/data/contracts/optimism_mintable_erc20/__init__.py +20 -0
- operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json +491 -0
- operate/data/contracts/optimism_mintable_erc20/contract.py +45 -0
- operate/data/contracts/optimism_mintable_erc20/contract.yaml +23 -0
- operate/data/contracts/recovery_module/__init__.py +20 -0
- operate/data/contracts/recovery_module/build/RecoveryModule.json +811 -0
- operate/data/contracts/recovery_module/contract.py +61 -0
- operate/data/contracts/recovery_module/contract.yaml +23 -0
- operate/data/contracts/requester_activity_checker/__init__.py +20 -0
- operate/data/contracts/requester_activity_checker/build/RequesterActivityChecker.json +111 -0
- operate/data/contracts/requester_activity_checker/contract.py +33 -0
- operate/data/contracts/requester_activity_checker/contract.yaml +23 -0
- operate/data/contracts/staking_token/__init__.py +20 -0
- operate/data/contracts/staking_token/build/StakingToken.json +1336 -0
- operate/data/contracts/{service_staking_token → staking_token}/contract.py +27 -13
- operate/data/contracts/staking_token/contract.yaml +23 -0
- operate/data/contracts/uniswap_v2_erc20/contract.yaml +3 -1
- operate/data/contracts/uniswap_v2_erc20/tests/__init__.py +20 -0
- operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py +363 -0
- operate/keys.py +118 -33
- operate/ledger/__init__.py +159 -56
- operate/ledger/profiles.py +321 -18
- operate/migration.py +555 -0
- operate/{http → operate_http}/__init__.py +3 -2
- operate/{http → operate_http}/exceptions.py +6 -4
- operate/operate_types.py +544 -0
- operate/pearl.py +13 -1
- operate/quickstart/analyse_logs.py +118 -0
- operate/quickstart/claim_staking_rewards.py +104 -0
- operate/quickstart/reset_configs.py +106 -0
- operate/quickstart/reset_password.py +70 -0
- operate/quickstart/reset_staking.py +145 -0
- operate/quickstart/run_service.py +726 -0
- operate/quickstart/stop_service.py +72 -0
- operate/quickstart/terminate_on_chain_service.py +83 -0
- operate/quickstart/utils.py +298 -0
- operate/resource.py +62 -3
- operate/services/agent_runner.py +202 -0
- operate/services/deployment_runner.py +868 -0
- operate/services/funding_manager.py +929 -0
- operate/services/health_checker.py +280 -0
- operate/services/manage.py +2356 -620
- operate/services/protocol.py +1246 -340
- operate/services/service.py +756 -391
- operate/services/utils/mech.py +103 -0
- operate/services/utils/tendermint.py +86 -12
- operate/settings.py +70 -0
- operate/utils/__init__.py +135 -0
- operate/utils/gnosis.py +407 -80
- operate/utils/single_instance.py +226 -0
- operate/utils/ssl.py +133 -0
- operate/wallet/master.py +708 -123
- operate/wallet/wallet_recovery_manager.py +507 -0
- olas_operate_middleware-0.1.0rc59.dist-info/METADATA +0 -304
- olas_operate_middleware-0.1.0rc59.dist-info/RECORD +0 -41
- operate/data/contracts/service_staking_token/build/ServiceStakingToken.json +0 -1273
- operate/data/contracts/service_staking_token/contract.yaml +0 -23
- operate/ledger/ethereum.py +0 -48
- operate/types.py +0 -260
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/entry_points.txt +0 -0
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info/licenses}/LICENSE +0 -0
operate/cli.py
CHANGED
|
@@ -18,107 +18,320 @@
|
|
|
18
18
|
# ------------------------------------------------------------------------------
|
|
19
19
|
|
|
20
20
|
"""Operate app CLI module."""
|
|
21
|
-
|
|
22
21
|
import asyncio
|
|
23
|
-
import
|
|
22
|
+
import atexit
|
|
23
|
+
import multiprocessing
|
|
24
24
|
import os
|
|
25
|
+
import shutil
|
|
25
26
|
import signal
|
|
26
27
|
import traceback
|
|
27
28
|
import typing as t
|
|
28
29
|
import uuid
|
|
30
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
31
|
+
from contextlib import asynccontextmanager, suppress
|
|
32
|
+
from http import HTTPStatus
|
|
29
33
|
from pathlib import Path
|
|
34
|
+
from time import time
|
|
35
|
+
from types import FrameType
|
|
30
36
|
|
|
37
|
+
import autonomy.chain.tx
|
|
31
38
|
from aea.helpers.logging import setup_logger
|
|
32
39
|
from clea import group, params, run
|
|
33
|
-
from compose.project import ProjectError
|
|
34
|
-
from docker.errors import APIError
|
|
35
40
|
from fastapi import FastAPI, Request
|
|
36
41
|
from fastapi.middleware.cors import CORSMiddleware
|
|
37
42
|
from fastapi.responses import JSONResponse
|
|
38
43
|
from typing_extensions import Annotated
|
|
39
|
-
from uvicorn.
|
|
44
|
+
from uvicorn.config import Config
|
|
45
|
+
from uvicorn.server import Server
|
|
40
46
|
|
|
41
|
-
from operate import services
|
|
47
|
+
from operate import __version__, services
|
|
42
48
|
from operate.account.user import UserAccount
|
|
43
|
-
from operate.
|
|
44
|
-
from operate.
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
from operate.bridge.bridge_manager import BridgeManager
|
|
50
|
+
from operate.constants import (
|
|
51
|
+
AGENT_RUNNER_PREFIX,
|
|
52
|
+
DEPLOYMENT_DIR,
|
|
53
|
+
KEYS_DIR,
|
|
54
|
+
MIN_PASSWORD_LENGTH,
|
|
55
|
+
MSG_INVALID_MNEMONIC,
|
|
56
|
+
MSG_INVALID_PASSWORD,
|
|
57
|
+
MSG_NEW_PASSWORD_MISSING,
|
|
58
|
+
OPERATE,
|
|
59
|
+
OPERATE_HOME,
|
|
60
|
+
SERVICES_DIR,
|
|
61
|
+
USER_JSON,
|
|
62
|
+
VERSION_FILE,
|
|
63
|
+
WALLETS_DIR,
|
|
64
|
+
WALLET_RECOVERY_DIR,
|
|
65
|
+
ZERO_ADDRESS,
|
|
66
|
+
)
|
|
67
|
+
from operate.keys import KeysManager
|
|
68
|
+
from operate.ledger.profiles import (
|
|
69
|
+
DEFAULT_EOA_TOPUPS,
|
|
70
|
+
DEFAULT_NEW_SAFE_FUNDS,
|
|
71
|
+
ERC20_TOKENS,
|
|
72
|
+
)
|
|
73
|
+
from operate.migration import MigrationManager
|
|
74
|
+
from operate.operate_types import (
|
|
75
|
+
Chain,
|
|
76
|
+
ChainAmounts,
|
|
77
|
+
DeploymentStatus,
|
|
78
|
+
LedgerType,
|
|
79
|
+
Version,
|
|
80
|
+
)
|
|
81
|
+
from operate.quickstart.analyse_logs import analyse_logs
|
|
82
|
+
from operate.quickstart.claim_staking_rewards import claim_staking_rewards
|
|
83
|
+
from operate.quickstart.reset_configs import reset_configs
|
|
84
|
+
from operate.quickstart.reset_password import reset_password
|
|
85
|
+
from operate.quickstart.reset_staking import reset_staking
|
|
86
|
+
from operate.quickstart.run_service import run_service
|
|
87
|
+
from operate.quickstart.stop_service import stop_service
|
|
88
|
+
from operate.quickstart.terminate_on_chain_service import terminate_service
|
|
89
|
+
from operate.services.deployment_runner import stop_deployment_manager
|
|
90
|
+
from operate.services.funding_manager import FundingInProgressError, FundingManager
|
|
91
|
+
from operate.services.health_checker import HealthChecker
|
|
92
|
+
from operate.settings import Settings
|
|
93
|
+
from operate.utils import subtract_dicts
|
|
94
|
+
from operate.utils.gnosis import get_assets_balances
|
|
95
|
+
from operate.utils.single_instance import AppSingleInstance, ParentWatchdog
|
|
96
|
+
from operate.wallet.master import InsufficientFundsException, MasterWalletManager
|
|
97
|
+
from operate.wallet.wallet_recovery_manager import (
|
|
98
|
+
WalletRecoveryError,
|
|
99
|
+
WalletRecoveryManager,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# TODO Backport to Open Autonomy
|
|
104
|
+
def should_rebuild(error: str) -> bool:
|
|
105
|
+
"""Check if we should rebuild the transaction."""
|
|
106
|
+
for _error in (
|
|
107
|
+
"wrong transaction nonce",
|
|
108
|
+
"OldNonce",
|
|
109
|
+
"nonce too low",
|
|
110
|
+
"replacement transaction underpriced",
|
|
111
|
+
):
|
|
112
|
+
if _error in error:
|
|
113
|
+
return True
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
autonomy.chain.tx.ERRORS_TO_RETRY += ("replacement transaction underpriced",)
|
|
118
|
+
autonomy.chain.tx.should_rebuild = should_rebuild
|
|
119
|
+
# End backport to Open Autonomy
|
|
47
120
|
|
|
48
121
|
|
|
49
|
-
DEFAULT_HARDHAT_KEY = (
|
|
50
|
-
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
|
51
|
-
).encode()
|
|
52
122
|
DEFAULT_MAX_RETRIES = 3
|
|
53
123
|
USER_NOT_LOGGED_IN_ERROR = JSONResponse(
|
|
54
|
-
content={"error": "User not logged in
|
|
124
|
+
content={"error": "User not logged in."}, status_code=HTTPStatus.UNAUTHORIZED
|
|
125
|
+
)
|
|
126
|
+
USER_LOGGED_IN_ERROR = JSONResponse(
|
|
127
|
+
content={"error": "User must be logged out to perform this operation."},
|
|
128
|
+
status_code=HTTPStatus.FORBIDDEN,
|
|
55
129
|
)
|
|
130
|
+
ACCOUNT_NOT_FOUND_ERROR = JSONResponse(
|
|
131
|
+
content={"error": "User account not found."},
|
|
132
|
+
status_code=HTTPStatus.NOT_FOUND,
|
|
133
|
+
)
|
|
134
|
+
TRY_TO_SHUTDOWN_PREVIOUS_INSTANCE = True
|
|
135
|
+
|
|
136
|
+
logger = setup_logger(name="operate")
|
|
56
137
|
|
|
57
138
|
|
|
58
|
-
def service_not_found_error(
|
|
139
|
+
def service_not_found_error(service_config_id: str) -> JSONResponse:
|
|
59
140
|
"""Service not found error response"""
|
|
60
141
|
return JSONResponse(
|
|
61
|
-
content={"error": f"Service {
|
|
142
|
+
content={"error": f"Service {service_config_id} not found"},
|
|
143
|
+
status_code=HTTPStatus.NOT_FOUND,
|
|
62
144
|
)
|
|
63
145
|
|
|
64
146
|
|
|
65
|
-
class OperateApp:
|
|
147
|
+
class OperateApp: # pylint: disable=too-many-instance-attributes
|
|
66
148
|
"""Operate app."""
|
|
67
149
|
|
|
68
150
|
def __init__(
|
|
69
151
|
self,
|
|
70
152
|
home: t.Optional[Path] = None,
|
|
71
|
-
logger: t.Optional[logging.Logger] = None,
|
|
72
153
|
) -> None:
|
|
73
154
|
"""Initialize object."""
|
|
74
|
-
|
|
75
|
-
self.
|
|
76
|
-
self.
|
|
77
|
-
self._keys = self._path / KEYS
|
|
78
|
-
self._master_key = self._path / KEY
|
|
155
|
+
self._path = (home or OPERATE_HOME).resolve()
|
|
156
|
+
self._services = self._path / SERVICES_DIR
|
|
157
|
+
self._keys = self._path / KEYS_DIR
|
|
79
158
|
self.setup()
|
|
159
|
+
self._backup_operate_if_new_version()
|
|
80
160
|
|
|
81
|
-
self.
|
|
82
|
-
self.
|
|
161
|
+
self._password: t.Optional[str] = os.environ.get("OPERATE_USER_PASSWORD")
|
|
162
|
+
self._keys_manager: KeysManager = KeysManager(
|
|
83
163
|
path=self._keys,
|
|
84
|
-
logger=
|
|
164
|
+
logger=logger,
|
|
165
|
+
password=self._password,
|
|
166
|
+
)
|
|
167
|
+
self.settings = Settings(path=self._path)
|
|
168
|
+
|
|
169
|
+
self._wallet_manager = MasterWalletManager(
|
|
170
|
+
path=self._path / WALLETS_DIR,
|
|
171
|
+
password=self.password,
|
|
172
|
+
)
|
|
173
|
+
self._wallet_manager.setup()
|
|
174
|
+
self._funding_manager = FundingManager(
|
|
175
|
+
keys_manager=self._keys_manager,
|
|
176
|
+
wallet_manager=self._wallet_manager,
|
|
177
|
+
logger=logger,
|
|
85
178
|
)
|
|
86
|
-
|
|
179
|
+
|
|
180
|
+
self._migration_manager = MigrationManager(self._path, logger)
|
|
181
|
+
self._migration_manager.migrate_user_account()
|
|
182
|
+
self._migration_manager.migrate_services(self.service_manager())
|
|
183
|
+
self._migration_manager.migrate_wallets(self.wallet_manager)
|
|
184
|
+
self._migration_manager.migrate_qs_configs()
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def password(self) -> t.Optional[str]:
|
|
188
|
+
"""Get the password."""
|
|
189
|
+
return self._password
|
|
190
|
+
|
|
191
|
+
@password.setter
|
|
192
|
+
def password(self, value: t.Optional[str]) -> None:
|
|
193
|
+
"""Set the password."""
|
|
194
|
+
self._password = value
|
|
195
|
+
self._keys_manager.password = value
|
|
196
|
+
self._wallet_manager.password = value
|
|
197
|
+
self._migration_manager.migrate_keys(self._keys_manager)
|
|
198
|
+
|
|
199
|
+
def _backup_operate_if_new_version(self) -> None:
|
|
200
|
+
"""Backup .operate directory if this is a new version."""
|
|
201
|
+
current_version = Version(__version__)
|
|
202
|
+
backup_required = False
|
|
203
|
+
version_file = self._path / VERSION_FILE
|
|
204
|
+
if not version_file.exists():
|
|
205
|
+
backup_required = True
|
|
206
|
+
found_version = "0.10.21" # first version with version file
|
|
207
|
+
else:
|
|
208
|
+
found_version = Version(version_file.read_text())
|
|
209
|
+
if current_version.major > found_version.major or (
|
|
210
|
+
current_version.major == found_version.major
|
|
211
|
+
and current_version.minor > found_version.minor
|
|
212
|
+
):
|
|
213
|
+
backup_required = True
|
|
214
|
+
|
|
215
|
+
if not backup_required:
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
backup_path = self._path.parent / f"{OPERATE}_v{found_version}_bak"
|
|
219
|
+
if backup_path.exists():
|
|
220
|
+
logger.info(f"Backup directory {backup_path} already exists.")
|
|
221
|
+
backup_path = (
|
|
222
|
+
self._path.parent / f"{OPERATE}_v{found_version}_bak_{int(time())}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
logger.info(f"Backing up existing {OPERATE} directory to {backup_path}")
|
|
226
|
+
shutil.copytree(self._path, backup_path, ignore_dangling_symlinks=True)
|
|
227
|
+
version_file.write_text(str(current_version))
|
|
228
|
+
|
|
229
|
+
# remove recoverable files from the backup to save space
|
|
230
|
+
service_dir = backup_path / SERVICES_DIR
|
|
231
|
+
for service_path in service_dir.iterdir():
|
|
232
|
+
deployment_dir = service_path / DEPLOYMENT_DIR
|
|
233
|
+
if deployment_dir.exists():
|
|
234
|
+
shutil.rmtree(deployment_dir)
|
|
235
|
+
|
|
236
|
+
for agent_runner_path in service_path.glob(f"{AGENT_RUNNER_PREFIX}*"):
|
|
237
|
+
agent_runner_path.unlink()
|
|
238
|
+
|
|
239
|
+
logger.info("Backup completed.")
|
|
87
240
|
|
|
88
241
|
def create_user_account(self, password: str) -> UserAccount:
|
|
89
242
|
"""Create a user account."""
|
|
90
243
|
self.password = password
|
|
91
244
|
return UserAccount.new(
|
|
92
245
|
password=password,
|
|
93
|
-
path=self._path /
|
|
246
|
+
path=self._path / USER_JSON,
|
|
94
247
|
)
|
|
95
248
|
|
|
96
|
-
def
|
|
249
|
+
def update_password(self, old_password: str, new_password: str) -> None:
|
|
250
|
+
"""Updates current password"""
|
|
251
|
+
|
|
252
|
+
if not new_password:
|
|
253
|
+
raise ValueError(MSG_NEW_PASSWORD_MISSING)
|
|
254
|
+
|
|
255
|
+
if not (
|
|
256
|
+
self.user_account.is_valid(old_password)
|
|
257
|
+
and self.wallet_manager.is_password_valid(old_password)
|
|
258
|
+
):
|
|
259
|
+
raise ValueError(MSG_INVALID_PASSWORD)
|
|
260
|
+
|
|
261
|
+
wallet_manager = self.wallet_manager
|
|
262
|
+
wallet_manager.password = old_password
|
|
263
|
+
wallet_manager.update_password(new_password)
|
|
264
|
+
self._keys_manager.update_password(new_password)
|
|
265
|
+
self.user_account.update(old_password, new_password)
|
|
266
|
+
|
|
267
|
+
def update_password_with_mnemonic(self, mnemonic: str, new_password: str) -> None:
|
|
268
|
+
"""Updates current password using the mnemonic"""
|
|
269
|
+
|
|
270
|
+
if not new_password:
|
|
271
|
+
raise ValueError(MSG_NEW_PASSWORD_MISSING)
|
|
272
|
+
|
|
273
|
+
mnemonic = mnemonic.strip().lower()
|
|
274
|
+
if not self.wallet_manager.is_mnemonic_valid(mnemonic):
|
|
275
|
+
raise ValueError(MSG_INVALID_MNEMONIC)
|
|
276
|
+
|
|
277
|
+
wallet_manager = self.wallet_manager
|
|
278
|
+
wallet_manager.update_password_with_mnemonic(mnemonic, new_password)
|
|
279
|
+
self.user_account.force_update(new_password)
|
|
280
|
+
|
|
281
|
+
def service_manager(
|
|
282
|
+
self, skip_dependency_check: t.Optional[bool] = False
|
|
283
|
+
) -> services.manage.ServiceManager:
|
|
97
284
|
"""Load service manager."""
|
|
98
285
|
return services.manage.ServiceManager(
|
|
99
286
|
path=self._services,
|
|
100
287
|
keys_manager=self.keys_manager,
|
|
101
288
|
wallet_manager=self.wallet_manager,
|
|
102
|
-
|
|
289
|
+
funding_manager=self.funding_manager,
|
|
290
|
+
logger=logger,
|
|
291
|
+
skip_dependency_check=skip_dependency_check,
|
|
103
292
|
)
|
|
104
293
|
|
|
294
|
+
@property
|
|
295
|
+
def funding_manager(self) -> FundingManager:
|
|
296
|
+
"""Load funding manager."""
|
|
297
|
+
return self._funding_manager
|
|
298
|
+
|
|
105
299
|
@property
|
|
106
300
|
def user_account(self) -> t.Optional[UserAccount]:
|
|
107
301
|
"""Load user account."""
|
|
108
|
-
|
|
109
|
-
UserAccount.load(self._path /
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
302
|
+
if (self._path / USER_JSON).exists():
|
|
303
|
+
return UserAccount.load(self._path / USER_JSON)
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def keys_manager(self) -> KeysManager:
|
|
308
|
+
"""Load keys manager."""
|
|
309
|
+
return self._keys_manager
|
|
113
310
|
|
|
114
311
|
@property
|
|
115
312
|
def wallet_manager(self) -> MasterWalletManager:
|
|
116
|
-
"""Load
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
313
|
+
"""Load wallet manager."""
|
|
314
|
+
return self._wallet_manager
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def wallet_recovery_manager(self) -> WalletRecoveryManager:
|
|
318
|
+
"""Load wallet recovery manager."""
|
|
319
|
+
manager = WalletRecoveryManager(
|
|
320
|
+
path=self._path / WALLET_RECOVERY_DIR,
|
|
321
|
+
wallet_manager=self.wallet_manager,
|
|
322
|
+
service_manager=self.service_manager(),
|
|
323
|
+
logger=logger,
|
|
324
|
+
)
|
|
325
|
+
return manager
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def bridge_manager(self) -> BridgeManager:
|
|
329
|
+
"""Load bridge manager."""
|
|
330
|
+
manager = BridgeManager(
|
|
331
|
+
path=self._path / "bridge",
|
|
332
|
+
wallet_manager=self.wallet_manager,
|
|
333
|
+
logger=logger,
|
|
120
334
|
)
|
|
121
|
-
manager.setup()
|
|
122
335
|
return manager
|
|
123
336
|
|
|
124
337
|
def setup(self) -> None:
|
|
@@ -132,7 +345,7 @@ class OperateApp:
|
|
|
132
345
|
"""Json representation of the app."""
|
|
133
346
|
return {
|
|
134
347
|
"name": "Operate HTTP server",
|
|
135
|
-
"version":
|
|
348
|
+
"version": (__version__),
|
|
136
349
|
"home": str(self._path),
|
|
137
350
|
}
|
|
138
351
|
|
|
@@ -141,207 +354,304 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
141
354
|
home: t.Optional[Path] = None,
|
|
142
355
|
) -> FastAPI:
|
|
143
356
|
"""Create FastAPI object."""
|
|
357
|
+
HEALTH_CHECKER_OFF = os.environ.get("HEALTH_CHECKER_OFF", "0") == "1"
|
|
358
|
+
number_of_fails = int(
|
|
359
|
+
os.environ.get(
|
|
360
|
+
"HEALTH_CHECKER_TRIES", str(HealthChecker.NUMBER_OF_FAILS_DEFAULT)
|
|
361
|
+
)
|
|
362
|
+
)
|
|
144
363
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
healthcheck_jobs: t.Dict[str, asyncio.Task] = {}
|
|
364
|
+
if HEALTH_CHECKER_OFF:
|
|
365
|
+
logger.warning("Healthchecker is off!!!")
|
|
366
|
+
operate = OperateApp(home=home)
|
|
149
367
|
|
|
368
|
+
funding_job: t.Optional[asyncio.Task] = None
|
|
369
|
+
health_checker = HealthChecker(
|
|
370
|
+
operate.service_manager(), number_of_fails=number_of_fails, logger=logger
|
|
371
|
+
)
|
|
150
372
|
# Create shutdown endpoint
|
|
151
373
|
shutdown_endpoint = uuid.uuid4().hex
|
|
152
374
|
(operate._path / "operate.kill").write_text( # pylint: disable=protected-access
|
|
153
375
|
shutdown_endpoint
|
|
154
376
|
)
|
|
377
|
+
thread_pool_executor = ThreadPoolExecutor(max_workers=12)
|
|
155
378
|
|
|
156
|
-
def
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
cancel_funding_job(service=service)
|
|
165
|
-
|
|
166
|
-
loop = asyncio.get_running_loop()
|
|
167
|
-
funding_jobs[service] = loop.create_task(
|
|
168
|
-
operate.service_manager().funding_job(
|
|
169
|
-
hash=service,
|
|
170
|
-
loop=loop,
|
|
171
|
-
from_safe=from_safe,
|
|
172
|
-
)
|
|
173
|
-
)
|
|
379
|
+
async def run_in_executor(fn: t.Callable, *args: t.Any) -> t.Any:
|
|
380
|
+
loop = asyncio.get_event_loop()
|
|
381
|
+
future = loop.run_in_executor(thread_pool_executor, fn, *args)
|
|
382
|
+
res = await future
|
|
383
|
+
exception = future.exception()
|
|
384
|
+
if exception is not None:
|
|
385
|
+
raise exception
|
|
386
|
+
return res
|
|
174
387
|
|
|
175
388
|
def schedule_healthcheck_job(
|
|
176
|
-
|
|
389
|
+
service_config_id: str,
|
|
177
390
|
) -> None:
|
|
178
391
|
"""Schedule a healthcheck job."""
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
392
|
+
if not HEALTH_CHECKER_OFF:
|
|
393
|
+
# dont start health checker if it's switched off
|
|
394
|
+
health_checker.start_for_service(service_config_id)
|
|
395
|
+
|
|
396
|
+
def schedule_funding_job() -> None:
|
|
397
|
+
"""Schedule the funding job."""
|
|
398
|
+
cancel_funding_job() # cancel previous job if any
|
|
399
|
+
logger.info("Starting the funding job")
|
|
400
|
+
|
|
401
|
+
loop = asyncio.get_event_loop()
|
|
402
|
+
nonlocal funding_job
|
|
403
|
+
funding_job = loop.create_task(
|
|
404
|
+
operate.funding_manager.funding_job(
|
|
405
|
+
service_manager=operate.service_manager(),
|
|
406
|
+
loop=loop,
|
|
188
407
|
)
|
|
189
408
|
)
|
|
190
409
|
|
|
191
|
-
def cancel_funding_job(
|
|
410
|
+
def cancel_funding_job() -> None:
|
|
192
411
|
"""Cancel funding job."""
|
|
193
|
-
|
|
412
|
+
nonlocal funding_job
|
|
413
|
+
if funding_job is None:
|
|
194
414
|
return
|
|
195
|
-
|
|
415
|
+
|
|
416
|
+
status = funding_job.cancel()
|
|
196
417
|
if not status:
|
|
197
|
-
logger.info(
|
|
418
|
+
logger.info("Funding job cancellation failed")
|
|
198
419
|
|
|
199
420
|
def pause_all_services_on_startup() -> None:
|
|
200
421
|
logger.info("Stopping services on startup...")
|
|
201
|
-
|
|
422
|
+
pause_all_services()
|
|
423
|
+
logger.info("Stopping services on startup done.")
|
|
424
|
+
|
|
425
|
+
def pause_all_services() -> None:
|
|
426
|
+
service_manager = operate.service_manager()
|
|
427
|
+
if not service_manager.validate_services():
|
|
428
|
+
logger.error(
|
|
429
|
+
"Some services are not valid. Only pausing the valid services."
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
service_config_ids = [
|
|
433
|
+
i["service_config_id"] for i in operate.service_manager().json
|
|
434
|
+
]
|
|
202
435
|
|
|
203
|
-
for
|
|
204
|
-
|
|
436
|
+
for service_config_id in service_config_ids:
|
|
437
|
+
logger.info(f"Stopping service {service_config_id=}")
|
|
438
|
+
if not operate.service_manager().exists(
|
|
439
|
+
service_config_id=service_config_id
|
|
440
|
+
):
|
|
205
441
|
continue
|
|
206
|
-
deployment =
|
|
442
|
+
deployment = (
|
|
443
|
+
operate.service_manager()
|
|
444
|
+
.load(service_config_id=service_config_id)
|
|
445
|
+
.deployment
|
|
446
|
+
)
|
|
207
447
|
if deployment.status == DeploymentStatus.DELETED:
|
|
208
448
|
continue
|
|
209
|
-
logger.info(f"stopping service {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
449
|
+
logger.info(f"stopping service {service_config_id}")
|
|
450
|
+
try:
|
|
451
|
+
deployment.stop(force=True)
|
|
452
|
+
except Exception: # pylint: disable=broad-except
|
|
453
|
+
logger.exception(
|
|
454
|
+
f"Deployment {service_config_id} stopping failed. but continue"
|
|
455
|
+
)
|
|
456
|
+
logger.info(f"Cancelling funding job for {service_config_id}")
|
|
457
|
+
health_checker.stop_for_service(service_config_id=service_config_id)
|
|
214
458
|
|
|
215
|
-
def
|
|
216
|
-
"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
459
|
+
def pause_all_services_on_exit(signum: int, frame: t.Optional[FrameType]) -> None:
|
|
460
|
+
logger.info("Stopping services on exit...")
|
|
461
|
+
pause_all_services()
|
|
462
|
+
logger.info("Stopping services on exit done.")
|
|
463
|
+
|
|
464
|
+
signal.signal(signal.SIGINT, pause_all_services_on_exit)
|
|
465
|
+
signal.signal(signal.SIGTERM, pause_all_services_on_exit)
|
|
222
466
|
|
|
223
467
|
# on backend app started we assume there are now started agents, so we force to pause all
|
|
224
468
|
pause_all_services_on_startup()
|
|
225
469
|
|
|
226
|
-
|
|
470
|
+
# stop all services at middleware exit
|
|
471
|
+
atexit.register(pause_all_services)
|
|
472
|
+
|
|
473
|
+
@asynccontextmanager
|
|
474
|
+
async def lifespan(app: FastAPI):
|
|
475
|
+
async def stop_app():
|
|
476
|
+
logger.info("Stopping services due to parent death...")
|
|
477
|
+
stop_deployment_manager()
|
|
478
|
+
await run_in_executor(pause_all_services)
|
|
479
|
+
app._server.should_exit = True # pylint: disable=protected-access
|
|
480
|
+
logger.info("App stopped due to parent death.")
|
|
481
|
+
|
|
482
|
+
watchdog = ParentWatchdog(on_parent_exit=stop_app)
|
|
483
|
+
watchdog.start()
|
|
484
|
+
|
|
485
|
+
yield # --- app is running ---
|
|
486
|
+
|
|
487
|
+
with suppress(Exception):
|
|
488
|
+
cancel_funding_job()
|
|
489
|
+
|
|
490
|
+
with suppress(Exception):
|
|
491
|
+
await watchdog.stop()
|
|
492
|
+
|
|
493
|
+
app = FastAPI(lifespan=lifespan)
|
|
227
494
|
|
|
228
495
|
app.add_middleware(
|
|
229
496
|
CORSMiddleware,
|
|
230
497
|
allow_origins=["*"],
|
|
231
|
-
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
|
498
|
+
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
232
499
|
)
|
|
233
500
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
logger.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
except (APIError, ProjectError) as e:
|
|
246
|
-
logger.error(f"Error {e}\n{traceback.format_exc()}")
|
|
247
|
-
error = {"traceback": traceback.format_exc()}
|
|
248
|
-
if "has active endpoints" in e.explanation:
|
|
249
|
-
error["error"] = "Service is already running"
|
|
250
|
-
else:
|
|
251
|
-
error["error"] = str(e)
|
|
252
|
-
errors.append(error)
|
|
253
|
-
return JSONResponse(content={"errors": errors}, status_code=500)
|
|
254
|
-
except Exception as e: # pylint: disable=broad-except
|
|
255
|
-
errors.append(
|
|
256
|
-
{"error": str(e.args[0]), "traceback": traceback.format_exc()}
|
|
257
|
-
)
|
|
258
|
-
logger.error(f"Error {str(e.args[0])}\n{traceback.format_exc()}")
|
|
259
|
-
retries += 1
|
|
260
|
-
return JSONResponse(content={"errors": errors}, status_code=500)
|
|
261
|
-
|
|
262
|
-
return _call
|
|
501
|
+
@app.middleware("http")
|
|
502
|
+
async def handle_internal_server_error(request: Request, call_next):
|
|
503
|
+
try:
|
|
504
|
+
response = await call_next(request)
|
|
505
|
+
except Exception as e: # pylint: disable=broad-except
|
|
506
|
+
logger.error(f"Error {str(e)}\n{traceback.format_exc()}")
|
|
507
|
+
return JSONResponse(
|
|
508
|
+
content={"error": str(e)},
|
|
509
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
510
|
+
)
|
|
511
|
+
return response
|
|
263
512
|
|
|
264
513
|
@app.get(f"/{shutdown_endpoint}")
|
|
265
514
|
async def _kill_server(request: Request) -> JSONResponse:
|
|
266
515
|
"""Kill backend server from inside."""
|
|
267
516
|
os.kill(os.getpid(), signal.SIGINT)
|
|
517
|
+
return JSONResponse(content={})
|
|
518
|
+
|
|
519
|
+
@app.get("/shutdown")
|
|
520
|
+
async def _shutdown(request: Request) -> JSONResponse:
|
|
521
|
+
"""Kill backend server from inside."""
|
|
522
|
+
logger.info("Stopping services on demand...")
|
|
523
|
+
await run_in_executor(pause_all_services)
|
|
524
|
+
logger.info("Stopping services on demand done.")
|
|
525
|
+
app._server.should_exit = True # pylint: disable=protected-access
|
|
526
|
+
await asyncio.sleep(0.3)
|
|
527
|
+
return JSONResponse(content={"stopped": True})
|
|
268
528
|
|
|
269
529
|
@app.get("/api")
|
|
270
|
-
@with_retries
|
|
271
530
|
async def _get_api(request: Request) -> JSONResponse:
|
|
272
531
|
"""Get API info."""
|
|
273
532
|
return JSONResponse(content=operate.json)
|
|
274
533
|
|
|
534
|
+
@app.get("/api/settings")
|
|
535
|
+
async def _get_settings(request: Request) -> JSONResponse:
|
|
536
|
+
"""Get settings."""
|
|
537
|
+
return JSONResponse(content=operate.settings.json)
|
|
538
|
+
|
|
275
539
|
@app.get("/api/account")
|
|
276
|
-
@with_retries
|
|
277
540
|
async def _get_account(request: Request) -> t.Dict:
|
|
278
541
|
"""Get account information."""
|
|
279
542
|
return {"is_setup": operate.user_account is not None}
|
|
280
543
|
|
|
281
544
|
@app.post("/api/account")
|
|
282
|
-
@with_retries
|
|
283
545
|
async def _setup_account(request: Request) -> t.Dict:
|
|
284
546
|
"""Setup account."""
|
|
285
547
|
if operate.user_account is not None:
|
|
286
548
|
return JSONResponse(
|
|
287
|
-
content={"error": "Account already exists"},
|
|
288
|
-
status_code=
|
|
549
|
+
content={"error": "Account already exists."},
|
|
550
|
+
status_code=HTTPStatus.CONFLICT,
|
|
289
551
|
)
|
|
290
552
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
553
|
+
password = (await request.json()).get("password")
|
|
554
|
+
if not password or len(password) < MIN_PASSWORD_LENGTH:
|
|
555
|
+
return JSONResponse(
|
|
556
|
+
content={
|
|
557
|
+
"error": f"Password must be at least {MIN_PASSWORD_LENGTH} characters long."
|
|
558
|
+
},
|
|
559
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
operate.create_user_account(password=password)
|
|
295
563
|
return JSONResponse(content={"error": None})
|
|
296
564
|
|
|
297
565
|
@app.put("/api/account")
|
|
298
|
-
|
|
299
|
-
|
|
566
|
+
async def _update_password( # pylint: disable=too-many-return-statements
|
|
567
|
+
request: Request,
|
|
568
|
+
) -> t.Dict:
|
|
300
569
|
"""Update password."""
|
|
301
570
|
if operate.user_account is None:
|
|
571
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
572
|
+
|
|
573
|
+
data = await request.json()
|
|
574
|
+
old_password = data.get("old_password")
|
|
575
|
+
new_password = data.get("new_password")
|
|
576
|
+
mnemonic = data.get("mnemonic")
|
|
577
|
+
|
|
578
|
+
if not old_password and not mnemonic:
|
|
579
|
+
return JSONResponse(
|
|
580
|
+
content={
|
|
581
|
+
"error": "Exactly one of 'old_password' or 'mnemonic' (seed phrase) is required.",
|
|
582
|
+
},
|
|
583
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
if old_password and mnemonic:
|
|
302
587
|
return JSONResponse(
|
|
303
|
-
content={
|
|
304
|
-
|
|
588
|
+
content={
|
|
589
|
+
"error": "Exactly one of 'old_password' or 'mnemonic' (seed phrase) is required.",
|
|
590
|
+
},
|
|
591
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
if not new_password or len(new_password) < MIN_PASSWORD_LENGTH:
|
|
595
|
+
return JSONResponse(
|
|
596
|
+
content={
|
|
597
|
+
"error": f"New password must be at least {MIN_PASSWORD_LENGTH} characters long."
|
|
598
|
+
},
|
|
599
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
305
600
|
)
|
|
306
601
|
|
|
307
|
-
data = await request.json()
|
|
308
602
|
try:
|
|
309
|
-
|
|
310
|
-
old_password
|
|
311
|
-
|
|
603
|
+
if old_password:
|
|
604
|
+
operate.update_password(old_password, new_password)
|
|
605
|
+
return JSONResponse(
|
|
606
|
+
content={"error": None, "message": "Password updated successfully."}
|
|
607
|
+
)
|
|
608
|
+
if mnemonic:
|
|
609
|
+
operate.update_password_with_mnemonic(mnemonic, new_password)
|
|
610
|
+
return JSONResponse(
|
|
611
|
+
content={
|
|
612
|
+
"error": None,
|
|
613
|
+
"message": "Password updated successfully using seed phrase.",
|
|
614
|
+
}
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
return JSONResponse(
|
|
618
|
+
content={"error": "Password update failed."},
|
|
619
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
312
620
|
)
|
|
313
|
-
return JSONResponse(content={"error": None})
|
|
314
621
|
except ValueError as e:
|
|
622
|
+
logger.error(f"Password update error: {e}\n{traceback.format_exc()}")
|
|
623
|
+
return JSONResponse(
|
|
624
|
+
content={"error": f"Failed to update password: {str(e)}"},
|
|
625
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
626
|
+
)
|
|
627
|
+
except Exception as e: # pylint: disable=broad-except
|
|
628
|
+
logger.error(f"Password update error: {e}\n{traceback.format_exc()}")
|
|
315
629
|
return JSONResponse(
|
|
316
|
-
content={"error":
|
|
317
|
-
status_code=
|
|
630
|
+
content={"error": "Failed to update password. Please check the logs."},
|
|
631
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
318
632
|
)
|
|
319
633
|
|
|
320
634
|
@app.post("/api/account/login")
|
|
321
|
-
@with_retries
|
|
322
635
|
async def _validate_password(request: Request) -> t.Dict:
|
|
323
636
|
"""Validate password."""
|
|
324
637
|
if operate.user_account is None:
|
|
325
|
-
return
|
|
326
|
-
content={"error": "Account does not exist"},
|
|
327
|
-
status_code=400,
|
|
328
|
-
)
|
|
638
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
329
639
|
|
|
330
640
|
data = await request.json()
|
|
331
641
|
if not operate.user_account.is_valid(password=data["password"]):
|
|
332
642
|
return JSONResponse(
|
|
333
|
-
content={"error":
|
|
334
|
-
status_code=
|
|
643
|
+
content={"error": MSG_INVALID_PASSWORD},
|
|
644
|
+
status_code=HTTPStatus.UNAUTHORIZED,
|
|
335
645
|
)
|
|
336
646
|
|
|
337
647
|
operate.password = data["password"]
|
|
648
|
+
schedule_funding_job()
|
|
338
649
|
return JSONResponse(
|
|
339
|
-
content={"message": "Login successful"},
|
|
340
|
-
status_code=
|
|
650
|
+
content={"message": "Login successful."},
|
|
651
|
+
status_code=HTTPStatus.OK,
|
|
341
652
|
)
|
|
342
653
|
|
|
343
654
|
@app.get("/api/wallet")
|
|
344
|
-
@with_retries
|
|
345
655
|
async def _get_wallets(request: Request) -> t.List[t.Dict]:
|
|
346
656
|
"""Get wallets."""
|
|
347
657
|
wallets = []
|
|
@@ -349,42 +659,17 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
349
659
|
wallets.append(wallet.json)
|
|
350
660
|
return JSONResponse(content=wallets)
|
|
351
661
|
|
|
352
|
-
@app.get("/api/wallet/{chain}")
|
|
353
|
-
@with_retries
|
|
354
|
-
async def _get_wallet_by_chain(request: Request) -> t.List[t.Dict]:
|
|
355
|
-
"""Create wallet safe"""
|
|
356
|
-
ledger_type = get_ledger_type_from_chain_type(
|
|
357
|
-
chain=ChainType.from_string(request.path_params["chain"])
|
|
358
|
-
)
|
|
359
|
-
manager = operate.wallet_manager
|
|
360
|
-
if not manager.exists(ledger_type=ledger_type):
|
|
361
|
-
return JSONResponse(
|
|
362
|
-
content={"error": "Wallet does not exist"},
|
|
363
|
-
status_code=404,
|
|
364
|
-
)
|
|
365
|
-
return JSONResponse(
|
|
366
|
-
content=manager.load(ledger_type=ledger_type).json,
|
|
367
|
-
)
|
|
368
|
-
|
|
369
662
|
@app.post("/api/wallet")
|
|
370
|
-
@with_retries
|
|
371
663
|
async def _create_wallet(request: Request) -> t.List[t.Dict]:
|
|
372
664
|
"""Create wallet"""
|
|
373
665
|
if operate.user_account is None:
|
|
374
|
-
return
|
|
375
|
-
content={"error": "Cannot create wallet; User account does not exist!"},
|
|
376
|
-
status_code=400,
|
|
377
|
-
)
|
|
666
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
378
667
|
|
|
379
668
|
if operate.password is None:
|
|
380
|
-
return
|
|
381
|
-
content={"error": "You need to login before creating a wallet"},
|
|
382
|
-
status_code=401,
|
|
383
|
-
)
|
|
669
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
384
670
|
|
|
385
671
|
data = await request.json()
|
|
386
|
-
|
|
387
|
-
ledger_type = get_ledger_type_from_chain_type(chain=chain_type)
|
|
672
|
+
ledger_type = LedgerType(data["ledger_type"])
|
|
388
673
|
manager = operate.wallet_manager
|
|
389
674
|
if manager.exists(ledger_type=ledger_type):
|
|
390
675
|
return JSONResponse(
|
|
@@ -396,326 +681,970 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
396
681
|
wallet, mnemonic = manager.create(ledger_type=ledger_type)
|
|
397
682
|
return JSONResponse(content={"wallet": wallet.json, "mnemonic": mnemonic})
|
|
398
683
|
|
|
684
|
+
@app.post("/api/wallet/private_key")
|
|
685
|
+
async def _get_private_key(request: Request) -> t.List[t.Dict]:
|
|
686
|
+
"""Get Master EOA private key."""
|
|
687
|
+
if operate.user_account is None:
|
|
688
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
689
|
+
|
|
690
|
+
data = await request.json()
|
|
691
|
+
password = data.get("password")
|
|
692
|
+
if operate.password is None:
|
|
693
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
694
|
+
if operate.password != password:
|
|
695
|
+
return JSONResponse(
|
|
696
|
+
content={"error": MSG_INVALID_PASSWORD},
|
|
697
|
+
status_code=HTTPStatus.UNAUTHORIZED,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
# TODO Should fail if not provided
|
|
701
|
+
ledger_type = data.get("ledger_type", LedgerType.ETHEREUM.value)
|
|
702
|
+
wallet = operate.wallet_manager.load(ledger_type=LedgerType(ledger_type))
|
|
703
|
+
return JSONResponse(content={"private_key": wallet.crypto.private_key})
|
|
704
|
+
|
|
705
|
+
@app.post("/api/wallet/mnemonic")
|
|
706
|
+
async def _get_mnemonic(request: Request) -> t.List[t.Dict]:
|
|
707
|
+
"""Get Master EOA mnemonic."""
|
|
708
|
+
if operate.user_account is None:
|
|
709
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
710
|
+
|
|
711
|
+
data = await request.json()
|
|
712
|
+
password = data.get("password")
|
|
713
|
+
if operate.password is None:
|
|
714
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
715
|
+
if operate.password != password:
|
|
716
|
+
return JSONResponse(
|
|
717
|
+
content={"error": "Password is not valid."},
|
|
718
|
+
status_code=HTTPStatus.UNAUTHORIZED,
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
try:
|
|
722
|
+
ledger_type = LedgerType(data.get("ledger_type"))
|
|
723
|
+
wallet = operate.wallet_manager.load(ledger_type=ledger_type)
|
|
724
|
+
mnemonic = wallet.decrypt_mnemonic(password=password)
|
|
725
|
+
if mnemonic is None:
|
|
726
|
+
return JSONResponse(
|
|
727
|
+
content={"error": "Mnemonic file does not exist."},
|
|
728
|
+
status_code=HTTPStatus.NOT_FOUND,
|
|
729
|
+
)
|
|
730
|
+
return JSONResponse(content={"mnemonic": mnemonic})
|
|
731
|
+
except Exception as e: # pylint: disable=broad-except
|
|
732
|
+
logger.error(f"Failed to retrieve mnemonic: {e}\n{traceback.format_exc()}")
|
|
733
|
+
return JSONResponse(
|
|
734
|
+
content={
|
|
735
|
+
"error": "Failed to retrieve mnemonic. Please check the logs."
|
|
736
|
+
},
|
|
737
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
@app.get("/api/wallet/extended")
|
|
741
|
+
async def _get_wallet_safe(request: Request) -> t.List[t.Dict]:
|
|
742
|
+
"""Get wallets."""
|
|
743
|
+
wallets = []
|
|
744
|
+
for wallet in operate.wallet_manager:
|
|
745
|
+
wallets.append(wallet.extended_json)
|
|
746
|
+
return JSONResponse(content=wallets)
|
|
747
|
+
|
|
399
748
|
@app.get("/api/wallet/safe")
|
|
400
|
-
@with_retries
|
|
401
749
|
async def _get_safes(request: Request) -> t.List[t.Dict]:
|
|
402
750
|
"""Create wallet safe"""
|
|
403
|
-
|
|
751
|
+
all_safes = []
|
|
404
752
|
for wallet in operate.wallet_manager:
|
|
405
|
-
safes
|
|
406
|
-
|
|
753
|
+
safes = []
|
|
754
|
+
if wallet.safes is not None:
|
|
755
|
+
safes = list(wallet.safes.values())
|
|
756
|
+
all_safes.append({wallet.ledger_type: safes})
|
|
757
|
+
return JSONResponse(content=all_safes)
|
|
407
758
|
|
|
408
759
|
@app.get("/api/wallet/safe/{chain}")
|
|
409
|
-
@with_retries
|
|
410
760
|
async def _get_safe(request: Request) -> t.List[t.Dict]:
|
|
411
|
-
"""
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
)
|
|
761
|
+
"""Get safe address"""
|
|
762
|
+
chain = Chain.from_string(request.path_params["chain"])
|
|
763
|
+
ledger_type = chain.ledger_type
|
|
415
764
|
manager = operate.wallet_manager
|
|
416
765
|
if not manager.exists(ledger_type=ledger_type):
|
|
417
766
|
return JSONResponse(
|
|
418
|
-
content={"error": "
|
|
419
|
-
status_code=
|
|
767
|
+
content={"error": "No Master EOA found for this chain."},
|
|
768
|
+
status_code=HTTPStatus.NOT_FOUND,
|
|
769
|
+
)
|
|
770
|
+
safes = manager.load(ledger_type=ledger_type).safes
|
|
771
|
+
if safes is None or safes.get(chain) is None:
|
|
772
|
+
return JSONResponse(
|
|
773
|
+
content={"error": "No Master Safe found for this chain."},
|
|
774
|
+
status_code=HTTPStatus.NOT_FOUND,
|
|
420
775
|
)
|
|
776
|
+
|
|
421
777
|
return JSONResponse(
|
|
422
778
|
content={
|
|
423
|
-
"safe":
|
|
779
|
+
"safe": safes[chain],
|
|
424
780
|
},
|
|
425
781
|
)
|
|
426
782
|
|
|
427
783
|
@app.post("/api/wallet/safe")
|
|
428
|
-
|
|
429
|
-
|
|
784
|
+
async def _create_safe( # pylint: disable=too-many-return-statements
|
|
785
|
+
request: Request,
|
|
786
|
+
) -> t.List[t.Dict]:
|
|
430
787
|
"""Create wallet safe"""
|
|
431
788
|
if operate.user_account is None:
|
|
432
|
-
return
|
|
433
|
-
content={"error": "Cannot create safe; User account does not exist!"},
|
|
434
|
-
status_code=400,
|
|
435
|
-
)
|
|
789
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
436
790
|
|
|
437
791
|
if operate.password is None:
|
|
792
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
793
|
+
|
|
794
|
+
data = await request.json()
|
|
795
|
+
|
|
796
|
+
if "initial_funds" in data and "transfer_excess_assets" in data:
|
|
438
797
|
return JSONResponse(
|
|
439
|
-
content={
|
|
440
|
-
|
|
798
|
+
content={
|
|
799
|
+
"error": "Only specify one of 'initial_funds' or 'transfer_excess_assets', but not both."
|
|
800
|
+
},
|
|
801
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
441
802
|
)
|
|
442
803
|
|
|
443
|
-
data
|
|
444
|
-
|
|
445
|
-
|
|
804
|
+
logger.info(f"POST /api/wallet/safe {data=}")
|
|
805
|
+
|
|
806
|
+
chain = Chain(data["chain"])
|
|
807
|
+
ledger_type = chain.ledger_type
|
|
446
808
|
manager = operate.wallet_manager
|
|
447
809
|
if not manager.exists(ledger_type=ledger_type):
|
|
448
|
-
return JSONResponse(
|
|
810
|
+
return JSONResponse(
|
|
811
|
+
content={"error": "No Master EOA found for this chain."},
|
|
812
|
+
status_code=HTTPStatus.NOT_FOUND,
|
|
813
|
+
)
|
|
449
814
|
|
|
450
815
|
wallet = manager.load(ledger_type=ledger_type)
|
|
451
|
-
if wallet.
|
|
816
|
+
if wallet.safes is not None and wallet.safes.get(chain) is not None:
|
|
452
817
|
return JSONResponse(
|
|
453
|
-
content={
|
|
818
|
+
content={
|
|
819
|
+
"safe": wallet.safes.get(chain),
|
|
820
|
+
"message": "Safe already exists for this chain.",
|
|
821
|
+
}
|
|
454
822
|
)
|
|
455
823
|
|
|
456
|
-
wallet.
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
824
|
+
ledger_api = wallet.ledger_api(chain=chain)
|
|
825
|
+
safes = t.cast(t.Dict[Chain, str], wallet.safes)
|
|
826
|
+
|
|
827
|
+
backup_owner = data.get("backup_owner")
|
|
828
|
+
if backup_owner:
|
|
829
|
+
backup_owner = ledger_api.api.to_checksum_address(backup_owner)
|
|
830
|
+
|
|
831
|
+
# A default nonzero balance might be required on the Safe after creation.
|
|
832
|
+
# This is possibly required to estimate gas in protocol transactions.
|
|
833
|
+
initial_funds = data.get("initial_funds", DEFAULT_NEW_SAFE_FUNDS[chain])
|
|
834
|
+
transfer_excess_assets = (
|
|
835
|
+
str(data.get("transfer_excess_assets", "false")).lower() == "true"
|
|
465
836
|
)
|
|
466
|
-
|
|
837
|
+
|
|
838
|
+
if transfer_excess_assets:
|
|
839
|
+
asset_addresses = {ZERO_ADDRESS} | {
|
|
840
|
+
token[chain] for token in ERC20_TOKENS.values()
|
|
841
|
+
}
|
|
842
|
+
balances = get_assets_balances(
|
|
843
|
+
ledger_api=ledger_api,
|
|
844
|
+
addresses={wallet.address},
|
|
845
|
+
asset_addresses=asset_addresses,
|
|
846
|
+
raise_on_invalid_address=False,
|
|
847
|
+
)[wallet.address]
|
|
848
|
+
initial_funds = subtract_dicts(balances, DEFAULT_EOA_TOPUPS[chain])
|
|
849
|
+
|
|
850
|
+
logger.info(f"_create_safe Computed {initial_funds=}")
|
|
851
|
+
|
|
852
|
+
try:
|
|
853
|
+
create_tx = wallet.create_safe( # pylint: disable=no-member
|
|
854
|
+
chain=chain,
|
|
855
|
+
backup_owner=backup_owner,
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
safe_address = t.cast(str, safes.get(chain))
|
|
859
|
+
|
|
860
|
+
transfer_txs = {}
|
|
861
|
+
for asset, amount in initial_funds.items():
|
|
862
|
+
if amount <= 0:
|
|
863
|
+
continue
|
|
864
|
+
|
|
865
|
+
logger.info(
|
|
866
|
+
f"_create_safe Transfer to={safe_address} {amount=} {chain} {asset=}"
|
|
867
|
+
)
|
|
868
|
+
tx_hash = wallet.transfer(
|
|
869
|
+
to=safe_address,
|
|
870
|
+
amount=int(amount),
|
|
871
|
+
chain=chain,
|
|
872
|
+
asset=asset,
|
|
873
|
+
from_safe=False,
|
|
874
|
+
)
|
|
875
|
+
transfer_txs[asset] = tx_hash
|
|
876
|
+
|
|
877
|
+
return JSONResponse(
|
|
878
|
+
content={
|
|
879
|
+
"create_tx": create_tx,
|
|
880
|
+
"transfer_txs": transfer_txs,
|
|
881
|
+
"safe": safes.get(chain),
|
|
882
|
+
"message": "Safe created successfully",
|
|
883
|
+
},
|
|
884
|
+
status_code=HTTPStatus.CREATED,
|
|
885
|
+
)
|
|
886
|
+
except Exception as e: # pylint: disable=broad-except
|
|
887
|
+
logger.error(f"Safe creation failed: {e}\n{traceback.format_exc()}")
|
|
888
|
+
return JSONResponse(
|
|
889
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
890
|
+
content={"error": "Failed to create safe. Please check the logs."},
|
|
891
|
+
)
|
|
467
892
|
|
|
468
893
|
@app.put("/api/wallet/safe")
|
|
469
|
-
@with_retries
|
|
470
894
|
async def _update_safe(request: Request) -> t.List[t.Dict]:
|
|
471
|
-
"""
|
|
895
|
+
"""Update wallet safe"""
|
|
472
896
|
# TODO: Extract login check as decorator
|
|
473
897
|
if operate.user_account is None:
|
|
474
|
-
return
|
|
475
|
-
content={"error": "Cannot create safe; User account does not exist!"},
|
|
476
|
-
status_code=400,
|
|
477
|
-
)
|
|
898
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
478
899
|
|
|
479
900
|
if operate.password is None:
|
|
901
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
902
|
+
|
|
903
|
+
data = await request.json()
|
|
904
|
+
|
|
905
|
+
if "chain" not in data:
|
|
480
906
|
return JSONResponse(
|
|
481
|
-
content={"error": "
|
|
482
|
-
status_code=
|
|
907
|
+
content={"error": "'chain' is required."},
|
|
908
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
483
909
|
)
|
|
484
910
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
ledger_type = get_ledger_type_from_chain_type(chain=chain_type)
|
|
911
|
+
chain = Chain(data["chain"])
|
|
912
|
+
ledger_type = chain.ledger_type
|
|
488
913
|
manager = operate.wallet_manager
|
|
489
914
|
if not manager.exists(ledger_type=ledger_type):
|
|
490
|
-
return JSONResponse(
|
|
915
|
+
return JSONResponse(
|
|
916
|
+
content={"error": "No Master EOA found for this chain."},
|
|
917
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
918
|
+
)
|
|
491
919
|
|
|
492
920
|
wallet = manager.load(ledger_type=ledger_type)
|
|
493
|
-
wallet.
|
|
494
|
-
|
|
495
|
-
|
|
921
|
+
ledger_api = wallet.ledger_api(chain=chain)
|
|
922
|
+
|
|
923
|
+
backup_owner = data.get("backup_owner")
|
|
924
|
+
if backup_owner:
|
|
925
|
+
backup_owner = ledger_api.api.to_checksum_address(backup_owner)
|
|
926
|
+
|
|
927
|
+
backup_owner_updated = wallet.update_backup_owner(
|
|
928
|
+
chain=chain,
|
|
929
|
+
backup_owner=backup_owner,
|
|
930
|
+
)
|
|
931
|
+
message = (
|
|
932
|
+
"Backup owner updated successfully"
|
|
933
|
+
if backup_owner_updated
|
|
934
|
+
else "Backup owner is already set to this address"
|
|
935
|
+
)
|
|
936
|
+
return JSONResponse(
|
|
937
|
+
content={
|
|
938
|
+
"wallet": wallet.json,
|
|
939
|
+
"chain": chain.value,
|
|
940
|
+
"backup_owner_updated": backup_owner_updated,
|
|
941
|
+
"message": message,
|
|
942
|
+
}
|
|
496
943
|
)
|
|
497
|
-
return JSONResponse(content=wallet.json)
|
|
498
944
|
|
|
499
|
-
@app.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
"""Get available services."""
|
|
503
|
-
return JSONResponse(content=operate.service_manager().json)
|
|
945
|
+
@app.post("/api/wallet/withdraw")
|
|
946
|
+
async def _wallet_withdraw(request: Request) -> JSONResponse:
|
|
947
|
+
"""Withdraw from Master Safe / master eoa"""
|
|
504
948
|
|
|
505
|
-
@app.post("/api/services")
|
|
506
|
-
@with_retries
|
|
507
|
-
async def _create_services(request: Request) -> JSONResponse:
|
|
508
|
-
"""Create a service."""
|
|
509
949
|
if operate.password is None:
|
|
510
950
|
return USER_NOT_LOGGED_IN_ERROR
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
logger.info(f'Loading service {template["hash"]}')
|
|
518
|
-
service = manager.create_or_load(
|
|
519
|
-
hash=template["hash"],
|
|
520
|
-
rpc=template["configuration"]["rpc"],
|
|
521
|
-
on_chain_user_params=services.manage.OnChainUserParams.from_json(
|
|
522
|
-
template["configuration"]
|
|
523
|
-
),
|
|
524
|
-
)
|
|
525
|
-
else:
|
|
526
|
-
logger.info(f"Updating service from {old_hash} to " + template["hash"])
|
|
527
|
-
service = manager.update_service(
|
|
528
|
-
old_hash=old_hash,
|
|
529
|
-
new_hash=template["hash"],
|
|
530
|
-
rpc=template["configuration"]["rpc"],
|
|
531
|
-
on_chain_user_params=services.manage.OnChainUserParams.from_json(
|
|
532
|
-
template["configuration"]
|
|
533
|
-
),
|
|
534
|
-
from_safe=True,
|
|
535
|
-
)
|
|
536
|
-
update = True
|
|
537
|
-
else:
|
|
538
|
-
logger.info(f'Creating service {template["hash"]}')
|
|
539
|
-
service = manager.create_or_load(
|
|
540
|
-
hash=template["hash"],
|
|
541
|
-
rpc=template["configuration"]["rpc"],
|
|
542
|
-
on_chain_user_params=services.manage.OnChainUserParams.from_json(
|
|
543
|
-
template["configuration"]
|
|
544
|
-
),
|
|
951
|
+
|
|
952
|
+
data = await request.json()
|
|
953
|
+
if not operate.user_account.is_valid(password=data["password"]):
|
|
954
|
+
return JSONResponse(
|
|
955
|
+
content={"error": MSG_INVALID_PASSWORD},
|
|
956
|
+
status_code=HTTPStatus.UNAUTHORIZED,
|
|
545
957
|
)
|
|
546
958
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
959
|
+
try:
|
|
960
|
+
withdraw_assets = data.get("withdraw_assets", {})
|
|
961
|
+
to = data["to"]
|
|
962
|
+
wallet_manager = operate.wallet_manager
|
|
963
|
+
transfer_txs: t.Dict[str, t.Dict[str, t.List[str]]] = {}
|
|
964
|
+
|
|
965
|
+
# TODO: Ensure master wallet has enough funding.
|
|
966
|
+
for chain_str, tokens in withdraw_assets.items():
|
|
967
|
+
chain = Chain(chain_str)
|
|
968
|
+
wallet = wallet_manager.load(chain.ledger_type)
|
|
969
|
+
transfer_txs[chain_str] = {}
|
|
970
|
+
|
|
971
|
+
# Process ERC20 first
|
|
972
|
+
for asset, amount in tokens.items():
|
|
973
|
+
if asset != ZERO_ADDRESS:
|
|
974
|
+
txs = wallet.transfer_from_safe_then_eoa(
|
|
975
|
+
to=to,
|
|
976
|
+
amount=int(amount),
|
|
977
|
+
chain=chain,
|
|
978
|
+
asset=asset,
|
|
979
|
+
)
|
|
980
|
+
transfer_txs[chain_str][asset] = txs
|
|
981
|
+
|
|
982
|
+
# Process native last
|
|
983
|
+
if ZERO_ADDRESS in tokens:
|
|
984
|
+
asset = ZERO_ADDRESS
|
|
985
|
+
amount = tokens[asset]
|
|
986
|
+
txs = wallet.transfer_from_safe_then_eoa(
|
|
987
|
+
to=to,
|
|
988
|
+
amount=int(amount),
|
|
989
|
+
chain=chain,
|
|
990
|
+
asset=asset,
|
|
991
|
+
)
|
|
992
|
+
transfer_txs[chain_str][asset] = txs
|
|
993
|
+
|
|
994
|
+
except InsufficientFundsException as e:
|
|
995
|
+
logger.error(f"Insufficient funds: {e}\n{traceback.format_exc()}")
|
|
996
|
+
return JSONResponse(
|
|
997
|
+
content={
|
|
998
|
+
"error": f"Failed to withdraw funds. Insufficient funds: {e}",
|
|
999
|
+
"transfer_txs": transfer_txs,
|
|
1000
|
+
},
|
|
1001
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1002
|
+
)
|
|
1003
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1004
|
+
logger.error(f"Failed to withdraw funds: {e}\n{traceback.format_exc()}")
|
|
1005
|
+
return JSONResponse(
|
|
1006
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1007
|
+
content={
|
|
1008
|
+
"error": "Failed to withdraw funds. Please check the logs.",
|
|
1009
|
+
"transfer_txs": transfer_txs,
|
|
1010
|
+
},
|
|
1011
|
+
)
|
|
554
1012
|
|
|
555
1013
|
return JSONResponse(
|
|
556
|
-
content=
|
|
1014
|
+
content={
|
|
1015
|
+
"error": None,
|
|
1016
|
+
"message": "Funds withdrawn successfully.",
|
|
1017
|
+
"transfer_txs": transfer_txs,
|
|
1018
|
+
}
|
|
557
1019
|
)
|
|
558
1020
|
|
|
559
|
-
@app.
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1021
|
+
@app.get("/api/v2/services")
|
|
1022
|
+
async def _get_services(request: Request) -> JSONResponse:
|
|
1023
|
+
"""Get all services."""
|
|
1024
|
+
return JSONResponse(content=operate.service_manager().json)
|
|
1025
|
+
|
|
1026
|
+
@app.get("/api/v2/services/validate")
|
|
1027
|
+
async def _validate_services(request: Request) -> JSONResponse:
|
|
1028
|
+
"""Validate all services."""
|
|
1029
|
+
service_manager = operate.service_manager()
|
|
1030
|
+
service_ids = service_manager.get_all_service_ids()
|
|
1031
|
+
_services = [
|
|
1032
|
+
service.service_config_id
|
|
1033
|
+
for service in service_manager.get_all_services()[0]
|
|
1034
|
+
]
|
|
1035
|
+
|
|
1036
|
+
return JSONResponse(
|
|
1037
|
+
content={service_id: service_id in _services for service_id in service_ids}
|
|
569
1038
|
)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1039
|
+
|
|
1040
|
+
@app.get("/api/v2/services/deployment")
|
|
1041
|
+
async def _get_services_deployment(request: Request) -> JSONResponse:
|
|
1042
|
+
"""Get a service deployment."""
|
|
1043
|
+
service_manager = operate.service_manager()
|
|
1044
|
+
output = {}
|
|
1045
|
+
for service in service_manager.get_all_services()[0]:
|
|
1046
|
+
deployment_json = service.deployment.json
|
|
1047
|
+
deployment_json["healthcheck"] = service.get_latest_healthcheck()
|
|
1048
|
+
output[service.service_config_id] = deployment_json
|
|
1049
|
+
|
|
1050
|
+
return JSONResponse(content=output)
|
|
1051
|
+
|
|
1052
|
+
@app.get("/api/v2/service/{service_config_id}")
|
|
583
1053
|
async def _get_service(request: Request) -> JSONResponse:
|
|
584
|
-
"""
|
|
585
|
-
|
|
586
|
-
|
|
1054
|
+
"""Get a service."""
|
|
1055
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1056
|
+
|
|
1057
|
+
if not operate.service_manager().exists(service_config_id=service_config_id):
|
|
1058
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
587
1059
|
return JSONResponse(
|
|
588
1060
|
content=(
|
|
589
1061
|
operate.service_manager()
|
|
590
|
-
.
|
|
591
|
-
|
|
1062
|
+
.load(
|
|
1063
|
+
service_config_id=service_config_id,
|
|
592
1064
|
)
|
|
593
1065
|
.json
|
|
594
1066
|
)
|
|
595
1067
|
)
|
|
596
1068
|
|
|
597
|
-
@app.
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
operate.service_manager().
|
|
606
|
-
|
|
1069
|
+
@app.get("/api/v2/service/{service_config_id}/deployment")
|
|
1070
|
+
async def _get_service_deployment(request: Request) -> JSONResponse:
|
|
1071
|
+
"""Get a service deployment."""
|
|
1072
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1073
|
+
|
|
1074
|
+
if not operate.service_manager().exists(service_config_id=service_config_id):
|
|
1075
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1076
|
+
|
|
1077
|
+
service = operate.service_manager().load(service_config_id=service_config_id)
|
|
1078
|
+
deployment_json = service.deployment.json
|
|
1079
|
+
deployment_json["healthcheck"] = service.get_latest_healthcheck()
|
|
1080
|
+
return JSONResponse(content=deployment_json)
|
|
1081
|
+
|
|
1082
|
+
@app.get("/api/v2/service/{service_config_id}/agent_performance")
|
|
1083
|
+
async def _get_agent_performance(request: Request) -> JSONResponse:
|
|
1084
|
+
"""Get the service refill requirements."""
|
|
1085
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1086
|
+
|
|
1087
|
+
if not operate.service_manager().exists(service_config_id=service_config_id):
|
|
1088
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1089
|
+
|
|
1090
|
+
return JSONResponse(
|
|
1091
|
+
content=operate.service_manager()
|
|
1092
|
+
.load(service_config_id=service_config_id)
|
|
1093
|
+
.get_agent_performance()
|
|
607
1094
|
)
|
|
608
|
-
|
|
609
|
-
|
|
1095
|
+
|
|
1096
|
+
@app.get("/api/v2/service/{service_config_id}/funding_requirements")
|
|
1097
|
+
async def _get_funding_requirements(request: Request) -> JSONResponse:
|
|
1098
|
+
"""Get the service refill requirements."""
|
|
1099
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1100
|
+
|
|
1101
|
+
if not operate.service_manager().exists(service_config_id=service_config_id):
|
|
1102
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1103
|
+
|
|
1104
|
+
return JSONResponse(
|
|
1105
|
+
content=operate.service_manager().funding_requirements(
|
|
1106
|
+
service_config_id=service_config_id
|
|
1107
|
+
)
|
|
610
1108
|
)
|
|
1109
|
+
|
|
1110
|
+
# TODO deprecate
|
|
1111
|
+
@app.get("/api/v2/service/{service_config_id}/refill_requirements")
|
|
1112
|
+
async def _get_refill_requirements(request: Request) -> JSONResponse:
|
|
1113
|
+
"""Get the service refill requirements."""
|
|
1114
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1115
|
+
|
|
1116
|
+
if not operate.service_manager().exists(service_config_id=service_config_id):
|
|
1117
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1118
|
+
|
|
611
1119
|
return JSONResponse(
|
|
612
|
-
content=(
|
|
613
|
-
|
|
614
|
-
.create_or_load(hash=request.path_params["service"])
|
|
615
|
-
.json
|
|
1120
|
+
content=operate.service_manager().refill_requirements(
|
|
1121
|
+
service_config_id=service_config_id
|
|
616
1122
|
)
|
|
617
1123
|
)
|
|
618
1124
|
|
|
619
|
-
@app.post("/api/
|
|
620
|
-
|
|
621
|
-
async def _stop_service_onchain(request: Request) -> JSONResponse:
|
|
1125
|
+
@app.post("/api/v2/service")
|
|
1126
|
+
async def _create_services_v2(request: Request) -> JSONResponse:
|
|
622
1127
|
"""Create a service."""
|
|
623
|
-
if not operate.service_manager().exists(service=request.path_params["service"]):
|
|
624
|
-
return service_not_found_error(service=request.path_params["service"])
|
|
625
1128
|
if operate.password is None:
|
|
626
1129
|
return USER_NOT_LOGGED_IN_ERROR
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
)
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1130
|
+
template = await request.json()
|
|
1131
|
+
manager = operate.service_manager()
|
|
1132
|
+
output = manager.create(service_template=template)
|
|
1133
|
+
|
|
1134
|
+
return JSONResponse(content=output.json)
|
|
1135
|
+
|
|
1136
|
+
@app.post("/api/v2/service/{service_config_id}")
|
|
1137
|
+
async def _deploy_and_run_service(request: Request) -> JSONResponse:
|
|
1138
|
+
"""Deploy a service."""
|
|
1139
|
+
if operate.password is None:
|
|
1140
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
1141
|
+
|
|
1142
|
+
pause_all_services()
|
|
1143
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1144
|
+
manager = operate.service_manager()
|
|
1145
|
+
|
|
1146
|
+
if not manager.exists(service_config_id=service_config_id):
|
|
1147
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1148
|
+
|
|
1149
|
+
def _fn() -> None:
|
|
1150
|
+
# deploy_service_onchain_from_safe includes stake_service_on_chain_from_safe
|
|
1151
|
+
manager.deploy_service_onchain_from_safe(
|
|
1152
|
+
service_config_id=service_config_id
|
|
1153
|
+
)
|
|
1154
|
+
manager.deploy_service_locally(service_config_id=service_config_id)
|
|
1155
|
+
|
|
1156
|
+
await run_in_executor(_fn)
|
|
1157
|
+
schedule_healthcheck_job(service_config_id=service_config_id)
|
|
1158
|
+
|
|
636
1159
|
return JSONResponse(
|
|
637
1160
|
content=(
|
|
638
|
-
operate.service_manager()
|
|
639
|
-
.create_or_load(hash=request.path_params["service"])
|
|
640
|
-
.json
|
|
1161
|
+
operate.service_manager().load(service_config_id=service_config_id).json
|
|
641
1162
|
)
|
|
642
1163
|
)
|
|
643
1164
|
|
|
644
|
-
@app.
|
|
645
|
-
@
|
|
646
|
-
async def
|
|
647
|
-
"""
|
|
648
|
-
if
|
|
649
|
-
return
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1165
|
+
@app.put("/api/v2/service/{service_config_id}")
|
|
1166
|
+
@app.patch("/api/v2/service/{service_config_id}")
|
|
1167
|
+
async def _update_service(request: Request) -> JSONResponse:
|
|
1168
|
+
"""Update a service."""
|
|
1169
|
+
if operate.password is None:
|
|
1170
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
1171
|
+
|
|
1172
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1173
|
+
manager = operate.service_manager()
|
|
1174
|
+
|
|
1175
|
+
if not manager.exists(service_config_id=service_config_id):
|
|
1176
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1177
|
+
|
|
1178
|
+
template = await request.json()
|
|
1179
|
+
allow_different_service_public_id = template.get(
|
|
1180
|
+
"allow_different_service_public_id", False
|
|
656
1181
|
)
|
|
657
1182
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
operate.service_manager()
|
|
666
|
-
.create_or_load(
|
|
667
|
-
request.path_params["service"],
|
|
668
|
-
)
|
|
669
|
-
.deployment
|
|
1183
|
+
if request.method == "PUT":
|
|
1184
|
+
partial_update = False
|
|
1185
|
+
else:
|
|
1186
|
+
partial_update = True
|
|
1187
|
+
|
|
1188
|
+
logger.info(
|
|
1189
|
+
f"_update_service {partial_update=} {allow_different_service_public_id=}"
|
|
670
1190
|
)
|
|
671
|
-
deployment.build(force=True)
|
|
672
|
-
return JSONResponse(content=deployment.json)
|
|
673
1191
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
manager.fund_service(hash=service)
|
|
685
|
-
manager.deploy_service_locally(hash=service, force=True)
|
|
686
|
-
schedule_funding_job(service=service)
|
|
687
|
-
schedule_healthcheck_job(service=service.hash)
|
|
688
|
-
return JSONResponse(content=manager.create_or_load(service).deployment)
|
|
689
|
-
|
|
690
|
-
@app.post("/api/services/{service}/deployment/stop")
|
|
691
|
-
@with_retries
|
|
1192
|
+
output = manager.update(
|
|
1193
|
+
service_config_id=service_config_id,
|
|
1194
|
+
service_template=template,
|
|
1195
|
+
allow_different_service_public_id=allow_different_service_public_id,
|
|
1196
|
+
partial_update=partial_update,
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
return JSONResponse(content=output.json)
|
|
1200
|
+
|
|
1201
|
+
@app.post("/api/v2/service/{service_config_id}/deployment/stop")
|
|
692
1202
|
async def _stop_service_locally(request: Request) -> JSONResponse:
|
|
693
|
-
"""
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1203
|
+
"""Stop a service deployment."""
|
|
1204
|
+
|
|
1205
|
+
# No authentication required to stop services.
|
|
1206
|
+
|
|
1207
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1208
|
+
manager = operate.service_manager()
|
|
1209
|
+
|
|
1210
|
+
if not manager.exists(service_config_id=service_config_id):
|
|
1211
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1212
|
+
|
|
1213
|
+
service = operate.service_manager().load(service_config_id=service_config_id)
|
|
1214
|
+
service.remove_latest_healthcheck()
|
|
1215
|
+
deployment = service.deployment
|
|
1216
|
+
health_checker.stop_for_service(service_config_id=service_config_id)
|
|
1217
|
+
|
|
1218
|
+
await run_in_executor(deployment.stop)
|
|
1219
|
+
logger.info(f"Cancelling funding job for {service_config_id}")
|
|
701
1220
|
return JSONResponse(content=deployment.json)
|
|
702
1221
|
|
|
703
|
-
|
|
704
|
-
@
|
|
705
|
-
async def
|
|
706
|
-
"""
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1222
|
+
# TODO Deprecate
|
|
1223
|
+
@app.post("/api/v2/service/{service_config_id}/onchain/withdraw")
|
|
1224
|
+
async def _withdraw_onchain(request: Request) -> JSONResponse:
|
|
1225
|
+
"""Withdraw all the funds from a service."""
|
|
1226
|
+
|
|
1227
|
+
if operate.password is None:
|
|
1228
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
1229
|
+
|
|
1230
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1231
|
+
service_manager = operate.service_manager()
|
|
1232
|
+
|
|
1233
|
+
if not service_manager.exists(service_config_id=service_config_id):
|
|
1234
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1235
|
+
|
|
1236
|
+
withdrawal_address = (await request.json()).get("withdrawal_address")
|
|
1237
|
+
if withdrawal_address is None:
|
|
1238
|
+
return JSONResponse(
|
|
1239
|
+
content={"error": "'withdrawal_address' is required"},
|
|
1240
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1243
|
+
try:
|
|
1244
|
+
pause_all_services()
|
|
1245
|
+
service = service_manager.load(service_config_id=service_config_id)
|
|
1246
|
+
|
|
1247
|
+
# terminate the service on chain
|
|
1248
|
+
for chain, chain_config in service.chain_configs.items():
|
|
1249
|
+
service_manager.terminate_service_on_chain_from_safe(
|
|
1250
|
+
service_config_id=service_config_id,
|
|
1251
|
+
chain=chain,
|
|
1252
|
+
)
|
|
1253
|
+
service_manager.drain(
|
|
1254
|
+
service_config_id=service_config_id,
|
|
1255
|
+
chain_str=chain,
|
|
1256
|
+
withdrawal_address=withdrawal_address,
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
# drain the Master Safe and master signer for the home chain
|
|
1260
|
+
chain = Chain(service.home_chain)
|
|
1261
|
+
master_wallet = service_manager.wallet_manager.load(
|
|
1262
|
+
ledger_type=chain.ledger_type
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
# drain the Master Safe
|
|
1266
|
+
logger.info(
|
|
1267
|
+
f"Draining the Master Safe {master_wallet.safes[chain]} on chain {chain.value} (withdrawal address {withdrawal_address})."
|
|
1268
|
+
)
|
|
1269
|
+
master_wallet.drain(
|
|
1270
|
+
withdrawal_address=withdrawal_address,
|
|
1271
|
+
chain=chain,
|
|
1272
|
+
from_safe=True,
|
|
1273
|
+
rpc=chain_config.ledger_config.rpc,
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
# drain the master signer
|
|
1277
|
+
logger.info(
|
|
1278
|
+
f"Draining the Master Signer {master_wallet.address} on chain {chain.value} (withdrawal address {withdrawal_address})."
|
|
1279
|
+
)
|
|
1280
|
+
master_wallet.drain(
|
|
1281
|
+
withdrawal_address=withdrawal_address,
|
|
1282
|
+
chain=chain,
|
|
1283
|
+
from_safe=False,
|
|
1284
|
+
rpc=chain_config.ledger_config.rpc,
|
|
1285
|
+
)
|
|
1286
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1287
|
+
logger.error(f"Withdrawal failed: {e}\n{traceback.format_exc()}")
|
|
1288
|
+
return JSONResponse(
|
|
1289
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1290
|
+
content={"error": "Failed to withdraw funds. Please check the logs."},
|
|
1291
|
+
)
|
|
1292
|
+
|
|
1293
|
+
return JSONResponse(content={"error": None, "message": "Withdrawal successful"})
|
|
1294
|
+
|
|
1295
|
+
@app.post("/api/v2/service/{service_config_id}/terminate_and_withdraw")
|
|
1296
|
+
async def _terminate_and_withdraw(request: Request) -> JSONResponse:
|
|
1297
|
+
"""Terminate the service and withdraw all the funds to Master Safe"""
|
|
1298
|
+
|
|
1299
|
+
if operate.password is None:
|
|
1300
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
1301
|
+
|
|
1302
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1303
|
+
service_manager = operate.service_manager()
|
|
1304
|
+
wallet_manager = operate.wallet_manager
|
|
1305
|
+
|
|
1306
|
+
if not service_manager.exists(service_config_id=service_config_id):
|
|
1307
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1308
|
+
|
|
1309
|
+
try:
|
|
1310
|
+
pause_all_services()
|
|
1311
|
+
service = service_manager.load(service_config_id=service_config_id)
|
|
1312
|
+
for chain in service.chain_configs:
|
|
1313
|
+
wallet = wallet_manager.load(Chain(chain).ledger_type)
|
|
1314
|
+
master_safe = wallet.safes[Chain(chain)]
|
|
1315
|
+
service_manager.terminate_service_on_chain_from_safe(
|
|
1316
|
+
service_config_id=service_config_id,
|
|
1317
|
+
chain=chain,
|
|
1318
|
+
)
|
|
1319
|
+
service_manager.drain(
|
|
1320
|
+
service_config_id=service_config_id,
|
|
1321
|
+
chain_str=chain,
|
|
1322
|
+
withdrawal_address=master_safe,
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
except InsufficientFundsException as e:
|
|
1326
|
+
logger.error(
|
|
1327
|
+
f"Failed to terminate service and withdraw funds. Insufficient funds: {e}\n{traceback.format_exc()}"
|
|
1328
|
+
)
|
|
1329
|
+
return JSONResponse(
|
|
1330
|
+
content={
|
|
1331
|
+
"error": f"Failed to terminate service and withdraw funds. Insufficient funds: {e}"
|
|
1332
|
+
},
|
|
1333
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1334
|
+
)
|
|
1335
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1336
|
+
logger.error(
|
|
1337
|
+
f"Terminate service and withdraw funds failed: {e}\n{traceback.format_exc()}"
|
|
1338
|
+
)
|
|
1339
|
+
return JSONResponse(
|
|
1340
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1341
|
+
content={
|
|
1342
|
+
"error": "Failed to terminate service and withdraw funds. Please check the logs."
|
|
1343
|
+
},
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
return JSONResponse(
|
|
1347
|
+
content={
|
|
1348
|
+
"error": None,
|
|
1349
|
+
"message": "Terminate service and withdraw funds successful",
|
|
1350
|
+
}
|
|
716
1351
|
)
|
|
717
|
-
|
|
718
|
-
|
|
1352
|
+
|
|
1353
|
+
@app.post("/api/v2/service/{service_config_id}/fund")
|
|
1354
|
+
async def fund_service( # pylint: disable=too-many-return-statements
|
|
1355
|
+
request: Request,
|
|
1356
|
+
) -> JSONResponse:
|
|
1357
|
+
"""Fund agent or service safe via Master Safe"""
|
|
1358
|
+
|
|
1359
|
+
if operate.password is None:
|
|
1360
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
1361
|
+
|
|
1362
|
+
service_config_id = request.path_params["service_config_id"]
|
|
1363
|
+
service_manager = operate.service_manager()
|
|
1364
|
+
|
|
1365
|
+
if not service_manager.exists(service_config_id=service_config_id):
|
|
1366
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
1367
|
+
|
|
1368
|
+
try:
|
|
1369
|
+
data = await request.json()
|
|
1370
|
+
service_manager.fund_service(
|
|
1371
|
+
service_config_id=service_config_id,
|
|
1372
|
+
amounts=ChainAmounts(
|
|
1373
|
+
{
|
|
1374
|
+
chain_str: {
|
|
1375
|
+
address: {
|
|
1376
|
+
asset: int(amount) for asset, amount in assets.items()
|
|
1377
|
+
}
|
|
1378
|
+
for address, assets in addresses.items()
|
|
1379
|
+
}
|
|
1380
|
+
for chain_str, addresses in data.items()
|
|
1381
|
+
}
|
|
1382
|
+
),
|
|
1383
|
+
)
|
|
1384
|
+
except ValueError as e:
|
|
1385
|
+
logger.error(
|
|
1386
|
+
f"Failed to fund from Master Safe: {e}\n{traceback.format_exc()}"
|
|
1387
|
+
)
|
|
1388
|
+
return JSONResponse(
|
|
1389
|
+
content={"error": str(e)},
|
|
1390
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1391
|
+
)
|
|
1392
|
+
except InsufficientFundsException as e:
|
|
1393
|
+
logger.error(
|
|
1394
|
+
f"Failed to fund from Master Safe. Insufficient funds: {e}\n{traceback.format_exc()}"
|
|
1395
|
+
)
|
|
1396
|
+
return JSONResponse(
|
|
1397
|
+
content={
|
|
1398
|
+
"error": f"Failed to fund from Master Safe. Insufficient funds: {e}"
|
|
1399
|
+
},
|
|
1400
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1401
|
+
)
|
|
1402
|
+
except FundingInProgressError as e:
|
|
1403
|
+
logger.error(
|
|
1404
|
+
f"Failed to fund from Master Safe: {e}\n{traceback.format_exc()}"
|
|
1405
|
+
)
|
|
1406
|
+
return JSONResponse(
|
|
1407
|
+
content={"error": str(e)},
|
|
1408
|
+
status_code=HTTPStatus.CONFLICT,
|
|
1409
|
+
)
|
|
1410
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1411
|
+
logger.error(
|
|
1412
|
+
f"Failed to fund from Master Safe: {e}\n{traceback.format_exc()}"
|
|
1413
|
+
)
|
|
1414
|
+
return JSONResponse(
|
|
1415
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1416
|
+
content={
|
|
1417
|
+
"error": "Failed to fund from Master Safe. Please check the logs."
|
|
1418
|
+
},
|
|
1419
|
+
)
|
|
1420
|
+
|
|
1421
|
+
return JSONResponse(
|
|
1422
|
+
content={"error": None, "message": "Funded from Master Safe successfully"}
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
@app.post("/api/bridge/bridge_refill_requirements")
|
|
1426
|
+
async def _bridge_refill_requirements(request: Request) -> JSONResponse:
|
|
1427
|
+
"""Get the bridge refill requirements."""
|
|
1428
|
+
if operate.password is None:
|
|
1429
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
1430
|
+
|
|
1431
|
+
try:
|
|
1432
|
+
data = await request.json()
|
|
1433
|
+
output = operate.bridge_manager.bridge_refill_requirements(
|
|
1434
|
+
requests_params=data["bridge_requests"],
|
|
1435
|
+
force_update=data.get("force_update", False),
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
return JSONResponse(
|
|
1439
|
+
content=output,
|
|
1440
|
+
status_code=HTTPStatus.OK,
|
|
1441
|
+
)
|
|
1442
|
+
except ValueError as e:
|
|
1443
|
+
logger.error(f"Bridge refill requirements error: {e}")
|
|
1444
|
+
return JSONResponse(
|
|
1445
|
+
content={"error": "Invalid bridge request parameters."},
|
|
1446
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1447
|
+
)
|
|
1448
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1449
|
+
logger.error(
|
|
1450
|
+
f"Bridge refill requirements error: {e}\n{traceback.format_exc()}"
|
|
1451
|
+
)
|
|
1452
|
+
return JSONResponse(
|
|
1453
|
+
content={
|
|
1454
|
+
"error": "Failed to get bridge requirements. Please check the logs."
|
|
1455
|
+
},
|
|
1456
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
@app.post("/api/bridge/execute")
|
|
1460
|
+
async def _bridge_execute(request: Request) -> JSONResponse:
|
|
1461
|
+
"""Execute bridge transaction."""
|
|
1462
|
+
if operate.password is None:
|
|
1463
|
+
return USER_NOT_LOGGED_IN_ERROR
|
|
1464
|
+
|
|
1465
|
+
try:
|
|
1466
|
+
data = await request.json()
|
|
1467
|
+
output = operate.bridge_manager.execute_bundle(bundle_id=data["id"])
|
|
1468
|
+
|
|
1469
|
+
return JSONResponse(
|
|
1470
|
+
content=output,
|
|
1471
|
+
status_code=HTTPStatus.OK,
|
|
1472
|
+
)
|
|
1473
|
+
except ValueError as e:
|
|
1474
|
+
logger.error(f"Bridge execute error: {e}")
|
|
1475
|
+
return JSONResponse(
|
|
1476
|
+
content={"error": "Invalid bundle ID or transaction failed."},
|
|
1477
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1478
|
+
)
|
|
1479
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1480
|
+
logger.error(f"Bridge execute error: {e}\n{traceback.format_exc()}")
|
|
1481
|
+
return JSONResponse(
|
|
1482
|
+
content={
|
|
1483
|
+
"error": "Failed to execute bridge transaction. Please check the logs."
|
|
1484
|
+
},
|
|
1485
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
@app.get("/api/bridge/last_executed_bundle_id")
|
|
1489
|
+
async def _bridge_last_executed_bundle_id(request: Request) -> t.List[t.Dict]:
|
|
1490
|
+
"""Get last executed bundle id."""
|
|
1491
|
+
content = {"id": operate.bridge_manager.last_executed_bundle_id()}
|
|
1492
|
+
return JSONResponse(content=content, status_code=HTTPStatus.OK)
|
|
1493
|
+
|
|
1494
|
+
@app.get("/api/bridge/status/{id}")
|
|
1495
|
+
async def _bridge_status(request: Request) -> JSONResponse:
|
|
1496
|
+
"""Get bridge transaction status."""
|
|
1497
|
+
|
|
1498
|
+
quote_bundle_id = request.path_params["id"]
|
|
1499
|
+
|
|
1500
|
+
try:
|
|
1501
|
+
output = operate.bridge_manager.get_status_json(bundle_id=quote_bundle_id)
|
|
1502
|
+
|
|
1503
|
+
return JSONResponse(
|
|
1504
|
+
content=output,
|
|
1505
|
+
status_code=HTTPStatus.OK,
|
|
1506
|
+
)
|
|
1507
|
+
except ValueError as e:
|
|
1508
|
+
logger.error(f"Bridge status error: {e}")
|
|
1509
|
+
return JSONResponse(
|
|
1510
|
+
content={"error": "Invalid bundle ID."},
|
|
1511
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1512
|
+
)
|
|
1513
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1514
|
+
logger.error(f"Bridge status error: {e}\n{traceback.format_exc()}")
|
|
1515
|
+
return JSONResponse(
|
|
1516
|
+
content={
|
|
1517
|
+
"error": "Failed to get bridge status. Please check the logs."
|
|
1518
|
+
},
|
|
1519
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1520
|
+
)
|
|
1521
|
+
|
|
1522
|
+
@app.post("/api/wallet/recovery/prepare")
|
|
1523
|
+
async def _wallet_recovery_prepare(request: Request) -> JSONResponse:
|
|
1524
|
+
"""Prepare wallet recovery."""
|
|
1525
|
+
if operate.user_account is None:
|
|
1526
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
1527
|
+
|
|
1528
|
+
if operate.password:
|
|
1529
|
+
return USER_LOGGED_IN_ERROR
|
|
1530
|
+
|
|
1531
|
+
data = await request.json()
|
|
1532
|
+
new_password = data.get("new_password")
|
|
1533
|
+
|
|
1534
|
+
if not new_password or len(new_password) < MIN_PASSWORD_LENGTH:
|
|
1535
|
+
return JSONResponse(
|
|
1536
|
+
content={
|
|
1537
|
+
"error": f"New password must be at least {MIN_PASSWORD_LENGTH} characters long."
|
|
1538
|
+
},
|
|
1539
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
try:
|
|
1543
|
+
output = operate.wallet_recovery_manager.prepare_recovery(
|
|
1544
|
+
new_password=new_password
|
|
1545
|
+
)
|
|
1546
|
+
return JSONResponse(
|
|
1547
|
+
content=output,
|
|
1548
|
+
status_code=HTTPStatus.OK,
|
|
1549
|
+
)
|
|
1550
|
+
except (ValueError, WalletRecoveryError) as e:
|
|
1551
|
+
logger.error(f"_recovery_prepare error: {e}")
|
|
1552
|
+
return JSONResponse(
|
|
1553
|
+
content={"error": f"Failed to prepare recovery: {e}"},
|
|
1554
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1555
|
+
)
|
|
1556
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1557
|
+
logger.error(f"_recovery_prepare error: {e}\n{traceback.format_exc()}")
|
|
1558
|
+
return JSONResponse(
|
|
1559
|
+
content={"error": "Failed to prepare recovery. Please check the logs."},
|
|
1560
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1561
|
+
)
|
|
1562
|
+
|
|
1563
|
+
@app.get("/api/wallet/recovery/funding_requirements")
|
|
1564
|
+
async def _get_recovery_funding_requirements(request: Request) -> JSONResponse:
|
|
1565
|
+
"""Get recovery funding requirements."""
|
|
1566
|
+
|
|
1567
|
+
try:
|
|
1568
|
+
output = operate.wallet_recovery_manager.recovery_requirements()
|
|
1569
|
+
return JSONResponse(
|
|
1570
|
+
content=output,
|
|
1571
|
+
status_code=HTTPStatus.OK,
|
|
1572
|
+
)
|
|
1573
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1574
|
+
logger.error(
|
|
1575
|
+
f"_recovery_funding_requirements error: {e}\n{traceback.format_exc()}"
|
|
1576
|
+
)
|
|
1577
|
+
return JSONResponse(
|
|
1578
|
+
content={
|
|
1579
|
+
"error": "Failed to retrieve recovery funding requirements. Please check the logs."
|
|
1580
|
+
},
|
|
1581
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
@app.get("/api/wallet/recovery/status")
|
|
1585
|
+
async def _get_recovery_status(request: Request) -> JSONResponse:
|
|
1586
|
+
"""Get recovery status."""
|
|
1587
|
+
|
|
1588
|
+
try:
|
|
1589
|
+
output = operate.wallet_recovery_manager.status()
|
|
1590
|
+
return JSONResponse(
|
|
1591
|
+
content=output,
|
|
1592
|
+
status_code=HTTPStatus.OK,
|
|
1593
|
+
)
|
|
1594
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1595
|
+
logger.error(f"_recovery_status error: {e}\n{traceback.format_exc()}")
|
|
1596
|
+
return JSONResponse(
|
|
1597
|
+
content={
|
|
1598
|
+
"error": "Failed to retrieve recovery status. Please check the logs."
|
|
1599
|
+
},
|
|
1600
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1603
|
+
@app.post("/api/wallet/recovery/complete")
|
|
1604
|
+
async def _wallet_recovery_complete(request: Request) -> JSONResponse:
|
|
1605
|
+
"""Complete wallet recovery."""
|
|
1606
|
+
if operate.user_account is None:
|
|
1607
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
1608
|
+
|
|
1609
|
+
if operate.password:
|
|
1610
|
+
return USER_LOGGED_IN_ERROR
|
|
1611
|
+
|
|
1612
|
+
data = {}
|
|
1613
|
+
if request.headers.get("content-type", "").startswith("application/json"):
|
|
1614
|
+
body = await request.body()
|
|
1615
|
+
if body:
|
|
1616
|
+
data = await request.json()
|
|
1617
|
+
|
|
1618
|
+
raise_if_inconsistent_owners = data.get("require_consistent_owners", True)
|
|
1619
|
+
|
|
1620
|
+
try:
|
|
1621
|
+
operate.wallet_recovery_manager.complete_recovery(
|
|
1622
|
+
raise_if_inconsistent_owners=raise_if_inconsistent_owners,
|
|
1623
|
+
)
|
|
1624
|
+
return JSONResponse(
|
|
1625
|
+
content=operate.wallet_manager.json,
|
|
1626
|
+
status_code=HTTPStatus.OK,
|
|
1627
|
+
)
|
|
1628
|
+
except KeyError as e:
|
|
1629
|
+
logger.error(f"_recovery_complete error: {e}")
|
|
1630
|
+
return JSONResponse(
|
|
1631
|
+
content={"error": f"Failed to complete recovery: {e}"},
|
|
1632
|
+
status_code=HTTPStatus.NOT_FOUND,
|
|
1633
|
+
)
|
|
1634
|
+
except (ValueError, WalletRecoveryError) as e:
|
|
1635
|
+
logger.error(f"_recovery_complete error: {e}")
|
|
1636
|
+
return JSONResponse(
|
|
1637
|
+
content={"error": f"Failed to complete recovery: {e}"},
|
|
1638
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1639
|
+
)
|
|
1640
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1641
|
+
logger.error(f"_recovery_complete error: {e}\n{traceback.format_exc()}")
|
|
1642
|
+
return JSONResponse(
|
|
1643
|
+
content={
|
|
1644
|
+
"error": "Failed to complete recovery. Please check the logs."
|
|
1645
|
+
},
|
|
1646
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1647
|
+
)
|
|
719
1648
|
|
|
720
1649
|
return app
|
|
721
1650
|
|
|
@@ -723,26 +1652,245 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
723
1652
|
@group(name="operate")
|
|
724
1653
|
def _operate() -> None:
|
|
725
1654
|
"""Operate - deploy autonomous services."""
|
|
1655
|
+
logger.info(f"Operate version: {__version__}")
|
|
726
1656
|
|
|
727
1657
|
|
|
728
1658
|
@_operate.command(name="daemon")
|
|
729
1659
|
def _daemon(
|
|
730
1660
|
host: Annotated[str, params.String(help="HTTP server host string")] = "localhost",
|
|
731
1661
|
port: Annotated[int, params.Integer(help="HTTP server port")] = 8000,
|
|
1662
|
+
ssl_keyfile: Annotated[str, params.String(help="Path to SSL key file")] = "",
|
|
1663
|
+
ssl_certfile: Annotated[
|
|
1664
|
+
str, params.String(help="Path to SSL certificate file")
|
|
1665
|
+
] = "",
|
|
732
1666
|
home: Annotated[
|
|
733
1667
|
t.Optional[Path], params.Directory(long_flag="--home", help="Home directory")
|
|
734
1668
|
] = None,
|
|
735
1669
|
) -> None:
|
|
736
1670
|
"""Launch operate daemon."""
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1671
|
+
# try automatically shutdown previous instance before creating the app
|
|
1672
|
+
if TRY_TO_SHUTDOWN_PREVIOUS_INSTANCE:
|
|
1673
|
+
app_single_instance = AppSingleInstance(port)
|
|
1674
|
+
app_single_instance.shutdown_previous_instance()
|
|
1675
|
+
|
|
1676
|
+
app = create_app(home=home)
|
|
1677
|
+
|
|
1678
|
+
config_kwargs = {
|
|
1679
|
+
"app": app,
|
|
1680
|
+
"host": host,
|
|
1681
|
+
"port": port,
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
# Use SSL certificates if ssl_keyfile and ssl_certfile are provided
|
|
1685
|
+
if ssl_keyfile and ssl_certfile:
|
|
1686
|
+
logger.info(f"Using SSL certificates: {ssl_certfile}")
|
|
1687
|
+
config_kwargs.update(
|
|
1688
|
+
{
|
|
1689
|
+
"ssl_keyfile": ssl_keyfile,
|
|
1690
|
+
"ssl_certfile": ssl_certfile,
|
|
1691
|
+
"ssl_version": 2,
|
|
1692
|
+
}
|
|
1693
|
+
)
|
|
1694
|
+
|
|
1695
|
+
server = Server(Config(**config_kwargs))
|
|
1696
|
+
app._server = server # pylint: disable=protected-access
|
|
1697
|
+
server.run()
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
@_operate.command(name="quickstart")
|
|
1701
|
+
def qs_start(
|
|
1702
|
+
config: Annotated[str, params.String(help="Quickstart config file path")],
|
|
1703
|
+
attended: Annotated[
|
|
1704
|
+
str, params.String(help="Run in attended/unattended mode (default: true")
|
|
1705
|
+
] = "true",
|
|
1706
|
+
build_only: Annotated[
|
|
1707
|
+
bool, params.Boolean(help="Only build the service without running it")
|
|
1708
|
+
] = False,
|
|
1709
|
+
skip_dependency_check: Annotated[
|
|
1710
|
+
bool,
|
|
1711
|
+
params.Boolean(help="Will skip the dependencies check for minting the service"),
|
|
1712
|
+
] = False,
|
|
1713
|
+
use_binary: Annotated[
|
|
1714
|
+
bool,
|
|
1715
|
+
params.Boolean(help="Will use the released binary to run the service"),
|
|
1716
|
+
] = False,
|
|
1717
|
+
) -> None:
|
|
1718
|
+
"""Quickstart."""
|
|
1719
|
+
os.environ["ATTENDED"] = attended.lower()
|
|
1720
|
+
operate = OperateApp()
|
|
1721
|
+
operate.setup()
|
|
1722
|
+
run_service(
|
|
1723
|
+
operate=operate,
|
|
1724
|
+
config_path=config,
|
|
1725
|
+
build_only=build_only,
|
|
1726
|
+
skip_dependency_check=skip_dependency_check,
|
|
1727
|
+
use_binary=use_binary,
|
|
1728
|
+
)
|
|
1729
|
+
|
|
1730
|
+
|
|
1731
|
+
@_operate.command(name="quickstop")
|
|
1732
|
+
def qs_stop(
|
|
1733
|
+
config: Annotated[str, params.String(help="Quickstart config file path")],
|
|
1734
|
+
use_binary: Annotated[
|
|
1735
|
+
bool,
|
|
1736
|
+
params.Boolean(help="Will use the released binary to run the service"),
|
|
1737
|
+
] = False,
|
|
1738
|
+
attended: Annotated[
|
|
1739
|
+
str, params.String(help="Run in attended/unattended mode (default: true")
|
|
1740
|
+
] = "true",
|
|
1741
|
+
) -> None:
|
|
1742
|
+
"""Quickstop."""
|
|
1743
|
+
os.environ["ATTENDED"] = attended.lower()
|
|
1744
|
+
operate = OperateApp()
|
|
1745
|
+
operate.setup()
|
|
1746
|
+
stop_service(operate=operate, config_path=config, use_binary=use_binary)
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
@_operate.command(name="terminate")
|
|
1750
|
+
def qs_terminate(
|
|
1751
|
+
config: Annotated[str, params.String(help="Quickstart config file path")],
|
|
1752
|
+
attended: Annotated[
|
|
1753
|
+
str, params.String(help="Run in attended/unattended mode (default: true")
|
|
1754
|
+
] = "true",
|
|
1755
|
+
) -> None:
|
|
1756
|
+
"""Terminate service."""
|
|
1757
|
+
os.environ["ATTENDED"] = attended.lower()
|
|
1758
|
+
operate = OperateApp()
|
|
1759
|
+
operate.setup()
|
|
1760
|
+
terminate_service(operate=operate, config_path=config)
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
@_operate.command(name="claim")
|
|
1764
|
+
def qs_claim(
|
|
1765
|
+
config: Annotated[str, params.String(help="Quickstart config file path")],
|
|
1766
|
+
attended: Annotated[
|
|
1767
|
+
str, params.String(help="Run in attended/unattended mode (default: true")
|
|
1768
|
+
] = "true",
|
|
1769
|
+
) -> None:
|
|
1770
|
+
"""Quickclaim staking rewards."""
|
|
1771
|
+
os.environ["ATTENDED"] = attended.lower()
|
|
1772
|
+
operate = OperateApp()
|
|
1773
|
+
operate.setup()
|
|
1774
|
+
claim_staking_rewards(operate=operate, config_path=config)
|
|
1775
|
+
|
|
1776
|
+
|
|
1777
|
+
@_operate.command(name="reset-configs")
|
|
1778
|
+
def qs_reset_configs(
|
|
1779
|
+
config: Annotated[str, params.String(help="Quickstart config file path")],
|
|
1780
|
+
attended: Annotated[
|
|
1781
|
+
str, params.String(help="Run in attended/unattended mode (default: true")
|
|
1782
|
+
] = "true",
|
|
1783
|
+
) -> None:
|
|
1784
|
+
"""Reset configs."""
|
|
1785
|
+
os.environ["ATTENDED"] = attended.lower()
|
|
1786
|
+
operate = OperateApp()
|
|
1787
|
+
operate.setup()
|
|
1788
|
+
reset_configs(operate=operate, config_path=config)
|
|
1789
|
+
|
|
1790
|
+
|
|
1791
|
+
@_operate.command(name="reset-staking")
|
|
1792
|
+
def qs_reset_staking(
|
|
1793
|
+
config: Annotated[str, params.String(help="Quickstart config file path")],
|
|
1794
|
+
attended: Annotated[
|
|
1795
|
+
str, params.String(help="Run in attended/unattended mode (default: true")
|
|
1796
|
+
] = "true",
|
|
1797
|
+
) -> None:
|
|
1798
|
+
"""Reset staking."""
|
|
1799
|
+
os.environ["ATTENDED"] = attended.lower()
|
|
1800
|
+
operate = OperateApp()
|
|
1801
|
+
operate.setup()
|
|
1802
|
+
reset_staking(operate=operate, config_path=config)
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
@_operate.command(name="reset-password")
|
|
1806
|
+
def qs_reset_password(
|
|
1807
|
+
attended: Annotated[
|
|
1808
|
+
str, params.String(help="Run in attended/unattended mode (default: true")
|
|
1809
|
+
] = "true",
|
|
1810
|
+
) -> None:
|
|
1811
|
+
"""Reset password."""
|
|
1812
|
+
os.environ["ATTENDED"] = attended.lower()
|
|
1813
|
+
operate = OperateApp()
|
|
1814
|
+
operate.setup()
|
|
1815
|
+
reset_password(operate=operate)
|
|
1816
|
+
|
|
1817
|
+
|
|
1818
|
+
@_operate.command(name="analyse-logs")
|
|
1819
|
+
def qs_analyse_logs( # pylint: disable=too-many-arguments
|
|
1820
|
+
config: Annotated[str, params.String(help="Quickstart config file path")],
|
|
1821
|
+
from_dir: Annotated[
|
|
1822
|
+
str,
|
|
1823
|
+
params.String(
|
|
1824
|
+
help="Path to the logs directory. If not provided, it is auto-detected.",
|
|
1825
|
+
default="",
|
|
1826
|
+
),
|
|
1827
|
+
],
|
|
1828
|
+
agent: Annotated[
|
|
1829
|
+
str,
|
|
1830
|
+
params.String(
|
|
1831
|
+
help="The agent name to analyze (default: 'aea_0').", default="aea_0"
|
|
1832
|
+
),
|
|
1833
|
+
],
|
|
1834
|
+
reset_db: Annotated[
|
|
1835
|
+
bool,
|
|
1836
|
+
params.Boolean(
|
|
1837
|
+
help="Use this flag to disable resetting the log database.", default=False
|
|
1838
|
+
),
|
|
1839
|
+
],
|
|
1840
|
+
start_time: Annotated[
|
|
1841
|
+
str,
|
|
1842
|
+
params.String(help="Start time in `YYYY-MM-DD H:M:S,MS` format.", default=""),
|
|
1843
|
+
],
|
|
1844
|
+
end_time: Annotated[
|
|
1845
|
+
str, params.String(help="End time in `YYYY-MM-DD H:M:S,MS` format.", default="")
|
|
1846
|
+
],
|
|
1847
|
+
log_level: Annotated[
|
|
1848
|
+
str,
|
|
1849
|
+
params.String(
|
|
1850
|
+
help="Logging level. (INFO, DEBUG, WARNING, ERROR, CRITICAL)",
|
|
1851
|
+
default="INFO",
|
|
1852
|
+
),
|
|
1853
|
+
],
|
|
1854
|
+
period: Annotated[int, params.Integer(help="Period ID.", default="")],
|
|
1855
|
+
round: Annotated[ # pylint: disable=redefined-builtin
|
|
1856
|
+
str, params.String(help="Round name.", default="")
|
|
1857
|
+
],
|
|
1858
|
+
behaviour: Annotated[str, params.String(help="Behaviour name filter.", default="")],
|
|
1859
|
+
fsm: Annotated[
|
|
1860
|
+
bool, params.Boolean(help="Print only the FSM execution path.", default=False)
|
|
1861
|
+
],
|
|
1862
|
+
include_regex: Annotated[
|
|
1863
|
+
str, params.String(help="Regex pattern to include in the result.", default="")
|
|
1864
|
+
],
|
|
1865
|
+
exclude_regex: Annotated[
|
|
1866
|
+
str, params.String(help="Regex pattern to exclude from the result.", default="")
|
|
1867
|
+
],
|
|
1868
|
+
) -> None:
|
|
1869
|
+
"""Analyse the logs of an agent."""
|
|
1870
|
+
operate = OperateApp()
|
|
1871
|
+
operate.setup()
|
|
1872
|
+
analyse_logs(
|
|
1873
|
+
operate=operate,
|
|
1874
|
+
config_path=config,
|
|
1875
|
+
from_dir=from_dir,
|
|
1876
|
+
agent=agent,
|
|
1877
|
+
reset_db=reset_db,
|
|
1878
|
+
start_time=start_time,
|
|
1879
|
+
end_time=end_time,
|
|
1880
|
+
log_level=log_level,
|
|
1881
|
+
period=period,
|
|
1882
|
+
round=round,
|
|
1883
|
+
behaviour=behaviour,
|
|
1884
|
+
fsm=fsm,
|
|
1885
|
+
include_regex=include_regex,
|
|
1886
|
+
exclude_regex=exclude_regex,
|
|
741
1887
|
)
|
|
742
1888
|
|
|
743
1889
|
|
|
744
1890
|
def main() -> None:
|
|
745
1891
|
"""CLI entry point."""
|
|
1892
|
+
if "freeze_support" in multiprocessing.__dict__:
|
|
1893
|
+
multiprocessing.freeze_support()
|
|
746
1894
|
run(cli=_operate)
|
|
747
1895
|
|
|
748
1896
|
|