olas-operate-middleware 0.11.4__tar.gz → 0.12.0__tar.gz

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 (100) hide show
  1. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/PKG-INFO +1 -1
  2. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/cli.py +14 -8
  3. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/keys.py +75 -70
  4. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/migration.py +66 -1
  5. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/quickstart/reset_password.py +3 -7
  6. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/resource.py +9 -25
  7. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/services/deployment_runner.py +61 -27
  8. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/services/funding_manager.py +3 -0
  9. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/services/manage.py +2 -1
  10. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/services/service.py +25 -7
  11. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/utils/__init__.py +46 -0
  12. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/pyproject.toml +1 -1
  13. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/LICENSE +0 -0
  14. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/README.md +0 -0
  15. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/__init__.py +0 -0
  16. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/account/__init__.py +0 -0
  17. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/account/user.py +0 -0
  18. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/bridge/bridge_manager.py +0 -0
  19. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/bridge/providers/lifi_provider.py +0 -0
  20. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/bridge/providers/native_bridge_provider.py +0 -0
  21. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/bridge/providers/provider.py +0 -0
  22. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/bridge/providers/relay_provider.py +0 -0
  23. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/constants.py +0 -0
  24. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/README.md +0 -0
  25. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/__init__.py +0 -0
  26. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/__init__.py +0 -0
  27. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/dual_staking_token/__init__.py +0 -0
  28. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/dual_staking_token/build/DualStakingToken.json +0 -0
  29. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/dual_staking_token/contract.py +0 -0
  30. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/dual_staking_token/contract.yaml +0 -0
  31. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/foreign_omnibridge/__init__.py +0 -0
  32. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json +0 -0
  33. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/foreign_omnibridge/contract.py +0 -0
  34. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/foreign_omnibridge/contract.yaml +0 -0
  35. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/home_omnibridge/__init__.py +0 -0
  36. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +0 -0
  37. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/home_omnibridge/contract.py +0 -0
  38. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/home_omnibridge/contract.yaml +0 -0
  39. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/l1_standard_bridge/__init__.py +0 -0
  40. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json +0 -0
  41. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/l1_standard_bridge/contract.py +0 -0
  42. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/l1_standard_bridge/contract.yaml +0 -0
  43. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/l2_standard_bridge/__init__.py +0 -0
  44. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json +0 -0
  45. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/l2_standard_bridge/contract.py +0 -0
  46. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/l2_standard_bridge/contract.yaml +0 -0
  47. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/mech_activity/__init__.py +0 -0
  48. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/mech_activity/build/MechActivity.json +0 -0
  49. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/mech_activity/contract.py +0 -0
  50. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/mech_activity/contract.yaml +0 -0
  51. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/optimism_mintable_erc20/__init__.py +0 -0
  52. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json +0 -0
  53. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/optimism_mintable_erc20/contract.py +0 -0
  54. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/optimism_mintable_erc20/contract.yaml +0 -0
  55. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/recovery_module/__init__.py +0 -0
  56. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/recovery_module/build/RecoveryModule.json +0 -0
  57. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/recovery_module/contract.py +0 -0
  58. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/recovery_module/contract.yaml +0 -0
  59. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/requester_activity_checker/__init__.py +0 -0
  60. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/requester_activity_checker/build/RequesterActivityChecker.json +0 -0
  61. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/requester_activity_checker/contract.py +0 -0
  62. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/requester_activity_checker/contract.yaml +0 -0
  63. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/staking_token/__init__.py +0 -0
  64. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/staking_token/build/StakingToken.json +0 -0
  65. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/staking_token/contract.py +0 -0
  66. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/staking_token/contract.yaml +0 -0
  67. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/uniswap_v2_erc20/__init__.py +0 -0
  68. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/uniswap_v2_erc20/build/IUniswapV2ERC20.json +0 -0
  69. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/uniswap_v2_erc20/contract.py +0 -0
  70. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/uniswap_v2_erc20/contract.yaml +0 -0
  71. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/uniswap_v2_erc20/tests/__init__.py +0 -0
  72. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py +0 -0
  73. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/ledger/__init__.py +0 -0
  74. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/ledger/profiles.py +0 -0
  75. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/operate_http/__init__.py +0 -0
  76. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/operate_http/exceptions.py +0 -0
  77. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/operate_types.py +0 -0
  78. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/pearl.py +0 -0
  79. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/quickstart/analyse_logs.py +0 -0
  80. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/quickstart/claim_staking_rewards.py +0 -0
  81. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/quickstart/reset_configs.py +0 -0
  82. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/quickstart/reset_staking.py +0 -0
  83. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/quickstart/run_service.py +0 -0
  84. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/quickstart/stop_service.py +0 -0
  85. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/quickstart/terminate_on_chain_service.py +0 -0
  86. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/quickstart/utils.py +0 -0
  87. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/services/__init__.py +0 -0
  88. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/services/agent_runner.py +0 -0
  89. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/services/health_checker.py +0 -0
  90. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/services/protocol.py +0 -0
  91. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/services/utils/__init__.py +0 -0
  92. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/services/utils/mech.py +0 -0
  93. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/services/utils/tendermint.py +0 -0
  94. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/settings.py +0 -0
  95. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/utils/gnosis.py +0 -0
  96. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/utils/single_instance.py +0 -0
  97. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/utils/ssl.py +0 -0
  98. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/wallet/__init__.py +0 -0
  99. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/wallet/master.py +0 -0
  100. {olas_operate_middleware-0.11.4 → olas_operate_middleware-0.12.0}/operate/wallet/wallet_recovery_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: olas-operate-middleware
3
- Version: 0.11.4
3
+ Version: 0.12.0
4
4
  Summary:
5
5
  License-File: LICENSE
6
6
  Author: David Vilela
@@ -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:
@@ -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
@@ -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!")
@@ -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)