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.
Files changed (98) hide show
  1. olas_operate_middleware-0.13.2.dist-info/METADATA +75 -0
  2. olas_operate_middleware-0.13.2.dist-info/RECORD +101 -0
  3. {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/WHEEL +1 -1
  4. operate/__init__.py +17 -0
  5. operate/account/user.py +35 -9
  6. operate/bridge/bridge_manager.py +470 -0
  7. operate/bridge/providers/lifi_provider.py +377 -0
  8. operate/bridge/providers/native_bridge_provider.py +677 -0
  9. operate/bridge/providers/provider.py +469 -0
  10. operate/bridge/providers/relay_provider.py +457 -0
  11. operate/cli.py +1565 -417
  12. operate/constants.py +60 -12
  13. operate/data/README.md +19 -0
  14. operate/data/contracts/{service_staking_token → dual_staking_token}/__init__.py +2 -2
  15. operate/data/contracts/dual_staking_token/build/DualStakingToken.json +443 -0
  16. operate/data/contracts/dual_staking_token/contract.py +132 -0
  17. operate/data/contracts/dual_staking_token/contract.yaml +23 -0
  18. operate/{ledger/base.py → data/contracts/foreign_omnibridge/__init__.py} +2 -19
  19. operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json +1372 -0
  20. operate/data/contracts/foreign_omnibridge/contract.py +130 -0
  21. operate/data/contracts/foreign_omnibridge/contract.yaml +23 -0
  22. operate/{ledger/solana.py → data/contracts/home_omnibridge/__init__.py} +2 -20
  23. operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +1421 -0
  24. operate/data/contracts/home_omnibridge/contract.py +80 -0
  25. operate/data/contracts/home_omnibridge/contract.yaml +23 -0
  26. operate/data/contracts/l1_standard_bridge/__init__.py +20 -0
  27. operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json +831 -0
  28. operate/data/contracts/l1_standard_bridge/contract.py +158 -0
  29. operate/data/contracts/l1_standard_bridge/contract.yaml +23 -0
  30. operate/data/contracts/l2_standard_bridge/__init__.py +20 -0
  31. operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json +626 -0
  32. operate/data/contracts/l2_standard_bridge/contract.py +130 -0
  33. operate/data/contracts/l2_standard_bridge/contract.yaml +23 -0
  34. operate/data/contracts/mech_activity/__init__.py +20 -0
  35. operate/data/contracts/mech_activity/build/MechActivity.json +111 -0
  36. operate/data/contracts/mech_activity/contract.py +44 -0
  37. operate/data/contracts/mech_activity/contract.yaml +23 -0
  38. operate/data/contracts/optimism_mintable_erc20/__init__.py +20 -0
  39. operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json +491 -0
  40. operate/data/contracts/optimism_mintable_erc20/contract.py +45 -0
  41. operate/data/contracts/optimism_mintable_erc20/contract.yaml +23 -0
  42. operate/data/contracts/recovery_module/__init__.py +20 -0
  43. operate/data/contracts/recovery_module/build/RecoveryModule.json +811 -0
  44. operate/data/contracts/recovery_module/contract.py +61 -0
  45. operate/data/contracts/recovery_module/contract.yaml +23 -0
  46. operate/data/contracts/requester_activity_checker/__init__.py +20 -0
  47. operate/data/contracts/requester_activity_checker/build/RequesterActivityChecker.json +111 -0
  48. operate/data/contracts/requester_activity_checker/contract.py +33 -0
  49. operate/data/contracts/requester_activity_checker/contract.yaml +23 -0
  50. operate/data/contracts/staking_token/__init__.py +20 -0
  51. operate/data/contracts/staking_token/build/StakingToken.json +1336 -0
  52. operate/data/contracts/{service_staking_token → staking_token}/contract.py +27 -13
  53. operate/data/contracts/staking_token/contract.yaml +23 -0
  54. operate/data/contracts/uniswap_v2_erc20/contract.yaml +3 -1
  55. operate/data/contracts/uniswap_v2_erc20/tests/__init__.py +20 -0
  56. operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py +363 -0
  57. operate/keys.py +118 -33
  58. operate/ledger/__init__.py +159 -56
  59. operate/ledger/profiles.py +321 -18
  60. operate/migration.py +555 -0
  61. operate/{http → operate_http}/__init__.py +3 -2
  62. operate/{http → operate_http}/exceptions.py +6 -4
  63. operate/operate_types.py +544 -0
  64. operate/pearl.py +13 -1
  65. operate/quickstart/analyse_logs.py +118 -0
  66. operate/quickstart/claim_staking_rewards.py +104 -0
  67. operate/quickstart/reset_configs.py +106 -0
  68. operate/quickstart/reset_password.py +70 -0
  69. operate/quickstart/reset_staking.py +145 -0
  70. operate/quickstart/run_service.py +726 -0
  71. operate/quickstart/stop_service.py +72 -0
  72. operate/quickstart/terminate_on_chain_service.py +83 -0
  73. operate/quickstart/utils.py +298 -0
  74. operate/resource.py +62 -3
  75. operate/services/agent_runner.py +202 -0
  76. operate/services/deployment_runner.py +868 -0
  77. operate/services/funding_manager.py +929 -0
  78. operate/services/health_checker.py +280 -0
  79. operate/services/manage.py +2356 -620
  80. operate/services/protocol.py +1246 -340
  81. operate/services/service.py +756 -391
  82. operate/services/utils/mech.py +103 -0
  83. operate/services/utils/tendermint.py +86 -12
  84. operate/settings.py +70 -0
  85. operate/utils/__init__.py +135 -0
  86. operate/utils/gnosis.py +407 -80
  87. operate/utils/single_instance.py +226 -0
  88. operate/utils/ssl.py +133 -0
  89. operate/wallet/master.py +708 -123
  90. operate/wallet/wallet_recovery_manager.py +507 -0
  91. olas_operate_middleware-0.1.0rc59.dist-info/METADATA +0 -304
  92. olas_operate_middleware-0.1.0rc59.dist-info/RECORD +0 -41
  93. operate/data/contracts/service_staking_token/build/ServiceStakingToken.json +0 -1273
  94. operate/data/contracts/service_staking_token/contract.yaml +0 -23
  95. operate/ledger/ethereum.py +0 -48
  96. operate/types.py +0 -260
  97. {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/entry_points.txt +0 -0
  98. {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,226 @@
1
+ # -*- coding: utf-8 -*-
2
+ # ------------------------------------------------------------------------------
3
+ #
4
+ # Copyright 2025 Valory AG
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ # ------------------------------------------------------------------------------
19
+
20
+ """Utility module for enforcing single-instance application behavior and monitoring parent process."""
21
+
22
+ import asyncio
23
+ import logging
24
+ import os
25
+ import socket
26
+ import time
27
+ from contextlib import suppress
28
+ from typing import Callable, Optional
29
+
30
+ import psutil
31
+ import requests
32
+
33
+
34
+ class AppSingleInstance:
35
+ """Ensure that only one instance of an application is running."""
36
+
37
+ host = "127.0.0.1"
38
+ after_kill_sleep_time = 1
39
+ proc_kill_wait_timeout = 10
40
+ proc_terminate_wait_timeout = 10
41
+ http_request_timeout = 3
42
+
43
+ def __init__(self, port_number: int, shutdown_endpoint: str = "/shutdown") -> None:
44
+ """Initialize the AppSingleInstance manager."""
45
+ self.port_number = port_number
46
+ self.shutdown_endpoint = shutdown_endpoint
47
+ self.logger = logging.getLogger("app_single_instance")
48
+ self.logger.setLevel(logging.DEBUG)
49
+
50
+ @staticmethod
51
+ def is_port_in_use(port: int) -> bool:
52
+ """Return True if a given TCP port is currently in use."""
53
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
54
+ return s.connect_ex(("127.0.0.1", port)) == 0
55
+
56
+ def shutdown_previous_instance(self) -> None:
57
+ """Attempt to stop a previously running instance of the application."""
58
+ if not self.is_port_in_use(self.port_number):
59
+ self.logger.info(f"Port {self.port_number} is free. All good.")
60
+ return
61
+
62
+ self.logger.warning(f"Port {self.port_number} is in use. Trying to free it!")
63
+ self.logger.warning(
64
+ f"Trying to stop previous instance via shutdown endpoint: {self.shutdown_endpoint}"
65
+ )
66
+ self.try_shutdown_with_endpoint()
67
+
68
+ if not self.is_port_in_use(self.port_number):
69
+ self.logger.info(f"Port {self.port_number} is free. All good.")
70
+ return
71
+
72
+ self.logger.warning(
73
+ f"Trying to stop previous instance by killing process using port {self.port_number}"
74
+ )
75
+ self.try_kill_proc_using_port()
76
+
77
+ if not self.is_port_in_use(self.port_number):
78
+ self.logger.info(f"Port {self.port_number} is free. All good.")
79
+ return
80
+
81
+ self.logger.error(f"Port {self.port_number} still in use. Cannot continue.")
82
+ raise RuntimeError(f"Port {self.port_number} is in use, cannot continue!")
83
+
84
+ def try_shutdown_with_endpoint(self) -> None:
85
+ """Attempt to gracefully shut down the previous instance via HTTP or HTTPS."""
86
+ try:
87
+ self.logger.warning(
88
+ "Attempting to stop previous instance via HTTPS shutdown endpoint."
89
+ )
90
+ requests.get(
91
+ f"https://{self.host}:{self.port_number}{self.shutdown_endpoint}",
92
+ timeout=self.http_request_timeout,
93
+ verify=False, # nosec
94
+ )
95
+ time.sleep(self.after_kill_sleep_time)
96
+ except requests.exceptions.SSLError:
97
+ self.logger.warning("HTTPS shutdown failed, retrying without SSL.")
98
+ try:
99
+ requests.get(
100
+ f"http://{self.host}:{self.port_number}{self.shutdown_endpoint}",
101
+ timeout=self.http_request_timeout,
102
+ )
103
+ time.sleep(self.after_kill_sleep_time)
104
+ except Exception as e: # pylint: disable=broad-except
105
+ self.logger.error(
106
+ f"Failed to stop previous instance (HTTP). Error: {e}"
107
+ )
108
+ except Exception as e: # pylint: disable=broad-except
109
+ self.logger.error(f"Failed to stop previous instance (HTTPS). Error: {e}")
110
+
111
+ def try_kill_proc_using_port(self) -> None:
112
+ """Attempt to forcibly terminate the process occupying the target port."""
113
+ for conn in psutil.net_connections(kind="tcp"):
114
+ if (
115
+ conn.laddr.port == self.port_number
116
+ and conn.status == psutil.CONN_LISTEN
117
+ ):
118
+ if conn.pid is None:
119
+ self.logger.info(
120
+ f"Process using port {self.port_number} found but PID is None. Cannot continue."
121
+ )
122
+ return
123
+ self.logger.info(
124
+ f"Process using port {self.port_number} found (PID={conn.pid}). Terminating..."
125
+ )
126
+ try:
127
+ self.kill_process_tree(conn.pid)
128
+ time.sleep(self.after_kill_sleep_time)
129
+ return
130
+ except Exception as e: # pylint: disable=broad-except
131
+ self.logger.error(f"Error stopping process {conn.pid}: {e}")
132
+ self.logger.info(f"No process found using port {self.port_number}.")
133
+
134
+ def kill_process_tree(self, pid: int) -> None:
135
+ """Terminate a process and all its child processes."""
136
+ try:
137
+ parent = psutil.Process(pid)
138
+ children = parent.children(recursive=True)
139
+
140
+ for child in children:
141
+ with suppress(psutil.NoSuchProcess):
142
+ child.terminate()
143
+
144
+ _, still_alive = psutil.wait_procs(
145
+ children, timeout=self.proc_terminate_wait_timeout
146
+ )
147
+
148
+ for child in still_alive:
149
+ with suppress(psutil.NoSuchProcess):
150
+ child.kill()
151
+
152
+ parent.terminate()
153
+ try:
154
+ parent.wait(timeout=self.proc_terminate_wait_timeout)
155
+ except psutil.TimeoutExpired:
156
+ parent.kill()
157
+ parent.wait(timeout=self.proc_kill_wait_timeout)
158
+
159
+ except psutil.NoSuchProcess:
160
+ self.logger.info(f"Process {pid} already terminated.")
161
+ except Exception as e: # pylint: disable=broad-except
162
+ self.logger.error(f"Error killing process {pid}: {e}")
163
+
164
+
165
+ logger = logging.getLogger("parent_watchdog")
166
+
167
+
168
+ class ParentWatchdog:
169
+ """Monitor the parent process and trigger a shutdown when it exits."""
170
+
171
+ def __init__(
172
+ self, on_parent_exit: Callable[[], asyncio.Future], check_interval: int = 3
173
+ ) -> None:
174
+ """Initialize the ParentWatchdog."""
175
+ self.on_parent_exit = on_parent_exit
176
+ self.check_interval = check_interval
177
+ self._task: Optional[asyncio.Task] = None
178
+ self._stopping = False
179
+
180
+ async def _watch_loop(self) -> None:
181
+ """Continuously monitor the parent process and invoke the shutdown callback when it exits."""
182
+ try:
183
+ own_pid = os.getpid()
184
+ logger.info(f"ParentWatchdog started (pid={own_pid}, ppid={os.getppid()})")
185
+
186
+ while not self._stopping:
187
+ try:
188
+ parent = psutil.Process(os.getppid())
189
+ if not parent.is_running() or os.getppid() == 1:
190
+ logger.warning(
191
+ "Parent process no longer alive, initiating shutdown."
192
+ )
193
+ await self.on_parent_exit()
194
+ break
195
+ except psutil.NoSuchProcess:
196
+ logger.warning("Parent process not found, initiating shutdown.")
197
+ await self.on_parent_exit()
198
+ break
199
+ except Exception: # pylint: disable=broad-except
200
+ logger.exception("Parent check iteration failed.")
201
+ await asyncio.sleep(self.check_interval)
202
+
203
+ except asyncio.CancelledError:
204
+ logger.info("ParentWatchdog task cancelled.")
205
+ except Exception: # pylint: disable=broad-except
206
+ logger.exception("ParentWatchdog crashed unexpectedly.")
207
+ finally:
208
+ logger.info("ParentWatchdog stopped.")
209
+
210
+ def start(self, loop: Optional[asyncio.AbstractEventLoop] = None) -> asyncio.Task:
211
+ """Start monitoring the parent process."""
212
+ if self._task:
213
+ logger.warning("ParentWatchdog already running.")
214
+ return self._task
215
+ loop = loop or asyncio.get_running_loop()
216
+ self._task = loop.create_task(self._watch_loop())
217
+ return self._task
218
+
219
+ async def stop(self) -> None:
220
+ """Stop the parent process watchdog."""
221
+ self._stopping = True
222
+ if self._task:
223
+ with suppress(Exception):
224
+ self._task.cancel()
225
+ await self._task
226
+ self._task = None
operate/utils/ssl.py ADDED
@@ -0,0 +1,133 @@
1
+ # -*- coding: utf-8 -*-
2
+ # ------------------------------------------------------------------------------
3
+ #
4
+ # Copyright 2025 Valory AG
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ # ------------------------------------------------------------------------------
19
+
20
+ """SSL certificate utilities."""
21
+
22
+ import datetime
23
+ import logging
24
+ import typing as t
25
+ from pathlib import Path
26
+
27
+ from cryptography import x509
28
+ from cryptography.hazmat.primitives import hashes, serialization
29
+ from cryptography.hazmat.primitives.asymmetric import rsa
30
+ from cryptography.x509.oid import NameOID
31
+
32
+
33
+ def create_ssl_certificate(
34
+ ssl_dir: Path,
35
+ key_filename: str = "key.pem",
36
+ cert_filename: str = "cert.pem",
37
+ validity_days: int = 365,
38
+ key_size: int = 2048,
39
+ common_name: str = "localhost",
40
+ ) -> t.Tuple[Path, Path]:
41
+ """
42
+ Create SSL certificate and private key files.
43
+
44
+ Args:
45
+ ssl_dir: Path to the ssl directory
46
+ key_filename: Name of the private key file
47
+ cert_filename: Name of the certificate file
48
+ validity_days: Number of days the certificate is valid
49
+ key_size: RSA key size in bits
50
+ common_name: Common name for the certificate
51
+
52
+ Returns:
53
+ Tuple of (key_path, cert_path) as Path objects
54
+ """
55
+ logger = logging.getLogger(__name__)
56
+
57
+ # Create SSL directory
58
+ ssl_dir.mkdir(parents=True, exist_ok=True)
59
+
60
+ key_path = ssl_dir / key_filename
61
+ cert_path = ssl_dir / cert_filename
62
+
63
+ # Generate RSA private key
64
+ private_key = rsa.generate_private_key(
65
+ public_exponent=65537,
66
+ key_size=key_size,
67
+ )
68
+
69
+ # Create certificate subject and issuer
70
+ subject = issuer = x509.Name(
71
+ [
72
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "CH"),
73
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Local"),
74
+ x509.NameAttribute(NameOID.LOCALITY_NAME, "Local"),
75
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Valory AG"),
76
+ x509.NameAttribute(NameOID.COMMON_NAME, common_name),
77
+ ]
78
+ )
79
+
80
+ # Create certificate
81
+ cert = (
82
+ x509.CertificateBuilder()
83
+ .subject_name(subject)
84
+ .issuer_name(issuer)
85
+ .public_key(private_key.public_key())
86
+ .serial_number(1)
87
+ .not_valid_before(datetime.datetime.now(datetime.timezone.utc))
88
+ .not_valid_after(
89
+ datetime.datetime.now(datetime.timezone.utc)
90
+ + datetime.timedelta(days=validity_days)
91
+ )
92
+ .add_extension(
93
+ x509.BasicConstraints(ca=False, path_length=None),
94
+ critical=True,
95
+ )
96
+ .add_extension(
97
+ x509.KeyUsage(
98
+ digital_signature=True,
99
+ key_encipherment=True,
100
+ key_agreement=False,
101
+ key_cert_sign=False,
102
+ crl_sign=False,
103
+ content_commitment=False,
104
+ data_encipherment=False,
105
+ encipher_only=False,
106
+ decipher_only=False,
107
+ ),
108
+ critical=True,
109
+ )
110
+ .add_extension(
111
+ x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]),
112
+ critical=True,
113
+ )
114
+ .sign(private_key, hashes.SHA256())
115
+ )
116
+
117
+ # Write private key to file
118
+ with open(key_path, "wb") as f:
119
+ f.write(
120
+ private_key.private_bytes(
121
+ encoding=serialization.Encoding.PEM,
122
+ format=serialization.PrivateFormat.PKCS8,
123
+ encryption_algorithm=serialization.NoEncryption(),
124
+ )
125
+ )
126
+
127
+ # Write certificate to file
128
+ with open(cert_path, "wb") as f:
129
+ f.write(cert.public_bytes(serialization.Encoding.PEM))
130
+
131
+ logger.info(f"SSL certificate created successfully at {key_path} and {cert_path}")
132
+
133
+ return key_path, cert_path