olas-operate-middleware 0.10.6__py3-none-any.whl → 0.10.7__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.
@@ -0,0 +1,61 @@
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
+ """This module contains the class to connect to the `RecoveryModule` contract."""
21
+
22
+
23
+ from typing import Any, Dict
24
+
25
+ from aea.configurations.base import PublicId
26
+ from aea.contracts.base import Contract
27
+ from aea.crypto.base import LedgerApi
28
+
29
+
30
+ PUBLIC_ID = PublicId.from_str("valory/recovery_module:0.1.0")
31
+
32
+
33
+ class RecoveryModule(Contract):
34
+ """The RecoveryModule contract"""
35
+
36
+ contract_id = PUBLIC_ID
37
+
38
+ @classmethod
39
+ def get_recover_access_transaction(
40
+ cls,
41
+ ledger_api: LedgerApi,
42
+ contract_address: str,
43
+ owner: str,
44
+ service_id: int,
45
+ raise_on_try: bool = False,
46
+ ) -> Dict[str, Any]:
47
+ """Get the recover access transaction."""
48
+
49
+ tx_params = ledger_api.build_transaction(
50
+ contract_instance=cls.get_instance(
51
+ ledger_api=ledger_api, contract_address=contract_address
52
+ ),
53
+ method_name="recoverAccess",
54
+ method_args={
55
+ "serviceId": service_id,
56
+ },
57
+ tx_args={"sender_address": owner},
58
+ raise_on_try=raise_on_try,
59
+ )
60
+
61
+ return tx_params
@@ -0,0 +1,23 @@
1
+ name: recovery_module
2
+ author: valory
3
+ version: 0.1.0
4
+ type: contract
5
+ description: Recovery module
6
+ license: Apache-2.0
7
+ aea_version: '>=1.0.0, <2.0.0'
8
+ fingerprint:
9
+ __init__.py: bafybeicjlkmxs5ikpgdtgndifstpmmpaixwcbgouvmt6gowuhf5dy3dpgu
10
+ build/RecoveryModule.json: bafybeifsyjdbprcp4kxpijlpqfovxcf7cv2ujapt3d5zn34wby26ldv4ky
11
+ contract.py: bafybeifihpwb3etbrnn2hqdizsp5zolis3ur23slt3q5zctcykm5nlvv7q
12
+ fingerprint_ignore_patterns: []
13
+ contracts: []
14
+ class_name: RecoveryModule
15
+ contract_interface_paths:
16
+ ethereum: build/RecoveryModule.json
17
+ dependencies:
18
+ open-aea-ledger-ethereum:
19
+ version: ==1.60.0
20
+ open-aea-test-autonomy:
21
+ version: ==0.18.3
22
+ web3:
23
+ version: <7,>=6.0.0
@@ -21,64 +21,46 @@
21
21
 
22
22
  import typing as t
23
23
 
24
+ from autonomy.chain.constants import CHAIN_PROFILES, DEFAULT_MULTISEND
25
+
24
26
  from operate.constants import NO_STAKING_PROGRAM_ID, ZERO_ADDRESS
25
27
  from operate.operate_types import Chain, ContractAddresses
26
28
 
27
29
 
28
- CONTRACTS: t.Dict[Chain, ContractAddresses] = {
29
- Chain.GNOSIS: ContractAddresses(
30
- {
31
- "service_manager": "0x04b0007b2aFb398015B76e5f22993a1fddF83644",
32
- "service_registry": "0x9338b5153AE39BB89f50468E608eD9d764B755fD",
33
- "service_registry_token_utility": "0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8",
34
- "gnosis_safe_proxy_factory": "0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE",
35
- "gnosis_safe_same_address_multisig": "0x6e7f594f680f7aBad18b7a63de50F0FeE47dfD06",
36
- "multisend": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
37
- }
38
- ),
39
- Chain.OPTIMISM: ContractAddresses(
40
- {
41
- "service_manager": "0xFbBEc0C8b13B38a9aC0499694A69a10204c5E2aB",
42
- "service_registry": "0x3d77596beb0f130a4415df3D2D8232B3d3D31e44",
43
- "service_registry_token_utility": "0xBb7e1D6Cb6F243D6bdE81CE92a9f2aFF7Fbe7eac",
44
- "gnosis_safe_proxy_factory": "0x5953f21495BD9aF1D78e87bb42AcCAA55C1e896C",
45
- "gnosis_safe_same_address_multisig": "0xb09CcF0Dbf0C178806Aaee28956c74bd66d21f73",
46
- "multisend": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
47
- }
48
- ),
49
- Chain.ETHEREUM: ContractAddresses(
50
- {
51
- "service_manager": "0x2EA682121f815FBcF86EA3F3CaFdd5d67F2dB143",
52
- "service_registry": "0x48b6af7B12C71f09e2fC8aF4855De4Ff54e775cA",
53
- "service_registry_token_utility": "0x3Fb926116D454b95c669B6Bf2E7c3bad8d19affA",
54
- "gnosis_safe_proxy_factory": "0x46C0D07F55d4F9B5Eed2Fc9680B5953e5fd7b461",
55
- "gnosis_safe_same_address_multisig": "0xfa517d01DaA100cB1932FA4345F68874f7E7eF46",
56
- "multisend": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
57
- }
58
- ),
59
- Chain.BASE: ContractAddresses(
60
- {
61
- "service_manager": "0x63e66d7ad413C01A7b49C7FF4e3Bb765C4E4bd1b",
62
- "service_registry": "0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE",
63
- "service_registry_token_utility": "0x34C895f302D0b5cf52ec0Edd3945321EB0f83dd5",
64
- "gnosis_safe_proxy_factory": "0x22bE6fDcd3e29851B29b512F714C328A00A96B83",
65
- "gnosis_safe_same_address_multisig": "0xFbBEc0C8b13B38a9aC0499694A69a10204c5E2aB",
66
- "multisend": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
67
- }
68
- ),
69
- Chain.MODE: ContractAddresses(
30
+ # TODO: Refactor, remove the usage of CONTRACTS and use CHAIN_PROFILES from Open Autonomy instead.
31
+ CHAINS = [
32
+ Chain.ARBITRUM_ONE,
33
+ Chain.BASE,
34
+ Chain.CELO,
35
+ Chain.ETHEREUM,
36
+ Chain.GNOSIS,
37
+ Chain.MODE,
38
+ Chain.OPTIMISM,
39
+ Chain.POLYGON,
40
+ ]
41
+
42
+ CONTRACTS: t.Dict[Chain, ContractAddresses] = {}
43
+ for _chain in CHAINS:
44
+ profile = CHAIN_PROFILES[_chain.value]
45
+ CONTRACTS[_chain] = ContractAddresses(
70
46
  {
71
- "service_manager": "0x63e66d7ad413C01A7b49C7FF4e3Bb765C4E4bd1b",
72
- "service_registry": "0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE",
73
- "service_registry_token_utility": "0x34C895f302D0b5cf52ec0Edd3945321EB0f83dd5",
74
- "gnosis_safe_proxy_factory": "0xBb7e1D6Cb6F243D6bdE81CE92a9f2aFF7Fbe7eac",
75
- "gnosis_safe_same_address_multisig": "0xFbBEc0C8b13B38a9aC0499694A69a10204c5E2aB",
76
- "multisend": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
47
+ "service_registry": profile["service_registry"],
48
+ "service_registry_token_utility": profile["service_registry_token_utility"],
49
+ "service_manager": profile["service_manager_token"],
50
+ "gnosis_safe_proxy_factory": profile["gnosis_safe_proxy_factory"],
51
+ "gnosis_safe_same_address_multisig": profile[
52
+ "gnosis_safe_same_address_multisig"
53
+ ],
54
+ "safe_multisig_with_recovery_module": profile[
55
+ "safe_multisig_with_recovery_module"
56
+ ],
57
+ "recovery_module": profile["recovery_module"],
58
+ "multisend": DEFAULT_MULTISEND,
77
59
  }
78
- ),
79
- }
60
+ )
80
61
 
81
62
  STAKING: t.Dict[Chain, t.Dict[str, str]] = {
63
+ Chain.ARBITRUM_ONE: {},
82
64
  Chain.GNOSIS: {
83
65
  "pearl_alpha": "0xEE9F19b5DF06c7E8Bfc7B28745dcf944C504198A",
84
66
  "pearl_beta": "0xeF44Fb0842DDeF59D37f85D61A1eF492bbA6135d",
@@ -150,6 +132,7 @@ STAKING: t.Dict[Chain, t.Dict[str, str]] = {
150
132
  "modius_alpha_3": "0x9034D0413D122015710f1744A19eFb1d7c2CEB13",
151
133
  "modius_alpha_4": "0x8BcAdb2c291C159F9385964e5eD95a9887302862",
152
134
  },
135
+ Chain.POLYGON: {},
153
136
  }
154
137
 
155
138
 
@@ -167,37 +150,50 @@ DEFAULT_PRIORITY_MECH = { # maps mech marketplace address to its default priori
167
150
 
168
151
  # ERC20 token addresses
169
152
  OLAS: t.Dict[Chain, str] = {
170
- Chain.GNOSIS: "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f",
171
- Chain.OPTIMISM: "0xFC2E6e6BCbd49ccf3A5f029c79984372DcBFE527",
153
+ Chain.ARBITRUM_ONE: "0x064F8B858C2A603e1b106a2039f5446D32dc81c1",
172
154
  Chain.BASE: "0x54330d28ca3357F294334BDC454a032e7f353416",
155
+ Chain.CELO: "0xaCFfAe8e57Ec6E394Eb1b41939A8CF7892DbDc51",
173
156
  Chain.ETHEREUM: "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0",
157
+ Chain.GNOSIS: "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f",
174
158
  Chain.MODE: "0xcfD1D50ce23C46D3Cf6407487B2F8934e96DC8f9",
159
+ Chain.OPTIMISM: "0xFC2E6e6BCbd49ccf3A5f029c79984372DcBFE527",
160
+ Chain.POLYGON: "0xFEF5d947472e72Efbb2E388c730B7428406F2F95",
175
161
  }
176
162
 
177
163
  USDC: t.Dict[Chain, str] = {
178
- Chain.GNOSIS: "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83",
179
- Chain.OPTIMISM: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
164
+ Chain.ARBITRUM_ONE: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
180
165
  Chain.BASE: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
166
+ Chain.CELO: "0xcebA9300f2b948710d2653dD7B07f33A8B32118C",
181
167
  Chain.ETHEREUM: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
168
+ Chain.GNOSIS: "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83",
182
169
  Chain.MODE: "0xd988097fb8612cc24eeC14542bC03424c656005f",
170
+ Chain.OPTIMISM: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
171
+ Chain.POLYGON: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
183
172
  }
184
173
 
185
174
  WRAPPED_NATIVE_ASSET = {
186
- Chain.GNOSIS: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d",
187
- Chain.OPTIMISM: "0x4200000000000000000000000000000000000006",
175
+ Chain.ARBITRUM_ONE: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
188
176
  Chain.BASE: "0x4200000000000000000000000000000000000006",
177
+ Chain.CELO: "0x471EcE3750Da237f93B8E339c536989b8978a438", # Dual token
189
178
  Chain.ETHEREUM: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
179
+ Chain.GNOSIS: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d",
190
180
  Chain.MODE: "0x4200000000000000000000000000000000000006",
181
+ Chain.OPTIMISM: "0x4200000000000000000000000000000000000006",
191
182
  Chain.POLYGON: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
192
- Chain.ARBITRUM_ONE: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
193
183
  }
194
184
 
195
185
  ERC20_TOKENS = [OLAS, USDC, WRAPPED_NATIVE_ASSET]
196
186
 
197
187
  DEFAULT_NEW_SAFE_FUNDS: t.Dict[Chain, t.Dict[str, int]] = {
188
+ Chain.ARBITRUM_ONE: {
189
+ ZERO_ADDRESS: int(1e15 / 4),
190
+ },
198
191
  Chain.BASE: {
199
192
  ZERO_ADDRESS: int(1e15 / 4),
200
193
  },
194
+ Chain.CELO: {
195
+ ZERO_ADDRESS: int(1e18),
196
+ },
201
197
  Chain.ETHEREUM: {
202
198
  ZERO_ADDRESS: int(1e15 / 4),
203
199
  },
@@ -210,21 +206,35 @@ DEFAULT_NEW_SAFE_FUNDS: t.Dict[Chain, t.Dict[str, int]] = {
210
206
  Chain.OPTIMISM: {
211
207
  ZERO_ADDRESS: int(1e15 / 4),
212
208
  },
209
+ Chain.POLYGON: {
210
+ ZERO_ADDRESS: int(1e18),
211
+ },
213
212
  }
214
213
 
215
214
  DEFAULT_MASTER_EOA_FUNDS = {
215
+ Chain.ARBITRUM_ONE: {ZERO_ADDRESS: 5_000_000_000_000_000},
216
216
  Chain.BASE: {ZERO_ADDRESS: 5_000_000_000_000_000},
217
+ Chain.CELO: {ZERO_ADDRESS: 1_500_000_000_000_000_000},
217
218
  Chain.ETHEREUM: {ZERO_ADDRESS: 20_000_000_000_000_000},
218
219
  Chain.GNOSIS: {ZERO_ADDRESS: 1_500_000_000_000_000_000},
219
220
  Chain.MODE: {ZERO_ADDRESS: 500_000_000_000_000},
220
221
  Chain.OPTIMISM: {ZERO_ADDRESS: 5_000_000_000_000_000},
222
+ Chain.POLYGON: {ZERO_ADDRESS: 1_500_000_000_000_000_000},
221
223
  }
222
224
 
223
225
  EXPLORER_URL = {
226
+ Chain.ARBITRUM_ONE: {
227
+ "tx": "https://arbiscan.io/tx/{tx_hash}",
228
+ "address": "https://arbiscan.io/address/{address}",
229
+ },
224
230
  Chain.BASE: {
225
231
  "tx": "https://basescan.org/tx/{tx_hash}",
226
232
  "address": "https://basescan.org/address/{address}",
227
233
  },
234
+ Chain.CELO: {
235
+ "tx": "https://celoscan.io/tx/{tx_hash}",
236
+ "address": "https://celoscan.io/address/{address}",
237
+ },
228
238
  Chain.ETHEREUM: {
229
239
  "tx": "https://etherscan.io/tx/{tx_hash}",
230
240
  "address": "https://etherscan.io/address/{address}",
@@ -234,13 +244,17 @@ EXPLORER_URL = {
234
244
  "address": "https://gnosisscan.io/address/{address}",
235
245
  },
236
246
  Chain.MODE: {
237
- "tx": "https://modescan.io/tx/{tx_hash}",
238
- "address": "https://modescan.io/address/{address}",
247
+ "tx": "https://explorer.mode.network/tx/{tx_hash}",
248
+ "address": "https://explorer.mode.network/address/{address}",
239
249
  },
240
250
  Chain.OPTIMISM: {
241
251
  "tx": "https://optimistic.etherscan.io/tx/{tx_hash}",
242
252
  "address": "https://optimistic.etherscan.io/address/{address}",
243
253
  },
254
+ Chain.POLYGON: {
255
+ "tx": "https://polygonscan.com/tx/{tx_hash}",
256
+ "address": "https://polygonscan.com/address/{address}",
257
+ },
244
258
  }
245
259
 
246
260
 
operate/operate_types.py CHANGED
@@ -154,6 +154,8 @@ class ContractAddresses(TypedDict):
154
154
  service_registry_token_utility: str
155
155
  gnosis_safe_proxy_factory: str
156
156
  gnosis_safe_same_address_multisig: str
157
+ safe_multisig_with_recovery_module: str
158
+ recovery_module: str
157
159
  multisend: str
158
160
 
159
161
 
@@ -23,6 +23,7 @@ import asyncio
23
23
  import json
24
24
  import logging
25
25
  import os
26
+ import tempfile
26
27
  import traceback
27
28
  import typing as t
28
29
  from collections import Counter, defaultdict
@@ -33,10 +34,11 @@ from pathlib import Path
33
34
 
34
35
  import requests
35
36
  from aea.helpers.base import IPFSHash
36
- from aea_ledger_ethereum import LedgerApi
37
+ from aea_ledger_ethereum import EthereumCrypto, LedgerApi
37
38
  from autonomy.chain.base import registry_contracts
38
39
  from autonomy.chain.config import CHAIN_PROFILES, ChainType
39
40
  from autonomy.chain.metadata import IPFS_URI_PREFIX
41
+ from web3 import Web3
40
42
 
41
43
  from operate.constants import IPFS_ADDRESS, ZERO_ADDRESS
42
44
  from operate.data import DATA_DIR
@@ -624,6 +626,8 @@ class ServiceManager:
624
626
  # TODO fix this
625
627
  os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc
626
628
 
629
+ self._enable_recovery_module(service_config_id=service_config_id, chain=chain)
630
+
627
631
  current_agent_id = None
628
632
  on_chain_state = OnChainState.NON_EXISTENT
629
633
  if chain_data.token > -1:
@@ -829,6 +833,11 @@ class ServiceManager:
829
833
  self._get_on_chain_state(service=service, chain=chain)
830
834
  == OnChainState.PRE_REGISTRATION
831
835
  ):
836
+ self.logger.info("Execute recovery module operations")
837
+ self._execute_recovery_module_flow_from_safe(
838
+ service_config_id=service_config_id, chain=chain
839
+ )
840
+
832
841
  self.logger.info("Updating service")
833
842
  receipt = (
834
843
  sftxb.new_tx()
@@ -1047,6 +1056,7 @@ class ServiceManager:
1047
1056
  )
1048
1057
  ).settle()
1049
1058
 
1059
+ # Deploy service
1050
1060
  if (
1051
1061
  self._get_on_chain_state(service=service, chain=chain)
1052
1062
  == OnChainState.FINISHED_REGISTRATION
@@ -1055,15 +1065,27 @@ class ServiceManager:
1055
1065
 
1056
1066
  reuse_multisig = True
1057
1067
  info = sftxb.info(token_id=chain_data.token)
1058
- if info["multisig"] == ZERO_ADDRESS:
1068
+ service_safe_address = info["multisig"]
1069
+ if service_safe_address == ZERO_ADDRESS:
1059
1070
  reuse_multisig = False
1060
1071
 
1061
1072
  self.logger.info(f"{reuse_multisig=}")
1062
1073
 
1074
+ is_recovery_module_enabled = (
1075
+ registry_contracts.gnosis_safe.is_module_enabled(
1076
+ ledger_api=sftxb.ledger_api,
1077
+ contract_address=service_safe_address,
1078
+ module_address=CONTRACTS[Chain(chain)]["recovery_module"],
1079
+ ).get("enabled")
1080
+ )
1081
+
1082
+ self.logger.info(f"{is_recovery_module_enabled=}")
1083
+
1063
1084
  messages = sftxb.get_deploy_data_from_safe(
1064
1085
  service_id=chain_data.token,
1065
1086
  reuse_multisig=reuse_multisig,
1066
1087
  master_safe=safe,
1088
+ use_recovery_module=is_recovery_module_enabled,
1067
1089
  )
1068
1090
  tx = sftxb.new_tx()
1069
1091
  for message in messages:
@@ -1198,6 +1220,9 @@ class ServiceManager:
1198
1220
  wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
1199
1221
  safe = wallet.safes[Chain(chain)] # type: ignore
1200
1222
 
1223
+ if withdrawal_address:
1224
+ withdrawal_address = Web3.to_checksum_address(withdrawal_address)
1225
+
1201
1226
  # TODO fixme
1202
1227
  os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc
1203
1228
 
@@ -1289,6 +1314,9 @@ class ServiceManager:
1289
1314
  },
1290
1315
  )
1291
1316
 
1317
+ self._enable_recovery_module(
1318
+ service_config_id=service_config_id, chain=chain
1319
+ )
1292
1320
  self.logger.info("Swapping Safe owners")
1293
1321
  sftxb.swap( # noqa: E800
1294
1322
  service_id=chain_data.token, # noqa: E800
@@ -1318,6 +1346,156 @@ class ServiceManager:
1318
1346
  )
1319
1347
  self.logger.info(f"{service.name} signer drained")
1320
1348
 
1349
+ def _execute_recovery_module_flow_from_safe( # pylint: disable=too-many-locals
1350
+ self,
1351
+ service_config_id: str,
1352
+ chain: str,
1353
+ ) -> None:
1354
+ """Execute recovery module operations from Safe"""
1355
+ self.logger.info(f"_execute_recovery_module_operations_from_safe {chain=}")
1356
+ service = self.load(service_config_id=service_config_id)
1357
+ chain_config = service.chain_configs[chain]
1358
+ chain_data = chain_config.chain_data
1359
+ ledger_config = chain_config.ledger_config
1360
+ wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
1361
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
1362
+ safe = wallet.safes[Chain(chain)]
1363
+
1364
+ if chain_data.token == NON_EXISTENT_TOKEN:
1365
+ self.logger.info("Service is not minted.")
1366
+ return
1367
+
1368
+ info = sftxb.info(token_id=chain_data.token)
1369
+ service_safe_address = info["multisig"]
1370
+ on_chain_state = OnChainState(info["service_state"])
1371
+
1372
+ if service_safe_address == ZERO_ADDRESS:
1373
+ self.logger.info("Service Safe is not deployed.")
1374
+ return
1375
+
1376
+ recovery_module_address = CONTRACTS[Chain(chain)]["recovery_module"]
1377
+ is_recovery_module_enabled = registry_contracts.gnosis_safe.is_module_enabled(
1378
+ ledger_api=sftxb.ledger_api,
1379
+ contract_address=service_safe_address,
1380
+ module_address=recovery_module_address,
1381
+ ).get("enabled")
1382
+
1383
+ service_safe_owners = sftxb.get_service_safe_owners(service_id=chain_data.token)
1384
+ master_safe_is_service_safe_owner = service_safe_owners == [safe]
1385
+
1386
+ self.logger.info(f"{is_recovery_module_enabled=}")
1387
+ self.logger.info(f"{master_safe_is_service_safe_owner=}")
1388
+
1389
+ if not is_recovery_module_enabled and not master_safe_is_service_safe_owner:
1390
+ self.logger.info(
1391
+ "Recovery module is not enabled and Master Safe is not service Safe owner. Skipping recovery operations."
1392
+ )
1393
+ return
1394
+
1395
+ if not is_recovery_module_enabled:
1396
+ self._enable_recovery_module(
1397
+ service_config_id=service_config_id, chain=chain
1398
+ )
1399
+
1400
+ if (
1401
+ not master_safe_is_service_safe_owner
1402
+ and on_chain_state == OnChainState.PRE_REGISTRATION
1403
+ ):
1404
+ self.logger.info("Recovering service Safe access through recovery module.")
1405
+ sftxb.new_tx().add(
1406
+ sftxb.get_recover_access_data(
1407
+ service_id=chain_data.token,
1408
+ )
1409
+ ).settle()
1410
+ self.logger.info("Recovering service Safe done.")
1411
+
1412
+ def _enable_recovery_module( # pylint: disable=too-many-locals
1413
+ self,
1414
+ service_config_id: str,
1415
+ chain: str,
1416
+ ) -> None:
1417
+ """Enable recovery module"""
1418
+ self.logger.info(f"_enable_recovery_module {chain=}")
1419
+ service = self.load(service_config_id=service_config_id)
1420
+ chain_config = service.chain_configs[chain]
1421
+ chain_data = chain_config.chain_data
1422
+ ledger_config = chain_config.ledger_config
1423
+ wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
1424
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
1425
+ safe = wallet.safes[Chain(chain)]
1426
+
1427
+ if chain_data.token == NON_EXISTENT_TOKEN:
1428
+ self.logger.info("Service is not minted.")
1429
+ return
1430
+
1431
+ info = sftxb.info(token_id=chain_data.token)
1432
+ service_safe_address = info["multisig"]
1433
+
1434
+ if service_safe_address == ZERO_ADDRESS:
1435
+ self.logger.info("Service Safe is not deployed.")
1436
+ return
1437
+
1438
+ recovery_module_address = CONTRACTS[Chain(chain)]["recovery_module"]
1439
+ is_recovery_module_enabled = registry_contracts.gnosis_safe.is_module_enabled(
1440
+ ledger_api=sftxb.ledger_api,
1441
+ contract_address=service_safe_address,
1442
+ module_address=recovery_module_address,
1443
+ ).get("enabled")
1444
+
1445
+ if is_recovery_module_enabled:
1446
+ self.logger.info("Recovery module is already enabled in service Safe.")
1447
+ return
1448
+
1449
+ self.logger.info("Recovery module is not enabled.")
1450
+
1451
+ # NOTE Recovery from agent only works for single-agent services
1452
+ agent_address = service.agent_addresses[0]
1453
+ service_safe_owners = sftxb.get_service_safe_owners(service_id=chain_data.token)
1454
+ agent_is_service_safe_owner = service_safe_owners == [agent_address]
1455
+ master_safe_is_service_safe_owner = service_safe_owners == [safe]
1456
+
1457
+ if agent_is_service_safe_owner:
1458
+ self.logger.info("(Agent) Enabling recovery module in service Safe.")
1459
+ try:
1460
+ with tempfile.NamedTemporaryFile(mode="w+", delete=True) as tmp_file:
1461
+ private_key = self.keys_manager.get(key=agent_address).private_key
1462
+ tmp_file.write(private_key)
1463
+ tmp_file.flush()
1464
+ crypto = EthereumCrypto(private_key_path=tmp_file.name)
1465
+ EthSafeTxBuilder._new_tx( # pylint: disable=protected-access
1466
+ ledger_api=sftxb.ledger_api,
1467
+ crypto=crypto,
1468
+ chain_type=ChainType(chain),
1469
+ safe=service_safe_address,
1470
+ ).add(
1471
+ sftxb.get_enable_module_data(
1472
+ module_address=recovery_module_address,
1473
+ safe_address=service_safe_address,
1474
+ )
1475
+ ).settle()
1476
+ tmp_file.seek(0)
1477
+ tmp_file.write("\0" * len(private_key))
1478
+ tmp_file.flush()
1479
+
1480
+ self.logger.info(
1481
+ "(Agent) Recovery module enabled successfully in service Safe."
1482
+ )
1483
+ except Exception as e: # pylint: disable=broad-except
1484
+ self.logger.error(
1485
+ f"Failed to enable recovery module in service Safe. Exception {e}: {traceback.format_exc()}"
1486
+ )
1487
+ elif master_safe_is_service_safe_owner:
1488
+ # TODO Enable recovery module when Safe owner = master Safe.
1489
+ # This should be similar to the above code, but
1490
+ # requires implement a transaction where the owner is another Safe.
1491
+ self.logger.info(
1492
+ "(Service owner) Enabling recovery module in service Safe. [Not implemented]"
1493
+ )
1494
+ else:
1495
+ self.logger.error(
1496
+ f"Cannot enable recovery module. Safe {service_safe_address} has inconsistent owners."
1497
+ )
1498
+
1321
1499
  def _get_current_staking_program(
1322
1500
  self, service: Service, chain: str
1323
1501
  ) -> t.Optional[str]:
@@ -2009,6 +2187,7 @@ class ServiceManager:
2009
2187
  wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
2010
2188
  ledger_api = wallet.ledger_api(chain=ledger_config.chain, rpc=ledger_config.rpc)
2011
2189
  ethereum_crypto = KeysManager().get_crypto_instance(service.agent_addresses[0])
2190
+ withdrawal_address = Web3.to_checksum_address(withdrawal_address)
2012
2191
 
2013
2192
  # drain ERC20 tokens from service safe
2014
2193
  for token_name, token_address in (