olas-operate-middleware 0.11.5__py3-none-any.whl → 0.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: olas-operate-middleware
3
- Version: 0.11.5
3
+ Version: 0.12.0
4
4
  Summary:
5
5
  License-File: LICENSE
6
6
  Author: David Vilela
@@ -6,7 +6,7 @@ operate/bridge/providers/lifi_provider.py,sha256=UzAeEnX9FGpnCYYml5lcICeEZeHHqNR
6
6
  operate/bridge/providers/native_bridge_provider.py,sha256=eo_X_mtsRRGFCI9CncZ1DpjLjYlgNqLDCeJ9jvvZ09c,24711
7
7
  operate/bridge/providers/provider.py,sha256=KXp5CITCQ-fSOv2iFMOt8Wer1QGhBvJG0HN5Tnh5Qns,17287
8
8
  operate/bridge/providers/relay_provider.py,sha256=4D2U8jrugh2DJZeSoxLCTVSZe8xMEwdCimqFDtfwWwc,17422
9
- operate/cli.py,sha256=FQdLAbUkXl1Aqgd70rGSLJlsdSfYTm7KDJDAWgVIV1w,67547
9
+ operate/cli.py,sha256=9IuYdcSv4fnLsvGBxwCav2A7pGXUncvT9InGzMchudM,68025
10
10
  operate/constants.py,sha256=FxQL9MmGHD-nNr7UBPb6ItltWtrCLB2fnT6fb4XNlKg,3849
11
11
  operate/data/README.md,sha256=jGPyZTvg2LCGdllvmYxmFMkkkiXb6YWatbqIkcX3kv4,879
12
12
  operate/data/__init__.py,sha256=ttC51Yqk9c4ehpIgs1Qbe7aJvzkrbbdZ1ClaCxJYByE,864
@@ -57,10 +57,10 @@ operate/data/contracts/uniswap_v2_erc20/contract.py,sha256=MwBks4QmZ3XouMT_TqWLn
57
57
  operate/data/contracts/uniswap_v2_erc20/contract.yaml,sha256=XUdz-XtKtmZgLfItbO8usP-QPbtUkAxKGn0hL7OftAg,741
58
58
  operate/data/contracts/uniswap_v2_erc20/tests/__init__.py,sha256=3Arw8dsCsJz6hVOl0t9UjFASHXbV9yp3hw6x4HqgXpU,847
59
59
  operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py,sha256=FzZbw9OTcr_yvjOXpk9YcO-K40eyDARyybcfSHDg2Ps,13392
60
- operate/keys.py,sha256=IyXbKQkcH33021VaOfQC_ub0lHD6uCJaGtwGvjOMRs8,5650
60
+ operate/keys.py,sha256=ufnH4655vj0h7WGYMjPo-nwvVhZWRHdjyw4UYL-McIk,5959
61
61
  operate/ledger/__init__.py,sha256=G0iWcA0Rc-Um8NwpwPJWsQNUQb2tWKyDjzirxPhGc98,6517
62
62
  operate/ledger/profiles.py,sha256=7dxUFzjlT-VNpu9z45qeqPGyz5fDz0bM3LUK9VLcgJk,14272
63
- operate/migration.py,sha256=TJhztfHUVhZ2buKHx10vXeVo1rRAORdbqGE8fLHL15I,17305
63
+ operate/migration.py,sha256=cE0SMZPLW8PG0BsjivYxXoYtSecBR5rRJwBU_9k10hM,19830
64
64
  operate/operate_http/__init__.py,sha256=dxCIVSUos23M4R-PFZZG6k5QrOlEiK0SxhCYSFNxh7U,4711
65
65
  operate/operate_http/exceptions.py,sha256=4UFzrn-GyDD71RhkaOyFPBynL6TrrtP3eywaaU3o4fc,1339
66
66
  operate/operate_types.py,sha256=hGzYW47MPxOQuvDQAD_TqyIHxNReTeDYQI_uKrfJ1Jc,15570
@@ -68,34 +68,34 @@ operate/pearl.py,sha256=yrTpSXLu_ML3qT-uNxq3kScOyo31JyxBujiSMfMUbcg,1690
68
68
  operate/quickstart/analyse_logs.py,sha256=cAeAL2iUy0Po8Eor70tq54-Ibg-Dn8rkuaS167yjE_I,4198
69
69
  operate/quickstart/claim_staking_rewards.py,sha256=K7X1Yq0mxe3qWmFLb1Xu9-Jghhml95lS_LpM_BXii0o,3533
70
70
  operate/quickstart/reset_configs.py,sha256=DVPM4mh6Djunwq16hf8lD9-nGkkm7wVtwr2JUXr1if8,3380
71
- operate/quickstart/reset_password.py,sha256=78riP7zyFM2JMa1H8Dh6pg-PtLJLQS7PFzx9SECPACQ,2571
71
+ operate/quickstart/reset_password.py,sha256=ZlsRUiISQo4GVnIqB0-WmkvLeCW_ck2cRdEjIw5xMNI,2424
72
72
  operate/quickstart/reset_staking.py,sha256=SB5LZq9EctG4SYn2M6oPZ7R7ARHSFLRGzAqfKkpRcy0,5111
73
73
  operate/quickstart/run_service.py,sha256=yej4VRYcOaBJfT-LLqOe6VQ7i38PrRoAarr-U74CkvA,27653
74
74
  operate/quickstart/stop_service.py,sha256=CNcCucI4sqfZG0wTxxh-k14xGcbOh50UGyXqTZVQJP0,2048
75
75
  operate/quickstart/terminate_on_chain_service.py,sha256=5ENU8_mkj06i80lKUX-v1QbLU0YzKeOZDUL1e_jzySE,2914
76
76
  operate/quickstart/utils.py,sha256=jvi7IgPtJEWf7-ciZFyEh_jgNthhv3Pus4VZa_Ha_ms,9221
77
- operate/resource.py,sha256=E59oIVqf6B6nN4LTmf_o2iCgLFAogTLPjm_cK6kMVxg,6305
77
+ operate/resource.py,sha256=MnLdoEV68vQkaoClMFEJqkuxvqBQSIySuKym6h61Pk4,5741
78
78
  operate/services/__init__.py,sha256=isrThS-Ccu5Sc15JZgkN4uTAVaSg-NwUUSDeTyJEqLk,855
79
79
  operate/services/agent_runner.py,sha256=IIN77vD_2lq_hCkai-3dJQ3Ae64GUOQncvv0wxb3qUE,7629
80
- operate/services/deployment_runner.py,sha256=iT3gVZvz94kvd7vwtqJgPv74OJliz5XrgjJ_SNFiuEk,27191
80
+ operate/services/deployment_runner.py,sha256=DHtp-DLOKNCT-8WR76cnuVKVxeCd1ZPY8smCNLaz9Xc,28116
81
81
  operate/services/funding_manager.py,sha256=v7ypERcYhzts2ESK6Wuvy5pckwlyZQ8pd4rfB4ogcC0,38533
82
82
  operate/services/health_checker.py,sha256=r_lMlRZu-UNfqaM-Zo5_cWjsigdqYTAty4h-ISpM7RE,9859
83
- operate/services/manage.py,sha256=PyjrcH46bMtSalbpl25roGZlwg7ZEuGkw8gEbahUzj8,112710
83
+ operate/services/manage.py,sha256=ybpQ0Wqd9SESdhb3S-eqFs-Df7I4shfRcPO3pMHe0FI,112800
84
84
  operate/services/protocol.py,sha256=KJK4jpwWbVqXaCNZNo84Sqxo32SFaxUQ_O8Jwq7im4g,71871
85
- operate/services/service.py,sha256=u_l9nWcfWAqm4lQsHuyXQnOHdZiFvuxaQd1no5IbDV8,43723
85
+ operate/services/service.py,sha256=XojE2r-GaatAl-vmAKWC3qj7YyrfYjmJ6kLCv4JbNwY,44376
86
86
  operate/services/utils/__init__.py,sha256=TvioaZ1mfTRUSCtrQoLNAp4WMVXyqEJqFJM4PxSQCRU,24
87
87
  operate/services/utils/mech.py,sha256=W2x4dqodivNKXjWU-Brp40QhoUHsIMyNAO7-caMoR0Q,3821
88
88
  operate/services/utils/tendermint.py,sha256=3h9nDb2Z89T0RwUr_AaVjqtymQmsu3u6DAVCfL_k1U0,25591
89
89
  operate/settings.py,sha256=0J2E69-Oplo-Ijy-7rzYHc2Q9Xvct-EUMiEdmKKaYOQ,2353
90
- operate/utils/__init__.py,sha256=rGxT0uLZYixofOrTY_MSnTlpfC4FDAxuNDyA4N-GDkM,3576
90
+ operate/utils/__init__.py,sha256=EXZ5SQFszLr4qr5oq9bCJ7L4zdjqP6tSCaoOudHyLBQ,5110
91
91
  operate/utils/gnosis.py,sha256=iyaFw3ZMlNnd1lDulhXfcYxQunPL4Zfhnk1fy20ga7g,19843
92
92
  operate/utils/single_instance.py,sha256=pmtumg0fFDWWcGzXFXQdLXSW54Zq9qBKgJTEPF6pVW8,9092
93
93
  operate/utils/ssl.py,sha256=O5DrDoZD4T4qQuHP8GLwWUVxQ-1qXeefGp6uDJiF2lM,4308
94
94
  operate/wallet/__init__.py,sha256=NGiozD3XhvkBi7_FaOWQ8x1thZPK4uGpokJaeDY_o2w,813
95
95
  operate/wallet/master.py,sha256=gcax6M8_KT2IkL0kGZP4lF4f2updoP5r6ix5e72ZrL4,33674
96
96
  operate/wallet/wallet_recovery_manager.py,sha256=ZOLq0B9irux_og8pLBzaxlRA1r6RqJfZA4j4RX0kzoU,7775
97
- olas_operate_middleware-0.11.5.dist-info/METADATA,sha256=D6lamfDlvLd_IbGCQO-Utacm8xowyMYfg2nsA0c7bXA,2139
98
- olas_operate_middleware-0.11.5.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
99
- olas_operate_middleware-0.11.5.dist-info/entry_points.txt,sha256=dM1g2I7ODApKQFcgl5J4NGA7pfBTo6qsUTXM-j2OLlw,44
100
- olas_operate_middleware-0.11.5.dist-info/licenses/LICENSE,sha256=mdBDB-mWKV5Cz4ejBzBiKqan6Z8zVLAh9xwM64O2FW4,11339
101
- olas_operate_middleware-0.11.5.dist-info/RECORD,,
97
+ olas_operate_middleware-0.12.0.dist-info/METADATA,sha256=ICFX6BSKYEA5u9wtiEQUdFou7MSWzbTKoTqPjddKj5g,2139
98
+ olas_operate_middleware-0.12.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
99
+ olas_operate_middleware-0.12.0.dist-info/entry_points.txt,sha256=dM1g2I7ODApKQFcgl5J4NGA7pfBTo6qsUTXM-j2OLlw,44
100
+ olas_operate_middleware-0.12.0.dist-info/licenses/LICENSE,sha256=mdBDB-mWKV5Cz4ejBzBiKqan6Z8zVLAh9xwM64O2FW4,11339
101
+ olas_operate_middleware-0.12.0.dist-info/RECORD,,
operate/cli.py CHANGED
@@ -64,6 +64,7 @@ from operate.constants import (
64
64
  WALLET_RECOVERY_DIR,
65
65
  ZERO_ADDRESS,
66
66
  )
67
+ from operate.keys import KeysManager
67
68
  from operate.ledger.profiles import (
68
69
  DEFAULT_EOA_TOPUPS,
69
70
  DEFAULT_NEW_SAFE_FUNDS,
@@ -143,7 +144,7 @@ def service_not_found_error(service_config_id: str) -> JSONResponse:
143
144
  )
144
145
 
145
146
 
146
- class OperateApp:
147
+ class OperateApp: # pylint: disable=too-many-instance-attributes
147
148
  """Operate app."""
148
149
 
149
150
  def __init__(
@@ -157,11 +158,13 @@ class OperateApp:
157
158
  self.setup()
158
159
  self._backup_operate_if_new_version()
159
160
 
160
- services.manage.KeysManager(
161
+ self._password: t.Optional[str] = os.environ.get("OPERATE_USER_PASSWORD")
162
+ KeysManager._instances.clear() # reset singleton instance
163
+ self._keys_manager: KeysManager = KeysManager(
161
164
  path=self._keys,
162
165
  logger=logger,
166
+ password=self._password,
163
167
  )
164
- self._password: t.Optional[str] = os.environ.get("OPERATE_USER_PASSWORD")
165
168
  self.settings = Settings(path=self._path)
166
169
 
167
170
  self._wallet_manager = MasterWalletManager(
@@ -174,11 +177,11 @@ class OperateApp:
174
177
  logger=logger,
175
178
  )
176
179
 
177
- mm = MigrationManager(self._path, logger)
178
- mm.migrate_user_account()
179
- mm.migrate_services(self.service_manager())
180
- mm.migrate_wallets(self.wallet_manager)
181
- mm.migrate_qs_configs()
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()
182
185
 
183
186
  @property
184
187
  def password(self) -> t.Optional[str]:
@@ -189,7 +192,9 @@ class OperateApp:
189
192
  def password(self, value: t.Optional[str]) -> None:
190
193
  """Set the password."""
191
194
  self._password = value
195
+ self._keys_manager.password = value
192
196
  self._wallet_manager.password = value
197
+ self._migration_manager.migrate_keys(self._keys_manager)
193
198
 
194
199
  def _backup_operate_if_new_version(self) -> None:
195
200
  """Backup .operate directory if this is a new version."""
@@ -256,6 +261,7 @@ class OperateApp:
256
261
  wallet_manager = self.wallet_manager
257
262
  wallet_manager.password = old_password
258
263
  wallet_manager.update_password(new_password)
264
+ self._keys_manager.update_password(new_password)
259
265
  self.user_account.update(old_password, new_password)
260
266
 
261
267
  def update_password_with_mnemonic(self, mnemonic: str, new_password: str) -> None:
operate/keys.py CHANGED
@@ -21,17 +21,17 @@
21
21
 
22
22
  import json
23
23
  import os
24
- import shutil
25
24
  import tempfile
26
25
  from dataclasses import dataclass
26
+ from logging import Logger
27
27
  from pathlib import Path
28
- from typing import Any
28
+ from typing import Any, Optional
29
29
 
30
30
  from aea_ledger_ethereum.ethereum import EthereumCrypto
31
31
 
32
32
  from operate.operate_types import LedgerType
33
33
  from operate.resource import LocalResource
34
- from operate.utils import SingletonMeta
34
+ from operate.utils import SingletonMeta, unrecoverable_delete
35
35
 
36
36
 
37
37
  @dataclass
@@ -42,6 +42,15 @@ class Key(LocalResource):
42
42
  address: str
43
43
  private_key: str
44
44
 
45
+ def get_decrypted(self, password: str) -> dict:
46
+ """Get decrypted key json."""
47
+ return {
48
+ "ledger": self.ledger.value,
49
+ "address": self.address,
50
+ "private_key": "0x"
51
+ + EthereumCrypto.decrypt(self.private_key, password=password),
52
+ }
53
+
45
54
  @classmethod
46
55
  def load(cls, path: Path) -> "Key":
47
56
  """Load a service"""
@@ -61,13 +70,41 @@ class KeysManager(metaclass=SingletonMeta):
61
70
  if "path" not in kwargs:
62
71
  raise ValueError("Path must be provided for KeysManager")
63
72
 
64
- self.path = kwargs["path"]
65
- self.logger = kwargs["logger"]
73
+ self.path: Path = kwargs["path"]
74
+ self.logger: Logger = kwargs["logger"]
75
+ self.password: Optional[str] = kwargs.get("password")
66
76
  self.path.mkdir(exist_ok=True, parents=True)
67
77
 
78
+ def private_key_to_crypto(
79
+ self, private_key: str, password: Optional[str]
80
+ ) -> EthereumCrypto:
81
+ """Convert private key string to EthereumCrypto instance."""
82
+ with tempfile.NamedTemporaryFile(
83
+ dir=self.path,
84
+ mode="w",
85
+ suffix=".txt",
86
+ delete=False, # Handle cleanup manually
87
+ ) as temp_file:
88
+ temp_file_name = temp_file.name
89
+ temp_file.write(private_key)
90
+ temp_file.flush()
91
+ temp_file.close() # Close the file before reading
92
+
93
+ # Set proper file permissions (readable by owner only)
94
+ os.chmod(temp_file_name, 0o600)
95
+ crypto = EthereumCrypto(private_key_path=temp_file_name, password=password)
96
+
97
+ try:
98
+ unrecoverable_delete(
99
+ Path(temp_file.name)
100
+ ) # Clean up the temporary file
101
+ except OSError as e:
102
+ self.logger.error(f"Failed to delete temp file {temp_file.name}: {e}")
103
+
104
+ return crypto
105
+
68
106
  def get(self, key: str) -> Key:
69
107
  """Get key object."""
70
- KeysManager.migrate_format(self.path / key)
71
108
  return Key.from_json( # type: ignore
72
109
  obj=json.loads(
73
110
  (self.path / key).read_text(
@@ -91,46 +128,20 @@ class KeysManager(metaclass=SingletonMeta):
91
128
 
92
129
  def get_crypto_instance(self, address: str) -> EthereumCrypto:
93
130
  """Get EthereumCrypto instance for the given address."""
94
- key: Key = Key.from_json( # type: ignore
95
- obj=json.loads(
96
- (self.path / address).read_text(
97
- encoding="utf-8",
98
- )
99
- )
100
- )
101
- private_key = key.private_key
102
- # Create temporary file with delete=False to handle it manually
103
- with tempfile.NamedTemporaryFile(
104
- dir=self.path,
105
- mode="w",
106
- suffix=".txt",
107
- delete=False, # Handle cleanup manually
108
- ) as temp_file:
109
- temp_file_name = temp_file.name
110
- temp_file.write(private_key)
111
- temp_file.flush()
112
- temp_file.close() # Close the file before reading
113
-
114
- # Set proper file permissions (readable by owner only)
115
- os.chmod(temp_file_name, 0o600)
116
- crypto = EthereumCrypto(private_key_path=temp_file_name)
117
-
118
- try:
119
- with open(temp_file_name, "r+", encoding="utf-8") as f:
120
- f.seek(0)
121
- f.write("\0" * len(private_key))
122
- f.flush()
123
- f.close()
124
- os.unlink(temp_file_name) # Clean up the temporary file
125
- except OSError as e:
126
- self.logger.error(f"Failed to delete temp file {temp_file.name}: {e}")
127
-
128
- return crypto
131
+ key: Key = self.get(address)
132
+ return self.private_key_to_crypto(key.private_key, self.password)
129
133
 
130
134
  def create(self) -> str:
131
135
  """Creates new key."""
132
136
  self.path.mkdir(exist_ok=True, parents=True)
133
- crypto = EthereumCrypto()
137
+ crypto = EthereumCrypto(password=self.password)
138
+ key = Key(
139
+ ledger=LedgerType.ETHEREUM,
140
+ address=crypto.address,
141
+ private_key=crypto.encrypt(password=self.password)
142
+ if self.password is not None
143
+ else crypto.private_key,
144
+ )
134
145
  for path in (
135
146
  self.path / f"{crypto.address}.bak",
136
147
  self.path / crypto.address,
@@ -140,12 +151,8 @@ class KeysManager(metaclass=SingletonMeta):
140
151
 
141
152
  path.write_text(
142
153
  json.dumps(
143
- Key(
144
- ledger=LedgerType.ETHEREUM,
145
- address=crypto.address,
146
- private_key=crypto.private_key,
147
- ).json,
148
- indent=4,
154
+ key.json,
155
+ indent=2,
149
156
  ),
150
157
  encoding="utf-8",
151
158
  )
@@ -156,25 +163,23 @@ class KeysManager(metaclass=SingletonMeta):
156
163
  """Delete key."""
157
164
  os.remove(self.path / key)
158
165
 
159
- @classmethod
160
- def migrate_format(cls, path: Path) -> bool:
161
- """Migrate the JSON file format if needed."""
162
- migrated = False
163
- backup_path = path.with_suffix(".bak")
164
- if not backup_path.is_file():
165
- shutil.copyfile(path, backup_path)
166
- migrated = True
167
-
168
- with open(path, "r", encoding="utf-8") as file:
169
- data = json.load(file)
170
-
171
- old_to_new_ledgers = {0: "ethereum", 1: "solana"}
172
- if data.get("ledger") in old_to_new_ledgers:
173
- data["ledger"] = old_to_new_ledgers.get(data["ledger"])
174
- migrated = True
175
-
176
- if migrated:
177
- with open(path, "w", encoding="utf-8") as file:
178
- json.dump(data, file, indent=2)
179
-
180
- return migrated
166
+ def update_password(self, new_password: str) -> None:
167
+ """Update password for all keys."""
168
+ for key_file in self.path.iterdir():
169
+ if not key_file.is_file() or key_file.suffix == ".bak":
170
+ continue
171
+
172
+ key = self.get(key_file.name)
173
+ crypto = self.get_crypto_instance(key_file.name)
174
+ encrypted_private_key = crypto.encrypt(password=new_password)
175
+ key.private_key = encrypted_private_key
176
+ key.path = self.path / key_file.name
177
+ key.store()
178
+
179
+ backup_path = self.path / f"{key.address}.bak"
180
+ backup_path.write_text(
181
+ json.dumps(key.json, indent=2),
182
+ encoding="utf-8",
183
+ )
184
+
185
+ self.password = new_password
operate/migration.py CHANGED
@@ -28,8 +28,11 @@ from pathlib import Path
28
28
  from time import time
29
29
 
30
30
  from aea_cli_ipfs.ipfs_utils import IPFSTool
31
+ from aea_ledger_ethereum import EthereumCrypto
32
+ from web3 import Web3
31
33
 
32
34
  from operate.constants import USER_JSON, ZERO_ADDRESS
35
+ from operate.keys import KeysManager
33
36
  from operate.operate_types import Chain, LedgerType
34
37
  from operate.services.manage import ServiceManager
35
38
  from operate.services.service import (
@@ -38,7 +41,7 @@ from operate.services.service import (
38
41
  SERVICE_CONFIG_VERSION,
39
42
  Service,
40
43
  )
41
- from operate.utils import create_backup
44
+ from operate.utils import create_backup, unrecoverable_delete
42
45
  from operate.wallet.master import LEDGER_TYPE_TO_WALLET_CLASS, MasterWalletManager
43
46
 
44
47
 
@@ -454,3 +457,65 @@ class MigrationManager:
454
457
  self.logger.info(
455
458
  "[MIGRATION MANAGER] Migrated quickstart config: %s.", qs_config.name
456
459
  )
460
+
461
+ def migrate_keys(self, keys_manager: KeysManager) -> None:
462
+ """Migrate keys format if needed."""
463
+ self.logger.info("Migrating keys...")
464
+
465
+ for key_file in keys_manager.path.iterdir():
466
+ if (
467
+ not key_file.is_file()
468
+ or key_file.suffix == ".bak"
469
+ or not Web3.is_address(key_file.name)
470
+ ):
471
+ self.logger.warning(f"Skipping non-key file: {key_file}")
472
+ continue
473
+
474
+ migrated = False
475
+ backup_path = key_file.with_suffix(".bak")
476
+
477
+ try:
478
+ with open(key_file, "r", encoding="utf-8") as file:
479
+ data = json.load(file)
480
+ except Exception as e: # pylint: disable=broad-except
481
+ self.logger.error(
482
+ f"Failed to read key file: {key_file}\n"
483
+ f"Key file content:\n{key_file.read_text(encoding='utf-8')}\n"
484
+ f"Exception {e}: {traceback.format_exc()}"
485
+ )
486
+ raise e
487
+
488
+ old_to_new_ledgers = {0: "ethereum", 1: "solana"}
489
+ if data.get("ledger") in old_to_new_ledgers:
490
+ data["ledger"] = old_to_new_ledgers.get(data["ledger"])
491
+ with open(key_file, "w", encoding="utf-8") as file:
492
+ json.dump(data, file, indent=2)
493
+
494
+ migrated = True
495
+
496
+ private_key = data.get("private_key")
497
+ if (
498
+ private_key
499
+ and keys_manager.password is not None
500
+ and private_key.startswith("0x")
501
+ ):
502
+ crypto: EthereumCrypto = keys_manager.private_key_to_crypto(
503
+ private_key=private_key,
504
+ password=None,
505
+ )
506
+ encrypted_private_key = crypto.encrypt(password=keys_manager.password)
507
+ data["private_key"] = encrypted_private_key
508
+ if backup_path.exists():
509
+ unrecoverable_delete(backup_path)
510
+
511
+ migrated = True
512
+
513
+ if migrated:
514
+ with open(key_file, "w", encoding="utf-8") as file:
515
+ json.dump(data, file, indent=2)
516
+
517
+ if not backup_path.exists():
518
+ shutil.copyfile(key_file, backup_path)
519
+
520
+ if migrated:
521
+ self.logger.info(f"Key {key_file.name} has been migrated.")
@@ -22,10 +22,9 @@ from typing import TYPE_CHECKING
22
22
 
23
23
  from operate.account.user import UserAccount
24
24
  from operate.constants import USER_JSON
25
- from operate.operate_types import LedgerType
25
+ from operate.keys import KeysManager
26
26
  from operate.quickstart.run_service import ask_confirm_password
27
27
  from operate.quickstart.utils import ask_or_get_from_env, print_section, print_title
28
- from operate.wallet.master import EthereumMasterWallet
29
28
 
30
29
 
31
30
  if TYPE_CHECKING:
@@ -66,10 +65,7 @@ def reset_password(operate: "OperateApp") -> None:
66
65
 
67
66
  print('Resetting password of "ethereum" wallet...')
68
67
  operate.password = old_password
69
- operate.wallet_manager.password = old_password
70
- wallet: EthereumMasterWallet = operate.wallet_manager.load(
71
- ledger_type=LedgerType.ETHEREUM
72
- )
73
- wallet.update_password(new_password=new_password)
68
+ operate.wallet_manager.update_password(new_password=new_password)
69
+ KeysManager().update_password(new_password=new_password)
74
70
 
75
71
  print_section("Password reset done!")
operate/resource.py CHANGED
@@ -24,12 +24,13 @@ import json
24
24
  import os
25
25
  import platform
26
26
  import shutil
27
- import time
28
27
  import types
29
28
  import typing as t
30
29
  from dataclasses import asdict, is_dataclass
31
30
  from pathlib import Path
32
31
 
32
+ from operate.utils import safe_file_operation
33
+
33
34
 
34
35
  # pylint: disable=too-many-return-statements,no-member
35
36
 
@@ -94,23 +95,6 @@ def deserialize(obj: t.Any, otype: t.Any) -> t.Any:
94
95
  return obj
95
96
 
96
97
 
97
- def _safe_file_operation(operation: t.Callable, *args: t.Any, **kwargs: t.Any) -> None:
98
- """Safely perform file operation with retries on Windows."""
99
- max_retries = 3 if platform.system() == "Windows" else 1
100
-
101
- for attempt in range(max_retries):
102
- try:
103
- operation(*args, **kwargs)
104
- return
105
- except (PermissionError, FileNotFoundError, OSError) as e:
106
- if attempt == max_retries - 1:
107
- raise e
108
-
109
- if platform.system() == "Windows":
110
- # On Windows, wait a bit and retry
111
- time.sleep(0.1)
112
-
113
-
114
98
  class LocalResource:
115
99
  """Initialize local resource."""
116
100
 
@@ -163,13 +147,13 @@ class LocalResource:
163
147
  bak0 = path.with_name(f"{path.name}.0.bak")
164
148
 
165
149
  if path.exists() and not bak0.exists():
166
- _safe_file_operation(shutil.copy2, path, bak0)
150
+ safe_file_operation(shutil.copy2, path, bak0)
167
151
 
168
152
  tmp_path = path.parent / f".{path.name}.tmp"
169
153
 
170
154
  # Clean up any existing tmp file
171
155
  if tmp_path.exists():
172
- _safe_file_operation(tmp_path.unlink)
156
+ safe_file_operation(tmp_path.unlink)
173
157
 
174
158
  tmp_path.write_text(
175
159
  json.dumps(
@@ -181,11 +165,11 @@ class LocalResource:
181
165
 
182
166
  # Atomic replace to avoid corruption
183
167
  try:
184
- _safe_file_operation(os.replace, tmp_path, path)
168
+ safe_file_operation(os.replace, tmp_path, path)
185
169
  except (PermissionError, FileNotFoundError):
186
170
  # On Windows, if the replace fails, clean up and skip
187
171
  if platform.system() == "Windows":
188
- _safe_file_operation(tmp_path.unlink)
172
+ safe_file_operation(tmp_path.unlink)
189
173
 
190
174
  self.load(self.path) # Validate before making backup
191
175
 
@@ -195,7 +179,7 @@ class LocalResource:
195
179
  older = path.with_name(f"{path.name}.{i + 1}.bak")
196
180
  if newer.exists():
197
181
  if older.exists():
198
- _safe_file_operation(older.unlink)
199
- _safe_file_operation(newer.rename, older)
182
+ safe_file_operation(older.unlink)
183
+ safe_file_operation(newer.rename, older)
200
184
 
201
- _safe_file_operation(shutil.copy2, path, bak0)
185
+ safe_file_operation(shutil.copy2, path, bak0)
@@ -56,7 +56,7 @@ class AbstractDeploymentRunner(ABC):
56
56
  self._work_directory = work_directory
57
57
 
58
58
  @abstractmethod
59
- def start(self) -> None:
59
+ def start(self, password: str) -> None:
60
60
  """Start the deployment."""
61
61
 
62
62
  @abstractmethod
@@ -182,7 +182,7 @@ class BaseDeploymentRunner(AbstractDeploymentRunner, metaclass=ABCMeta):
182
182
  )
183
183
  return env
184
184
 
185
- def _setup_agent(self) -> None:
185
+ def _setup_agent(self, password: str) -> None:
186
186
  """Setup agent."""
187
187
  working_dir = self._work_directory
188
188
  env = self._prepare_agent_env()
@@ -223,18 +223,37 @@ class BaseDeploymentRunner(AbstractDeploymentRunner, metaclass=ABCMeta):
223
223
  working_dir / "agent" / "ethereum_private_key.txt",
224
224
  )
225
225
 
226
- self._run_aea_command("-s", "add-key", "ethereum", cwd=working_dir / "agent")
227
226
  self._run_aea_command(
228
- "-s", "add-key", "ethereum", "--connection", cwd=working_dir / "agent"
227
+ "-s",
228
+ "add-key",
229
+ "--password",
230
+ password,
231
+ "ethereum",
232
+ cwd=working_dir / "agent",
233
+ )
234
+ self._run_aea_command(
235
+ "-s",
236
+ "add-key",
237
+ "--password",
238
+ password,
239
+ "ethereum",
240
+ "--connection",
241
+ cwd=working_dir / "agent",
229
242
  )
230
243
 
231
- self._run_aea_command("-s", "issue-certificates", cwd=working_dir / "agent")
244
+ self._run_aea_command(
245
+ "-s",
246
+ "issue-certificates",
247
+ "--password",
248
+ password,
249
+ cwd=working_dir / "agent",
250
+ )
232
251
 
233
- def start(self) -> None:
252
+ def start(self, password: str) -> None:
234
253
  """Start the deployment with retries."""
235
254
  for _ in range(self.START_TRIES):
236
255
  try:
237
- self._start()
256
+ self._start(password=password)
238
257
  return
239
258
  except Exception as e: # pylint: disable=broad-except
240
259
  self.logger.exception(f"Error on starting deployment: {e}")
@@ -242,11 +261,11 @@ class BaseDeploymentRunner(AbstractDeploymentRunner, metaclass=ABCMeta):
242
261
  f"Failed to start the deployment after {self.START_TRIES} attempts! Check logs"
243
262
  )
244
263
 
245
- def _start(self) -> None:
264
+ def _start(self, password: str) -> None:
246
265
  """Start the deployment."""
247
- self._setup_agent()
266
+ self._setup_agent(password=password)
248
267
  self._start_tendermint()
249
- self._start_agent()
268
+ self._start_agent(password=password)
250
269
 
251
270
  def stop(self) -> None:
252
271
  """Stop the deployment."""
@@ -285,7 +304,7 @@ class BaseDeploymentRunner(AbstractDeploymentRunner, metaclass=ABCMeta):
285
304
  """Start tendermint process."""
286
305
 
287
306
  @abstractmethod
288
- def _start_agent(self) -> None:
307
+ def _start_agent(self, password: str) -> None:
289
308
  """Start aea process."""
290
309
 
291
310
  @property
@@ -318,7 +337,7 @@ class PyInstallerHostDeploymentRunner(BaseDeploymentRunner):
318
337
  """Return tendermint path."""
319
338
  return str(Path(os.path.dirname(sys.executable)) / "tendermint_bin") # type: ignore # pylint: disable=protected-access
320
339
 
321
- def _start_agent(self) -> None:
340
+ def _start_agent(self, password: str) -> None:
322
341
  """Start agent process."""
323
342
  working_dir = self._work_directory
324
343
  env = json.loads((working_dir / "agent.json").read_text(encoding="utf-8"))
@@ -326,13 +345,17 @@ class PyInstallerHostDeploymentRunner(BaseDeploymentRunner):
326
345
  env["PYTHONIOENCODING"] = "utf8"
327
346
  env = {**os.environ, **env}
328
347
 
329
- process = self._start_agent_process(env=env, working_dir=working_dir)
348
+ process = self._start_agent_process(
349
+ env=env, working_dir=working_dir, password=password
350
+ )
330
351
  (working_dir / "agent.pid").write_text(
331
352
  data=str(process.pid),
332
353
  encoding="utf-8",
333
354
  )
334
355
 
335
- def _start_agent_process(self, env: Dict, working_dir: Path) -> subprocess.Popen:
356
+ def _start_agent_process(
357
+ self, env: Dict, working_dir: Path, password: str
358
+ ) -> subprocess.Popen:
336
359
  """Start agent process."""
337
360
  raise NotImplementedError
338
361
 
@@ -364,7 +387,9 @@ class PyInstallerHostDeploymentRunner(BaseDeploymentRunner):
364
387
  class PyInstallerHostDeploymentRunnerMac(PyInstallerHostDeploymentRunner):
365
388
  """Mac deployment runner."""
366
389
 
367
- def _start_agent_process(self, env: Dict, working_dir: Path) -> subprocess.Popen:
390
+ def _start_agent_process(
391
+ self, env: Dict, working_dir: Path, password: str
392
+ ) -> subprocess.Popen:
368
393
  """Start agent process."""
369
394
  agent_runner_log_file = self._open_agent_runner_log_file()
370
395
  process = subprocess.Popen( # pylint: disable=consider-using-with,subprocess-popen-preexec-fn # nosec
@@ -372,6 +397,8 @@ class PyInstallerHostDeploymentRunnerMac(PyInstallerHostDeploymentRunner):
372
397
  self._agent_runner_bin,
373
398
  "-s",
374
399
  "run",
400
+ "--password",
401
+ password,
375
402
  ],
376
403
  cwd=working_dir / "agent",
377
404
  stdout=agent_runner_log_file,
@@ -486,7 +513,9 @@ class PyInstallerHostDeploymentRunnerWindows(PyInstallerHostDeploymentRunner):
486
513
  """Return tendermint path."""
487
514
  return str(Path(os.path.dirname(sys.executable)) / "tendermint_win.exe") # type: ignore # pylint: disable=protected-access
488
515
 
489
- def _start_agent_process(self, env: Dict, working_dir: Path) -> subprocess.Popen:
516
+ def _start_agent_process(
517
+ self, env: Dict, working_dir: Path, password: str
518
+ ) -> subprocess.Popen:
490
519
  """Start agent process."""
491
520
  agent_runner_log_file = self._open_agent_runner_log_file()
492
521
  process = subprocess.Popen( # pylint: disable=consider-using-with # nosec
@@ -494,6 +523,8 @@ class PyInstallerHostDeploymentRunnerWindows(PyInstallerHostDeploymentRunner):
494
523
  self._agent_runner_bin,
495
524
  "-s",
496
525
  "run",
526
+ "--password",
527
+ password,
497
528
  ], # TODO: Patch for Windows failing hash
498
529
  cwd=working_dir / "agent",
499
530
  stdout=agent_runner_log_file,
@@ -533,7 +564,7 @@ class HostPythonHostDeploymentRunner(BaseDeploymentRunner):
533
564
  """Return aea_bin path."""
534
565
  return str(self._venv_dir / "bin" / "aea")
535
566
 
536
- def _start_agent(self) -> None:
567
+ def _start_agent(self, password: str) -> None:
537
568
  """Start agent process."""
538
569
  working_dir = self._work_directory
539
570
  env = json.loads((working_dir / "agent.json").read_text(encoding="utf-8"))
@@ -546,6 +577,8 @@ class HostPythonHostDeploymentRunner(BaseDeploymentRunner):
546
577
  self._agent_runner_bin,
547
578
  "-s",
548
579
  "run",
580
+ "--password",
581
+ password,
549
582
  ], # TODO: Patch for Windows failing hash
550
583
  cwd=str(working_dir / "agent"),
551
584
  env={**os.environ, **env},
@@ -613,14 +646,15 @@ class HostPythonHostDeploymentRunner(BaseDeploymentRunner):
613
646
  # Install tendermint dependencies
614
647
  "flask",
615
648
  "requests",
649
+ "multiaddr==0.0.9", # TODO: remove when pinned on open-aea
616
650
  ],
617
651
  )
618
652
 
619
- def _setup_agent(self) -> None:
653
+ def _setup_agent(self, password: str) -> None:
620
654
  """Prepare agent."""
621
655
  multiprocessing.set_start_method("spawn")
622
656
  self._setup_venv()
623
- super()._setup_agent()
657
+ super()._setup_agent(password=password)
624
658
  # Install agent dependencies
625
659
  self._run_cmd(
626
660
  args=[
@@ -707,7 +741,7 @@ class DeploymentManager:
707
741
  "Failed to perform test connection to ipfs to check network connection!"
708
742
  )
709
743
 
710
- def run_deployment(self, build_dir: Path) -> None:
744
+ def run_deployment(self, build_dir: Path, password: str) -> None:
711
745
  """Run deployment."""
712
746
  if self._is_stopping:
713
747
  raise RuntimeError("deployment manager stopped")
@@ -721,7 +755,7 @@ class DeploymentManager:
721
755
  self._states[build_dir] = States.STARTING
722
756
  try:
723
757
  deployment_runner = self._get_deployment_runner(build_dir=build_dir)
724
- deployment_runner.start()
758
+ deployment_runner.start(password=password)
725
759
  self.logger.info(f"Started deployment {build_dir}")
726
760
  self._states[build_dir] = States.STARTED
727
761
  except Exception: # pylint: disable=broad-except
@@ -729,15 +763,15 @@ class DeploymentManager:
729
763
  f"Starting deployment failed {build_dir}. so try to stop"
730
764
  )
731
765
  self._states[build_dir] = States.ERROR
732
- self.stop_deployemnt(build_dir=build_dir, force=True)
766
+ self.stop_deployment(build_dir=build_dir, force=True)
733
767
 
734
768
  if self._is_stopping:
735
769
  self.logger.warning(
736
770
  f"Deployment at {build_dir} started when it was going to stop, so stop it"
737
771
  )
738
- self.stop_deployemnt(build_dir=build_dir, force=True)
772
+ self.stop_deployment(build_dir=build_dir, force=True)
739
773
 
740
- def stop_deployemnt(self, build_dir: Path, force: bool = False) -> None:
774
+ def stop_deployment(self, build_dir: Path, force: bool = False) -> None:
741
775
  """Stop the deployment."""
742
776
  if (
743
777
  self.get_state(build_dir=build_dir) in [States.STARTING, States.STOPPING]
@@ -760,14 +794,14 @@ class DeploymentManager:
760
794
  deployment_manager = DeploymentManager()
761
795
 
762
796
 
763
- def run_host_deployment(build_dir: Path) -> None:
797
+ def run_host_deployment(build_dir: Path, password: str) -> None:
764
798
  """Run host deployment."""
765
- deployment_manager.run_deployment(build_dir=build_dir)
799
+ deployment_manager.run_deployment(build_dir=build_dir, password=password)
766
800
 
767
801
 
768
802
  def stop_host_deployment(build_dir: Path) -> None:
769
803
  """Stop host deployment."""
770
- deployment_manager.stop_deployemnt(build_dir=build_dir)
804
+ deployment_manager.stop_deployment(build_dir=build_dir)
771
805
 
772
806
 
773
807
  def stop_deployment_manager() -> None:
@@ -2232,10 +2232,11 @@ class ServiceManager:
2232
2232
  use_kubernetes=use_kubernetes,
2233
2233
  force=True,
2234
2234
  chain=chain or service.home_chain,
2235
+ password=self.wallet_manager.password,
2235
2236
  )
2236
2237
  if build_only:
2237
2238
  return deployment
2238
- deployment.start(use_docker=use_docker)
2239
+ deployment.start(password=self.wallet_manager.password, use_docker=use_docker)
2239
2240
  return deployment
2240
2241
 
2241
2242
  def stop_service_locally(
@@ -97,6 +97,7 @@ from operate.operate_types import (
97
97
  from operate.resource import LocalResource
98
98
  from operate.services.deployment_runner import run_host_deployment, stop_host_deployment
99
99
  from operate.services.utils import tendermint
100
+ from operate.utils import unrecoverable_delete
100
101
  from operate.utils.gnosis import get_asset_balance
101
102
  from operate.utils.ssl import create_ssl_certificate
102
103
 
@@ -394,7 +395,7 @@ class Deployment(LocalResource):
394
395
  if source_path.exists():
395
396
  shutil.copy(source_path, destination_path)
396
397
 
397
- def _build_kubernetes(self, force: bool = True) -> None:
398
+ def _build_kubernetes(self, password: str, force: bool = True) -> None:
398
399
  """Build kubernetes deployment."""
399
400
  k8s_build = self.path / DEPLOYMENT_DIR / "abci_build_k8s"
400
401
  if k8s_build.exists() and force:
@@ -402,11 +403,23 @@ class Deployment(LocalResource):
402
403
  mkdirs(build_dir=k8s_build)
403
404
 
404
405
  service = Service.load(path=self.path)
406
+ keys_file = self.path / DEFAULT_KEYS_FILE
407
+ keys_file.write_text(
408
+ json.dumps(
409
+ [
410
+ KeysManager().get(address).get_decrypted(password)
411
+ for address in service.agent_addresses
412
+ ],
413
+ indent=4,
414
+ ),
415
+ encoding="utf-8",
416
+ )
405
417
  builder = ServiceBuilder.from_dir(
406
418
  path=service.package_absolute_path,
407
- keys_file=self.path / DEFAULT_KEYS_FILE,
419
+ keys_file=keys_file,
408
420
  number_of_agents=len(service.agent_addresses),
409
421
  )
422
+ unrecoverable_delete(keys_file)
410
423
  builder.deplopyment_type = KubernetesGenerator.deployment_type
411
424
  (
412
425
  KubernetesGenerator(
@@ -424,6 +437,7 @@ class Deployment(LocalResource):
424
437
 
425
438
  def _build_docker(
426
439
  self,
440
+ password: str,
427
441
  force: bool = True,
428
442
  chain: t.Optional[str] = None,
429
443
  ) -> None:
@@ -448,7 +462,7 @@ class Deployment(LocalResource):
448
462
  keys_file.write_text(
449
463
  json.dumps(
450
464
  [
451
- KeysManager().get(address).json
465
+ KeysManager().get(address).get_decrypted(password)
452
466
  for address in service.agent_addresses
453
467
  ],
454
468
  indent=4,
@@ -461,6 +475,7 @@ class Deployment(LocalResource):
461
475
  keys_file=keys_file,
462
476
  number_of_agents=len(service.agent_addresses),
463
477
  )
478
+ unrecoverable_delete(keys_file)
464
479
  builder.deplopyment_type = DockerComposeGenerator.deployment_type
465
480
  builder.try_update_abci_connection_params()
466
481
 
@@ -614,6 +629,7 @@ class Deployment(LocalResource):
614
629
 
615
630
  def build(
616
631
  self,
632
+ password: str,
617
633
  use_docker: bool = False,
618
634
  use_kubernetes: bool = False,
619
635
  force: bool = True,
@@ -649,9 +665,9 @@ class Deployment(LocalResource):
649
665
  )
650
666
  service.consume_env_variables()
651
667
  if use_docker:
652
- self._build_docker(force=force, chain=chain)
668
+ self._build_docker(password=password, force=force, chain=chain)
653
669
  if use_kubernetes:
654
- self._build_kubernetes(force=force)
670
+ self._build_kubernetes(password=password, force=force)
655
671
  else:
656
672
  ssl_key_path, ssl_cert_path = create_ssl_certificate(
657
673
  ssl_dir=service.path / DEPLOYMENT_DIR / "ssl"
@@ -668,7 +684,7 @@ class Deployment(LocalResource):
668
684
  os.environ.clear()
669
685
  os.environ.update(original_env)
670
686
 
671
- def start(self, use_docker: bool = False) -> None:
687
+ def start(self, password: str, use_docker: bool = False) -> None:
672
688
  """Start the service"""
673
689
  if self.status != DeploymentStatus.BUILT:
674
690
  raise NotAllowed(
@@ -686,7 +702,9 @@ class Deployment(LocalResource):
686
702
  project_name=self.path.name,
687
703
  )
688
704
  else:
689
- run_host_deployment(build_dir=self.path / "deployment")
705
+ run_host_deployment(
706
+ build_dir=self.path / "deployment", password=password
707
+ )
690
708
  except Exception:
691
709
  self.status = DeploymentStatus.BUILT
692
710
  self.store()
operate/utils/__init__.py CHANGED
@@ -19,6 +19,8 @@
19
19
 
20
20
  """Helper utilities."""
21
21
 
22
+ import os
23
+ import platform
22
24
  import shutil
23
25
  import time
24
26
  import typing as t
@@ -107,3 +109,47 @@ def subtract_dicts(
107
109
  else:
108
110
  result[key] = max((va or 0) - (vb or 0), 0) # type: ignore
109
111
  return result
112
+
113
+
114
+ def safe_file_operation(operation: t.Callable, *args: t.Any, **kwargs: t.Any) -> None:
115
+ """Safely perform file operation with retries on Windows."""
116
+ max_retries = 3 if platform.system() == "Windows" else 1
117
+
118
+ for attempt in range(max_retries):
119
+ try:
120
+ operation(*args, **kwargs)
121
+ return
122
+ except (PermissionError, FileNotFoundError, OSError) as e:
123
+ if attempt == max_retries - 1:
124
+ raise e
125
+
126
+ if platform.system() == "Windows":
127
+ # On Windows, wait a bit and retry
128
+ time.sleep(0.1)
129
+
130
+
131
+ def unrecoverable_delete(file_path: Path, passes: int = 3) -> None:
132
+ """Delete a file unrecoverably."""
133
+ if not file_path.exists():
134
+ return
135
+
136
+ if not file_path.is_file():
137
+ raise ValueError(f"{file_path} is not a file")
138
+
139
+ try:
140
+ file_size = os.path.getsize(file_path)
141
+
142
+ with open(file_path, "r+b") as f:
143
+ for _ in range(passes):
144
+ # Overwrite with random bytes
145
+ f.seek(0)
146
+ random_data = os.urandom(file_size)
147
+ f.write(random_data)
148
+ f.flush() # Ensure data is written to disk
149
+
150
+ # Finally, delete the file
151
+ safe_file_operation(os.remove, file_path)
152
+ except PermissionError:
153
+ print(f"Permission denied to securely delete file '{file_path}'.")
154
+ except Exception as e: # pylint: disable=broad-except
155
+ print(f"Error during secure deletion of '{file_path}': {e}")