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
@@ -24,46 +24,61 @@ import contextlib
24
24
  import io
25
25
  import json
26
26
  import logging
27
+ import os
27
28
  import tempfile
28
- import time
29
29
  import typing as t
30
- from datetime import datetime
31
30
  from enum import Enum
31
+ from functools import cache, cached_property
32
32
  from pathlib import Path
33
- from typing import Optional, Union
33
+ from typing import Optional, Union, cast
34
34
 
35
35
  from aea.configurations.data_types import PackageType
36
36
  from aea.crypto.base import Crypto, LedgerApi
37
37
  from aea.helpers.base import IPFSHash, cd
38
- from aea_ledger_ethereum.ethereum import EthereumCrypto
39
38
  from autonomy.chain.base import registry_contracts
40
39
  from autonomy.chain.config import ChainConfigs, ChainType, ContractConfigs
41
40
  from autonomy.chain.constants import (
42
41
  GNOSIS_SAFE_PROXY_FACTORY_CONTRACT,
43
42
  GNOSIS_SAFE_SAME_ADDRESS_MULTISIG_CONTRACT,
43
+ MULTISEND_CONTRACT,
44
+ RECOVERY_MODULE_CONTRACT,
45
+ SAFE_MULTISIG_WITH_RECOVERY_MODULE_CONTRACT,
44
46
  )
47
+ from autonomy.chain.metadata import publish_metadata
45
48
  from autonomy.chain.service import (
46
49
  get_agent_instances,
47
- get_delployment_payload,
48
- get_reuse_multisig_payload,
50
+ get_deployment_payload,
51
+ get_deployment_with_recovery_payload,
49
52
  get_service_info,
53
+ get_token_deposit_amount,
50
54
  )
51
55
  from autonomy.chain.tx import TxSettler
52
- from autonomy.cli.helpers.chain import MintHelper as MintManager
53
- from autonomy.cli.helpers.chain import OnChainHelper
56
+ from autonomy.cli.helpers.chain import MintHelper, OnChainHelper
54
57
  from autonomy.cli.helpers.chain import ServiceHelper as ServiceManager
58
+ from eth_utils import to_bytes
55
59
  from hexbytes import HexBytes
60
+ from web3.contract import Contract
56
61
 
57
62
  from operate.constants import (
63
+ NO_STAKING_PROGRAM_ID,
58
64
  ON_CHAIN_INTERACT_RETRIES,
59
65
  ON_CHAIN_INTERACT_SLEEP,
60
66
  ON_CHAIN_INTERACT_TIMEOUT,
67
+ ZERO_ADDRESS,
61
68
  )
62
69
  from operate.data import DATA_DIR
63
- from operate.data.contracts.service_staking_token.contract import (
64
- ServiceStakingTokenContract,
70
+ from operate.data.contracts.dual_staking_token.contract import DualStakingTokenContract
71
+ from operate.data.contracts.recovery_module.contract import RecoveryModule
72
+ from operate.data.contracts.staking_token.contract import StakingTokenContract
73
+ from operate.ledger import (
74
+ get_default_ledger_api,
75
+ update_tx_with_gas_estimate,
76
+ update_tx_with_gas_pricing,
65
77
  )
66
- from operate.types import ContractAddresses
78
+ from operate.ledger.profiles import CONTRACTS, STAKING
79
+ from operate.operate_types import Chain as OperateChain
80
+ from operate.operate_types import ContractAddresses
81
+ from operate.services.service import NON_EXISTENT_TOKEN
67
82
  from operate.utils.gnosis import (
68
83
  MultiSendOperation,
69
84
  SafeOperation,
@@ -100,14 +115,15 @@ class GnosisSafeTransaction:
100
115
  self.chain_type = chain_type
101
116
  self.safe = safe
102
117
  self._txs: t.List[t.Dict] = []
103
- self.tx: t.Optional[t.Dict] = None
104
118
 
105
119
  def add(self, tx: t.Dict) -> "GnosisSafeTransaction":
106
120
  """Add a transaction"""
107
121
  self._txs.append(tx)
108
122
  return self
109
123
 
110
- def build(self) -> t.Dict:
124
+ def build( # pylint: disable=unused-argument
125
+ self, *args: t.Any, **kwargs: t.Any
126
+ ) -> t.Dict:
111
127
  """Build the transaction."""
112
128
  multisend_data = bytes.fromhex(
113
129
  registry_contracts.multisend.get_tx_data(
@@ -155,53 +171,116 @@ class GnosisSafeTransaction:
155
171
  operation=SafeOperation.DELEGATE_CALL.value,
156
172
  nonce=self.ledger_api.api.eth.get_transaction_count(owner),
157
173
  )
158
- self.tx = self.crypto.sign_transaction(tx)
159
- return t.cast(t.Dict, self.tx)
174
+ update_tx_with_gas_pricing(tx, self.ledger_api)
175
+ update_tx_with_gas_estimate(tx, self.ledger_api)
176
+ return t.cast(t.Dict, tx)
160
177
 
161
178
  def settle(self) -> t.Dict:
162
179
  """Settle the transaction."""
163
- retries = 0
164
- deadline = datetime.now().timestamp() + ON_CHAIN_INTERACT_TIMEOUT
165
- while (
166
- retries < ON_CHAIN_INTERACT_RETRIES
167
- and datetime.now().timestamp() < deadline
168
- ):
169
- try:
170
- self.build()
171
- tx_digest = self.ledger_api.send_signed_transaction(self.tx)
172
- except Exception as e: # pylint: disable=broad-except
173
- print(f"Error sending the safe tx: {e}")
174
- tx_digest = None
175
-
176
- if tx_digest is not None:
177
- receipt = self.ledger_api.api.eth.wait_for_transaction_receipt(
178
- tx_digest
179
- )
180
- if receipt["status"] != 0:
181
- return receipt
182
- time.sleep(ON_CHAIN_INTERACT_SLEEP)
183
- raise RuntimeError("Timeout while waiting for safe transaction to go through")
180
+ tx_settler = TxSettler(
181
+ ledger_api=self.ledger_api,
182
+ crypto=self.crypto,
183
+ chain_type=self.chain_type,
184
+ timeout=ON_CHAIN_INTERACT_TIMEOUT,
185
+ retries=ON_CHAIN_INTERACT_RETRIES,
186
+ sleep=ON_CHAIN_INTERACT_SLEEP,
187
+ )
188
+ setattr(tx_settler, "build", self.build) # noqa: B010
189
+ return tx_settler.transact(
190
+ method=lambda: {},
191
+ contract="",
192
+ kwargs={},
193
+ dry_run=False,
194
+ )
184
195
 
185
196
 
186
- class StakingManager(OnChainHelper):
197
+ class StakingManager:
187
198
  """Helper class for staking a service."""
188
199
 
200
+ staking_ctr = t.cast(
201
+ StakingTokenContract,
202
+ StakingTokenContract.from_dir(
203
+ directory=str(DATA_DIR / "contracts" / "staking_token")
204
+ ),
205
+ )
206
+
207
+ dual_staking_ctr = t.cast(
208
+ DualStakingTokenContract,
209
+ DualStakingTokenContract.from_dir(
210
+ directory=str(DATA_DIR / "contracts" / "dual_staking_token")
211
+ ),
212
+ )
213
+
189
214
  def __init__(
190
215
  self,
191
- key: Path,
192
- chain_type: ChainType = ChainType.CUSTOM,
193
- password: Optional[str] = None,
216
+ chain: OperateChain,
194
217
  ) -> None:
195
218
  """Initialize object."""
196
- super().__init__(key=key, chain_type=chain_type, password=password)
197
- self.staking_ctr = t.cast(
198
- ServiceStakingTokenContract,
199
- ServiceStakingTokenContract.from_dir(
200
- directory=str(DATA_DIR / "contracts" / "service_staking_token")
201
- ),
219
+ self.chain = chain
220
+
221
+ @property
222
+ def ledger_api(self) -> LedgerApi:
223
+ """Get ledger api."""
224
+ return get_default_ledger_api(OperateChain(self.chain.value))
225
+
226
+ @staticmethod
227
+ @cache
228
+ def _get_staking_params(chain: OperateChain, staking_contract: str) -> t.Dict:
229
+ """Get staking params"""
230
+ ledger_api = get_default_ledger_api(chain=chain)
231
+ instance = StakingManager.staking_ctr.get_instance(
232
+ ledger_api=ledger_api,
233
+ contract_address=staking_contract,
234
+ )
235
+ agent_ids = instance.functions.getAgentIds().call()
236
+ service_registry = instance.functions.serviceRegistry().call()
237
+ staking_token = instance.functions.stakingToken().call()
238
+ service_registry_token_utility = (
239
+ instance.functions.serviceRegistryTokenUtility().call()
240
+ )
241
+ min_staking_deposit = instance.functions.minStakingDeposit().call()
242
+ activity_checker = instance.functions.activityChecker().call()
243
+
244
+ output = {
245
+ "staking_contract": staking_contract,
246
+ "agent_ids": agent_ids,
247
+ "service_registry": service_registry,
248
+ "staking_token": staking_token,
249
+ "service_registry_token_utility": service_registry_token_utility,
250
+ "min_staking_deposit": min_staking_deposit,
251
+ "activity_checker": activity_checker,
252
+ "additional_staking_tokens": {},
253
+ }
254
+ try:
255
+ instance = StakingManager.dual_staking_ctr.get_instance(
256
+ ledger_api=ledger_api,
257
+ contract_address=staking_contract,
258
+ )
259
+ output["additional_staking_tokens"][
260
+ instance.functions.secondToken().call()
261
+ ] = instance.functions.secondTokenAmount().call()
262
+ except Exception: # pylint: disable=broad-except # nosec
263
+ # Contract is not a dual staking contract
264
+
265
+ # TODO The exception caught here should be ContractLogicError.
266
+ # This exception is typically raised when the contract reverts with
267
+ # a reason string. However, in some cases, the error message
268
+ # does not contain a reason string, which means web3.py raises
269
+ # a generic ValueError instead. It should be properly analyzed
270
+ # what exceptions might be raised by web3.py in this case. To
271
+ # avoid any issues we are simply catching all exceptions.
272
+ pass
273
+
274
+ return output
275
+
276
+ def get_staking_params(self, staking_contract: str) -> t.Dict:
277
+ """Get staking params"""
278
+ return StakingManager._get_staking_params(
279
+ chain=self.chain,
280
+ staking_contract=staking_contract,
202
281
  )
203
282
 
204
- def status(self, service_id: int, staking_contract: str) -> StakingState:
283
+ def staking_state(self, service_id: int, staking_contract: str) -> StakingState:
205
284
  """Is the service staked?"""
206
285
  return StakingState(
207
286
  self.staking_ctr.get_instance(
@@ -232,13 +311,71 @@ class StakingManager(OnChainHelper):
232
311
  available_rewards = instance.functions.availableRewards().call()
233
312
  return available_rewards
234
313
 
314
+ def claimable_rewards(self, staking_contract: str, service_id: int) -> int:
315
+ """Get the claimable staking rewards on the staking contract"""
316
+ instance = self.staking_ctr.get_instance(
317
+ ledger_api=self.ledger_api,
318
+ contract_address=staking_contract,
319
+ )
320
+ claimable_rewards = instance.functions.calculateStakingReward(service_id).call()
321
+ return claimable_rewards
322
+
235
323
  def service_info(self, staking_contract: str, service_id: int) -> dict:
236
324
  """Get the service onchain info"""
237
- return self.staking_ctr.get_service_info(
238
- self.ledger_api,
239
- staking_contract,
240
- service_id,
241
- ).get("data")
325
+ instance = self.staking_ctr.get_instance(
326
+ ledger_api=self.ledger_api,
327
+ contract_address=staking_contract,
328
+ )
329
+ service_info = instance.functions.getServiceInfo(service_id).call()
330
+ return service_info
331
+
332
+ def agent_ids(self, staking_contract: str) -> t.List[int]:
333
+ """Get a list of agent IDs for the given staking contract."""
334
+ instance = self.staking_ctr.get_instance(
335
+ ledger_api=self.ledger_api,
336
+ contract_address=staking_contract,
337
+ )
338
+ return instance.functions.getAgentIds().call()
339
+
340
+ def service_registry(self, staking_contract: str) -> str:
341
+ """Retrieve the service registry address for the given staking contract."""
342
+ instance = self.staking_ctr.get_instance(
343
+ ledger_api=self.ledger_api,
344
+ contract_address=staking_contract,
345
+ )
346
+ return instance.functions.serviceRegistry().call()
347
+
348
+ def staking_token(self, staking_contract: str) -> str:
349
+ """Get the staking token address for the staking contract."""
350
+ instance = self.staking_ctr.get_instance(
351
+ ledger_api=self.ledger_api,
352
+ contract_address=staking_contract,
353
+ )
354
+ return instance.functions.stakingToken().call()
355
+
356
+ def service_registry_token_utility(self, staking_contract: str) -> str:
357
+ """Get the service registry token utility address for the staking contract."""
358
+ instance = self.staking_ctr.get_instance(
359
+ ledger_api=self.ledger_api,
360
+ contract_address=staking_contract,
361
+ )
362
+ return instance.functions.serviceRegistryTokenUtility().call()
363
+
364
+ def min_staking_deposit(self, staking_contract: str) -> int:
365
+ """Retrieve the minimum staking deposit required for the staking contract."""
366
+ instance = self.staking_ctr.get_instance(
367
+ ledger_api=self.ledger_api,
368
+ contract_address=staking_contract,
369
+ )
370
+ return instance.functions.minStakingDeposit().call()
371
+
372
+ def activity_checker(self, staking_contract: str) -> str:
373
+ """Retrieve the activity checker address for the staking contract."""
374
+ instance = self.staking_ctr.get_instance(
375
+ ledger_api=self.ledger_api,
376
+ contract_address=staking_contract,
377
+ )
378
+ return instance.functions.activityChecker().call()
242
379
 
243
380
  def check_staking_compatibility(
244
381
  self,
@@ -246,7 +383,7 @@ class StakingManager(OnChainHelper):
246
383
  staking_contract: str,
247
384
  ) -> None:
248
385
  """Check if service can be staked."""
249
- status = self.status(service_id, staking_contract)
386
+ status = self.staking_state(service_id, staking_contract)
250
387
  if status == StakingState.STAKED:
251
388
  raise ValueError("Service already staked")
252
389
 
@@ -256,21 +393,28 @@ class StakingManager(OnChainHelper):
256
393
  if not self.slots_available(staking_contract):
257
394
  raise ValueError("No sataking slots available.")
258
395
 
396
+ # TODO To be deprecated, only used in on-chain manager
259
397
  def stake(
260
398
  self,
261
399
  service_id: int,
262
400
  service_registry: str,
263
401
  staking_contract: str,
402
+ key: Path,
403
+ password: str,
264
404
  ) -> None:
265
405
  """Stake the service"""
406
+ och = OnChainHelper(
407
+ key=key, chain_type=ChainType(self.chain.value), password=password
408
+ )
409
+
266
410
  self.check_staking_compatibility(
267
411
  service_id=service_id, staking_contract=staking_contract
268
412
  )
269
413
 
270
414
  tx_settler = TxSettler(
271
- ledger_api=self.ledger_api,
272
- crypto=self.crypto,
273
- chain_type=self.chain_type,
415
+ ledger_api=och.ledger_api,
416
+ crypto=och.crypto,
417
+ chain_type=och.chain_type,
274
418
  timeout=ON_CHAIN_INTERACT_TIMEOUT,
275
419
  retries=ON_CHAIN_INTERACT_RETRIES,
276
420
  sleep=ON_CHAIN_INTERACT_SLEEP,
@@ -279,15 +423,18 @@ class StakingManager(OnChainHelper):
279
423
  # we make use of the ERC20 contract to build the approval transaction
280
424
  # since it has the same interface as ERC721 we might want to create
281
425
  # a ERC721 contract package
426
+ # this is very bad way to do it but it works because the ERC721 contract expects two arguments
427
+ # for approve call (spender, token_id), and the ERC20 contract wrapper used here from open-autonomy
428
+ # passes the amount as the second argument.
282
429
  def _build_approval_tx( # pylint: disable=unused-argument
283
430
  *args: t.Any, **kargs: t.Any
284
431
  ) -> t.Dict:
285
432
  return registry_contracts.erc20.get_approve_tx(
286
- ledger_api=self.ledger_api,
433
+ ledger_api=och.ledger_api,
287
434
  contract_address=service_registry,
288
435
  spender=staking_contract,
289
- sender=self.crypto.address,
290
- amount=service_id,
436
+ sender=och.crypto.address,
437
+ amount=service_id, # TODO: This is a workaround and it should be fixed
291
438
  )
292
439
 
293
440
  setattr(tx_settler, "build", _build_approval_tx) # noqa: B010
@@ -301,15 +448,15 @@ class StakingManager(OnChainHelper):
301
448
  def _build_staking_tx( # pylint: disable=unused-argument
302
449
  *args: t.Any, **kargs: t.Any
303
450
  ) -> t.Dict:
304
- return self.ledger_api.build_transaction(
451
+ return och.ledger_api.build_transaction(
305
452
  contract_instance=self.staking_ctr.get_instance(
306
- ledger_api=self.ledger_api,
453
+ ledger_api=och.ledger_api,
307
454
  contract_address=staking_contract,
308
455
  ),
309
456
  method_name="stake",
310
457
  method_args={"serviceId": service_id},
311
458
  tx_args={
312
- "sender_address": self.crypto.address,
459
+ "sender_address": och.crypto.address,
313
460
  },
314
461
  raise_on_try=True,
315
462
  )
@@ -328,7 +475,7 @@ class StakingManager(OnChainHelper):
328
475
  staking_contract: str,
329
476
  ) -> None:
330
477
  """Check unstaking availability"""
331
- if self.status(
478
+ if self.staking_state(
332
479
  service_id=service_id, staking_contract=staking_contract
333
480
  ) not in {StakingState.STAKED, StakingState.EVICTED}:
334
481
  raise ValueError("Service not staked.")
@@ -346,17 +493,29 @@ class StakingManager(OnChainHelper):
346
493
  self.ledger_api, staking_contract
347
494
  ).get("data"),
348
495
  )
349
- staked_duration = time.time() - ts_start
496
+ current_block = self.ledger_api.api.eth.get_block("latest")
497
+ current_timestamp = current_block.timestamp
498
+ staked_duration = current_timestamp - ts_start
350
499
  if staked_duration < minimum_staking_duration and available_rewards > 0:
351
500
  raise ValueError("Service cannot be unstaked yet.")
352
501
 
353
- def unstake(self, service_id: int, staking_contract: str) -> None:
502
+ # TODO To be deprecated, only used in on-chain manager
503
+ def unstake(
504
+ self,
505
+ service_id: int,
506
+ staking_contract: str,
507
+ key: Path,
508
+ password: str,
509
+ ) -> None:
354
510
  """Unstake the service"""
511
+ och = OnChainHelper(
512
+ key=key, chain_type=ChainType(self.chain.value), password=password
513
+ )
355
514
 
356
515
  tx_settler = TxSettler(
357
- ledger_api=self.ledger_api,
358
- crypto=self.crypto,
359
- chain_type=self.chain_type,
516
+ ledger_api=och.ledger_api,
517
+ crypto=och.crypto,
518
+ chain_type=och.chain_type,
360
519
  timeout=ON_CHAIN_INTERACT_TIMEOUT,
361
520
  retries=ON_CHAIN_INTERACT_RETRIES,
362
521
  sleep=ON_CHAIN_INTERACT_SLEEP,
@@ -365,15 +524,15 @@ class StakingManager(OnChainHelper):
365
524
  def _build_unstaking_tx( # pylint: disable=unused-argument
366
525
  *args: t.Any, **kargs: t.Any
367
526
  ) -> t.Dict:
368
- return self.ledger_api.build_transaction(
527
+ return och.ledger_api.build_transaction(
369
528
  contract_instance=self.staking_ctr.get_instance(
370
- ledger_api=self.ledger_api,
529
+ ledger_api=och.ledger_api,
371
530
  contract_address=staking_contract,
372
531
  ),
373
532
  method_name="unstake",
374
533
  method_args={"serviceId": service_id},
375
534
  tx_args={
376
- "sender_address": self.crypto.address,
535
+ "sender_address": och.crypto.address,
377
536
  },
378
537
  raise_on_try=True,
379
538
  )
@@ -436,6 +595,136 @@ class StakingManager(OnChainHelper):
436
595
  args=[service_id],
437
596
  )
438
597
 
598
+ def get_claim_tx_data(self, service_id: int, staking_contract: str) -> bytes:
599
+ """Claim rewards for the service"""
600
+ return self.staking_ctr.get_instance(
601
+ ledger_api=self.ledger_api,
602
+ contract_address=staking_contract,
603
+ ).encodeABI(
604
+ fn_name="claim",
605
+ args=[service_id],
606
+ )
607
+
608
+ def get_forced_unstake_tx_data(
609
+ self, service_id: int, staking_contract: str
610
+ ) -> bytes:
611
+ """Forced unstake the service"""
612
+ return self.staking_ctr.get_instance(
613
+ ledger_api=self.ledger_api,
614
+ contract_address=staking_contract,
615
+ ).encodeABI(
616
+ fn_name="forcedUnstake",
617
+ args=[service_id],
618
+ )
619
+
620
+ def get_staking_contract(
621
+ self, staking_program_id: t.Optional[str]
622
+ ) -> t.Optional[str]:
623
+ """Get staking contract based on the config and the staking program."""
624
+ if staking_program_id == NO_STAKING_PROGRAM_ID or staking_program_id is None:
625
+ return None
626
+
627
+ return STAKING[self.chain].get(
628
+ staking_program_id,
629
+ staking_program_id,
630
+ )
631
+
632
+ def get_current_staking_program(self, service_id: int) -> t.Optional[str]:
633
+ """Get the current staking program of a service"""
634
+ ledger_api = self.ledger_api
635
+
636
+ if service_id == NON_EXISTENT_TOKEN:
637
+ return None
638
+
639
+ service_registry = registry_contracts.service_registry.get_instance(
640
+ ledger_api=ledger_api,
641
+ contract_address=CONTRACTS[self.chain]["service_registry"],
642
+ )
643
+
644
+ service_owner = service_registry.functions.ownerOf(service_id).call()
645
+
646
+ try:
647
+ state = self.staking_state(
648
+ service_id=service_id, staking_contract=service_owner
649
+ )
650
+
651
+ except Exception: # pylint: disable=broad-except
652
+ # Service owner is not a staking contract
653
+
654
+ # TODO The exception caught here should be ContractLogicError.
655
+ # This exception is typically raised when the contract reverts with
656
+ # a reason string. However, in some cases, the error message
657
+ # does not contain a reason string, which means web3.py raises
658
+ # a generic ValueError instead. It should be properly analyzed
659
+ # what exceptions might be raised by web3.py in this case. To
660
+ # avoid any issues we are simply catching all exceptions.
661
+ return None
662
+
663
+ if state == StakingState.UNSTAKED:
664
+ return None
665
+
666
+ for staking_program_id, val in STAKING[self.chain].items():
667
+ if val == service_owner:
668
+ return staking_program_id
669
+
670
+ # Fallback, if not possible to determine staking_program_id it means it's an "inner" staking contract
671
+ # (e.g., in the case of DualStakingToken). Loop trough all the known contracts.
672
+ for staking_program_id, staking_program_address in STAKING[self.chain].items():
673
+ state = self.staking_state(
674
+ service_id=service_id, staking_contract=staking_program_address
675
+ )
676
+ if state in (StakingState.STAKED, StakingState.EVICTED):
677
+ return staking_program_id
678
+
679
+ # it's staked, but we don't know which staking program
680
+ # so the staking_program_id should be an arbitrary staking contract
681
+ return service_owner
682
+
683
+
684
+ # TODO Backport this to Open Autonomy MintHelper class
685
+ # MintHelper should support passing custom 'description', 'name' and 'attributes'.
686
+ # If some of these fields are not defined, then it can take the current default values.
687
+ # (Version is included as an attribute.)
688
+ # The current code here is a workaround and just addresses the description,
689
+ # because modifying the name and attributes requires touching lower-level code.
690
+ # A proper refactor of this should be done in Open Autonomy.
691
+ class MintManager(MintHelper):
692
+ """MintManager"""
693
+
694
+ metadata_description: t.Optional[str] = None
695
+ metadata_name: t.Optional[str] = None
696
+ metadata_attributes: t.Optional[t.Dict[str, str]] = None
697
+
698
+ def set_metadata_fields(
699
+ self,
700
+ name: t.Optional[str] = None,
701
+ description: t.Optional[str] = None,
702
+ attributes: t.Optional[t.Dict[str, str]] = None,
703
+ ) -> "MintManager":
704
+ """Set metadata fields."""
705
+ self.metadata_name = (
706
+ name # Not used currently, just an indication for the OA refactor
707
+ )
708
+ self.metadata_description = description
709
+ self.metadata_attributes = (
710
+ attributes # Not used currently, just an indication for the OA refactor
711
+ )
712
+ return self
713
+
714
+ def publish_metadata(self) -> "MintManager":
715
+ """Publish metadata."""
716
+ self.metadata_hash, self.metadata_string = publish_metadata(
717
+ package_id=self.package_configuration.package_id,
718
+ package_path=self.package_path,
719
+ nft=cast(str, self.nft),
720
+ description=self.metadata_description
721
+ or self.package_configuration.description,
722
+ )
723
+ return self
724
+
725
+
726
+ # End Backport
727
+
439
728
 
440
729
  class _ChainUtil:
441
730
  """On chain service management."""
@@ -452,16 +741,25 @@ class _ChainUtil:
452
741
  self.wallet = wallet
453
742
  self.contracts = contracts
454
743
  self.chain_type = chain_type or ChainType.CUSTOM
744
+ os.environ[f"{self.chain_type.name}_CHAIN_RPC"] = self.rpc
455
745
 
456
746
  def _patch(self) -> None:
457
747
  """Patch contract and chain config."""
458
748
  ChainConfigs.get(self.chain_type).rpc = self.rpc
459
- if self.chain_type != ChainType.CUSTOM:
460
- return
461
-
462
749
  for name, address in self.contracts.items():
463
750
  ContractConfigs.get(name=name).contracts[self.chain_type] = address
464
751
 
752
+ @property
753
+ def safe(self) -> str:
754
+ """Get safe address."""
755
+ chain_id = self.ledger_api.api.eth.chain_id
756
+ chain = OperateChain.from_id(chain_id)
757
+ if self.wallet.safes is None:
758
+ raise ValueError("Safes not initialized")
759
+ if chain not in self.wallet.safes:
760
+ raise ValueError(f"Safe for chain type {chain} not found")
761
+ return self.wallet.safes[chain]
762
+
465
763
  @property
466
764
  def crypto(self) -> Crypto:
467
765
  """Load crypto object."""
@@ -477,12 +775,33 @@ class _ChainUtil:
477
775
  def ledger_api(self) -> LedgerApi:
478
776
  """Load ledger api object."""
479
777
  self._patch()
480
- ledger_api, _ = OnChainHelper.get_ledger_and_crypto_objects(
481
- chain_type=self.chain_type,
482
- key=self.wallet.key_path,
483
- password=self.wallet.password,
778
+ return self.wallet.ledger_api(
779
+ chain=OperateChain.from_string(self.chain_type.value),
780
+ rpc=self.rpc,
781
+ )
782
+
783
+ @cached_property
784
+ def service_manager_address(self) -> str: # TODO: backport to OA
785
+ """Get service manager contract address."""
786
+ service_registry = registry_contracts.service_registry.get_instance(
787
+ ledger_api=self.ledger_api,
788
+ contract_address=CONTRACTS[OperateChain(self.chain_type.value)][
789
+ "service_registry"
790
+ ],
791
+ )
792
+ return service_registry.functions.manager().call()
793
+
794
+ @property
795
+ def service_manager_instance(self) -> Contract:
796
+ """Load service manager contract instance."""
797
+ contract_interface = registry_contracts.service_manager.contract_interface.get(
798
+ self.ledger_api.identifier, {}
484
799
  )
485
- return ledger_api
800
+ instance = self.ledger_api.get_contract_instance(
801
+ contract_interface,
802
+ self.service_manager_address,
803
+ )
804
+ return instance
486
805
 
487
806
  def info(self, token_id: int) -> t.Dict:
488
807
  """Get service info."""
@@ -498,7 +817,7 @@ class _ChainUtil:
498
817
  max_agents,
499
818
  number_of_agent_instances,
500
819
  service_state,
501
- cannonical_agents,
820
+ canonical_agents,
502
821
  ) = get_service_info(
503
822
  ledger_api=ledger_api,
504
823
  chain_type=self.chain_type,
@@ -517,151 +836,62 @@ class _ChainUtil:
517
836
  max_agents=max_agents,
518
837
  number_of_agent_instances=number_of_agent_instances,
519
838
  service_state=service_state,
520
- cannonical_agents=cannonical_agents,
839
+ canonical_agents=canonical_agents,
521
840
  instances=instances,
522
841
  )
523
842
 
843
+ def get_agent_bond(self, service_id: int, agent_id: int) -> int:
844
+ """Get the agent bond for a given service"""
845
+ self._patch()
524
846
 
525
- class OnChainManager(_ChainUtil):
526
- """On chain service management."""
847
+ if service_id <= 0 or agent_id <= 0:
848
+ return 0
527
849
 
528
- def mint( # pylint: disable=too-many-arguments,too-many-locals
529
- self,
530
- package_path: Path,
531
- agent_id: int,
532
- number_of_slots: int,
533
- cost_of_bond: int,
534
- threshold: int,
535
- nft: Optional[Union[Path, IPFSHash]],
536
- update_token: t.Optional[int] = None,
537
- token: t.Optional[str] = None,
538
- ) -> t.Dict:
539
- """Mint service."""
540
- # TODO: Support for update
541
- self._patch()
542
- manager = MintManager(
850
+ ledger_api, _ = OnChainHelper.get_ledger_and_crypto_objects(
851
+ chain_type=self.chain_type
852
+ )
853
+ bond = get_token_deposit_amount(
854
+ ledger_api=ledger_api,
543
855
  chain_type=self.chain_type,
544
- key=self.wallet.key_path,
545
- password=self.wallet.password,
546
- update_token=update_token,
547
- timeout=ON_CHAIN_INTERACT_TIMEOUT,
548
- retries=ON_CHAIN_INTERACT_RETRIES,
549
- sleep=ON_CHAIN_INTERACT_SLEEP,
856
+ service_id=service_id,
857
+ agent_id=agent_id,
550
858
  )
859
+ return bond
551
860
 
552
- # Prepare for minting
861
+ def get_service_safe_owners(self, service_id: int) -> t.List[str]:
862
+ """Get list of owners."""
863
+ ledger_api, _ = OnChainHelper.get_ledger_and_crypto_objects(
864
+ chain_type=self.chain_type
865
+ )
553
866
  (
554
- manager.load_package_configuration(
555
- package_path=package_path, package_type=PackageType.SERVICE
556
- )
557
- .load_metadata()
558
- .verify_nft(nft=nft)
559
- .verify_service_dependencies(agent_id=agent_id)
560
- .publish_metadata()
867
+ _,
868
+ multisig_address,
869
+ _,
870
+ _,
871
+ _,
872
+ _,
873
+ _,
874
+ _,
875
+ ) = get_service_info(
876
+ ledger_api=ledger_api,
877
+ chain_type=self.chain_type,
878
+ token_id=service_id,
561
879
  )
562
880
 
563
- with tempfile.TemporaryDirectory() as temp, contextlib.redirect_stdout(
564
- io.StringIO()
565
- ):
566
- with cd(temp):
567
- kwargs = dict(
568
- number_of_slots=number_of_slots,
569
- cost_of_bond=cost_of_bond,
570
- threshold=threshold,
571
- token=token,
572
- )
573
- # TODO: Enable after consulting smart contracts team re a safe
574
- # being a service owner
575
- # if update_token is None:
576
- # kwargs["owner"] = self.wallet.safe # noqa: F401
577
- method = (
578
- manager.mint_service
579
- if update_token is None
580
- else manager.update_service
581
- )
582
- method(**kwargs)
583
- (metadata,) = Path(temp).glob("*.json")
584
- published = {
585
- "token": int(Path(metadata).name.replace(".json", "")),
586
- "metadata": json.loads(Path(metadata).read_text(encoding="utf-8")),
587
- }
588
- return published
881
+ if multisig_address == ZERO_ADDRESS:
882
+ return []
589
883
 
590
- def activate(
591
- self,
592
- service_id: int,
593
- token: t.Optional[str] = None,
594
- ) -> None:
595
- """Activate service."""
596
- logging.info(f"Activating service {service_id}...")
597
- self._patch()
598
- with contextlib.redirect_stdout(io.StringIO()):
599
- ServiceManager(
600
- service_id=service_id,
601
- chain_type=self.chain_type,
602
- key=self.wallet.key_path,
603
- password=self.wallet.password,
604
- timeout=ON_CHAIN_INTERACT_TIMEOUT,
605
- retries=ON_CHAIN_INTERACT_RETRIES,
606
- sleep=ON_CHAIN_INTERACT_SLEEP,
607
- ).check_is_service_token_secured(
608
- token=token,
609
- ).activate_service()
884
+ return registry_contracts.gnosis_safe.get_owners(
885
+ ledger_api=ledger_api,
886
+ contract_address=multisig_address,
887
+ ).get("owners", [])
610
888
 
611
- def register(
889
+ def swap( # pylint: disable=too-many-arguments,too-many-locals
612
890
  self,
613
891
  service_id: int,
614
- instances: t.List[str],
615
- agents: t.List[int],
616
- token: t.Optional[str] = None,
617
- ) -> None:
618
- """Register instance."""
619
- logging.info(f"Registering service {service_id}...")
620
- with contextlib.redirect_stdout(io.StringIO()):
621
- ServiceManager(
622
- service_id=service_id,
623
- chain_type=self.chain_type,
624
- key=self.wallet.key_path,
625
- password=self.wallet.password,
626
- timeout=ON_CHAIN_INTERACT_TIMEOUT,
627
- retries=ON_CHAIN_INTERACT_RETRIES,
628
- sleep=ON_CHAIN_INTERACT_SLEEP,
629
- ).check_is_service_token_secured(
630
- token=token,
631
- ).register_instance(
632
- instances=instances,
633
- agent_ids=agents,
634
- )
635
-
636
- def deploy(
637
- self,
638
- service_id: int,
639
- reuse_multisig: bool = False,
640
- token: t.Optional[str] = None,
641
- ) -> None:
642
- """Deploy service."""
643
- logging.info(f"Deploying service {service_id}...")
644
- self._patch()
645
- with contextlib.redirect_stdout(io.StringIO()):
646
- ServiceManager(
647
- service_id=service_id,
648
- chain_type=self.chain_type,
649
- key=self.wallet.key_path,
650
- password=self.wallet.password,
651
- timeout=ON_CHAIN_INTERACT_TIMEOUT,
652
- retries=ON_CHAIN_INTERACT_RETRIES,
653
- sleep=ON_CHAIN_INTERACT_SLEEP,
654
- ).check_is_service_token_secured(
655
- token=token,
656
- ).deploy_service(
657
- reuse_multisig=reuse_multisig,
658
- )
659
-
660
- def swap( # pylint: disable=too-many-arguments,too-many-locals
661
- self,
662
- service_id: int,
663
- multisig: str,
664
- owner_key: str,
892
+ multisig: str,
893
+ owner_cryptos: t.List[Crypto],
894
+ new_owner_address: str,
665
895
  ) -> None:
666
896
  """Swap safe owner."""
667
897
  logging.info(f"Swapping safe for service {service_id} [{multisig}]...")
@@ -675,11 +905,6 @@ class OnChainManager(_ChainUtil):
675
905
  retries=ON_CHAIN_INTERACT_RETRIES,
676
906
  sleep=ON_CHAIN_INTERACT_SLEEP,
677
907
  )
678
- with tempfile.TemporaryDirectory() as temp_dir:
679
- key_file = Path(temp_dir, "key.txt")
680
- key_file.write_text(owner_key, encoding="utf-8")
681
- owner_crypto = EthereumCrypto(private_key_path=str(key_file))
682
- owner_cryptos: t.List[EthereumCrypto] = [owner_crypto]
683
908
  owners = [
684
909
  manager.ledger_api.api.to_checksum_address(owner_crypto.address)
685
910
  for owner_crypto in owner_cryptos
@@ -690,9 +915,7 @@ class OnChainManager(_ChainUtil):
690
915
  ledger_api=manager.ledger_api,
691
916
  contract_address=multisig,
692
917
  old_owner=manager.ledger_api.api.to_checksum_address(owner_to_swap),
693
- new_owner=manager.ledger_api.api.to_checksum_address(
694
- manager.crypto.address
695
- ),
918
+ new_owner=manager.ledger_api.api.to_checksum_address(new_owner_address),
696
919
  ).get("data")
697
920
  multisend_txs.append(
698
921
  {
@@ -738,7 +961,7 @@ class OnChainManager(_ChainUtil):
738
961
  tx = registry_contracts.gnosis_safe.get_raw_safe_transaction(
739
962
  ledger_api=manager.ledger_api,
740
963
  contract_address=multisig,
741
- sender_address=owner_crypto.address,
964
+ sender_address=owner_cryptos[0].address,
742
965
  owners=tuple(owners), # type: ignore
743
966
  to_address=tx_params["to_address"],
744
967
  value=tx_params["ether_value"],
@@ -747,12 +970,209 @@ class OnChainManager(_ChainUtil):
747
970
  signatures_by_owner=owner_to_signature,
748
971
  operation=SafeOperation.DELEGATE_CALL.value,
749
972
  )
750
- stx = owner_crypto.sign_transaction(tx)
973
+ stx = owner_cryptos[0].sign_transaction(tx)
751
974
  tx_digest = manager.ledger_api.send_signed_transaction(stx)
752
975
  receipt = manager.ledger_api.api.eth.wait_for_transaction_receipt(tx_digest)
753
976
  if receipt["status"] != 1:
754
977
  raise RuntimeError("Error swapping owners")
755
978
 
979
+ def staking_slots_available(self, staking_contract: str) -> bool:
980
+ """Check if there are available slots on the staking contract"""
981
+ self._patch()
982
+ return StakingManager(
983
+ chain=OperateChain(self.chain_type.value),
984
+ ).slots_available(
985
+ staking_contract=staking_contract,
986
+ )
987
+
988
+ def staking_rewards_available(self, staking_contract: str) -> bool:
989
+ """Check if there are available staking rewards on the staking contract"""
990
+ self._patch()
991
+ available_rewards = StakingManager(
992
+ chain=OperateChain(self.chain_type.value),
993
+ ).available_rewards(
994
+ staking_contract=staking_contract,
995
+ )
996
+ return available_rewards > 0
997
+
998
+ def staking_rewards_claimable(self, staking_contract: str, service_id: int) -> bool:
999
+ """Check if there are claimable staking rewards on the staking contract"""
1000
+ self._patch()
1001
+ claimable_rewards = StakingManager(
1002
+ chain=OperateChain(self.chain_type.value),
1003
+ ).claimable_rewards(
1004
+ staking_contract=staking_contract,
1005
+ service_id=service_id,
1006
+ )
1007
+ return claimable_rewards > 0
1008
+
1009
+ def staking_status(self, service_id: int, staking_contract: str) -> StakingState:
1010
+ """Stake the service"""
1011
+ self._patch()
1012
+ return StakingManager(
1013
+ chain=OperateChain(self.chain_type.value),
1014
+ ).staking_state(
1015
+ service_id=service_id,
1016
+ staking_contract=staking_contract,
1017
+ )
1018
+
1019
+ def get_staking_params(
1020
+ self, staking_contract: str, fallback_params: t.Optional[t.Dict] = None
1021
+ ) -> t.Dict:
1022
+ """Get agent IDs for the staking contract"""
1023
+ if staking_contract is None and fallback_params is not None:
1024
+ return fallback_params
1025
+ self._patch()
1026
+ staking_manager = StakingManager(
1027
+ chain=OperateChain(self.chain_type.value),
1028
+ )
1029
+ return staking_manager.get_staking_params(
1030
+ staking_contract=staking_contract,
1031
+ )
1032
+
1033
+
1034
+ class OnChainManager(_ChainUtil):
1035
+ """On chain service management."""
1036
+
1037
+ def mint( # pylint: disable=too-many-arguments,too-many-locals
1038
+ self,
1039
+ package_path: Path,
1040
+ agent_id: int,
1041
+ number_of_slots: int,
1042
+ cost_of_bond: int,
1043
+ threshold: int,
1044
+ nft: Optional[Union[Path, IPFSHash]],
1045
+ update_token: t.Optional[int] = None,
1046
+ token: t.Optional[str] = None,
1047
+ metadata_description: t.Optional[str] = None,
1048
+ skip_dependency_check: t.Optional[bool] = False,
1049
+ ) -> t.Dict:
1050
+ """Mint service."""
1051
+ # TODO: Support for update
1052
+ self._patch()
1053
+ manager = MintManager(
1054
+ chain_type=self.chain_type,
1055
+ key=self.wallet.key_path,
1056
+ password=self.wallet.password,
1057
+ update_token=update_token,
1058
+ timeout=ON_CHAIN_INTERACT_TIMEOUT,
1059
+ retries=ON_CHAIN_INTERACT_RETRIES,
1060
+ sleep=ON_CHAIN_INTERACT_SLEEP,
1061
+ )
1062
+
1063
+ # Prepare for minting
1064
+ (
1065
+ manager.load_package_configuration(
1066
+ package_path=package_path, package_type=PackageType.SERVICE
1067
+ )
1068
+ .load_metadata()
1069
+ .set_metadata_fields(description=metadata_description)
1070
+ .verify_nft(nft=nft)
1071
+ )
1072
+
1073
+ if skip_dependency_check is False:
1074
+ logging.warning("Skipping depencencies check")
1075
+ manager.verify_service_dependencies(agent_id=agent_id)
1076
+
1077
+ manager.publish_metadata()
1078
+
1079
+ with tempfile.TemporaryDirectory() as temp, contextlib.redirect_stdout(
1080
+ io.StringIO()
1081
+ ):
1082
+ with cd(temp):
1083
+ kwargs = dict(
1084
+ number_of_slots=number_of_slots,
1085
+ cost_of_bond=cost_of_bond,
1086
+ threshold=threshold,
1087
+ token=token,
1088
+ )
1089
+ # TODO: Enable after consulting smart contracts team re a safe
1090
+ # being a service owner
1091
+ # if update_token is None:
1092
+ # kwargs["owner"] = self.wallet.safe # noqa: F401
1093
+ method = (
1094
+ manager.mint_service
1095
+ if update_token is None
1096
+ else manager.update_service
1097
+ )
1098
+ method(**kwargs)
1099
+ (metadata,) = Path(temp).glob("*.json")
1100
+ published = {
1101
+ "token": int(Path(metadata).name.replace(".json", "")),
1102
+ "metadata": json.loads(Path(metadata).read_text(encoding="utf-8")),
1103
+ }
1104
+ return published
1105
+
1106
+ def activate(
1107
+ self,
1108
+ service_id: int,
1109
+ token: t.Optional[str] = None,
1110
+ ) -> None:
1111
+ """Activate service."""
1112
+ logging.info(f"Activating service {service_id}...")
1113
+ self._patch()
1114
+ with contextlib.redirect_stdout(io.StringIO()):
1115
+ ServiceManager(
1116
+ service_id=service_id,
1117
+ chain_type=self.chain_type,
1118
+ key=self.wallet.key_path,
1119
+ password=self.wallet.password,
1120
+ timeout=ON_CHAIN_INTERACT_TIMEOUT,
1121
+ retries=ON_CHAIN_INTERACT_RETRIES,
1122
+ sleep=ON_CHAIN_INTERACT_SLEEP,
1123
+ ).check_is_service_token_secured(
1124
+ token=token,
1125
+ ).activate_service()
1126
+
1127
+ def register(
1128
+ self,
1129
+ service_id: int,
1130
+ instances: t.List[str],
1131
+ agents: t.List[int],
1132
+ token: t.Optional[str] = None,
1133
+ ) -> None:
1134
+ """Register instance."""
1135
+ logging.info(f"Registering service {service_id}...")
1136
+ with contextlib.redirect_stdout(io.StringIO()):
1137
+ ServiceManager(
1138
+ service_id=service_id,
1139
+ chain_type=self.chain_type,
1140
+ key=self.wallet.key_path,
1141
+ password=self.wallet.password,
1142
+ timeout=ON_CHAIN_INTERACT_TIMEOUT,
1143
+ retries=ON_CHAIN_INTERACT_RETRIES,
1144
+ sleep=ON_CHAIN_INTERACT_SLEEP,
1145
+ ).check_is_service_token_secured(
1146
+ token=token,
1147
+ ).register_instance(
1148
+ instances=instances,
1149
+ agent_ids=agents,
1150
+ )
1151
+
1152
+ def deploy(
1153
+ self,
1154
+ service_id: int,
1155
+ reuse_multisig: bool = False,
1156
+ token: t.Optional[str] = None,
1157
+ ) -> None:
1158
+ """Deploy service."""
1159
+ logging.info(f"Deploying service {service_id}...")
1160
+ self._patch()
1161
+ with contextlib.redirect_stdout(io.StringIO()):
1162
+ ServiceManager(
1163
+ service_id=service_id,
1164
+ chain_type=self.chain_type,
1165
+ key=self.wallet.key_path,
1166
+ password=self.wallet.password,
1167
+ timeout=ON_CHAIN_INTERACT_TIMEOUT,
1168
+ retries=ON_CHAIN_INTERACT_RETRIES,
1169
+ sleep=ON_CHAIN_INTERACT_SLEEP,
1170
+ ).check_is_service_token_secured(
1171
+ token=token,
1172
+ ).deploy_service(
1173
+ reuse_multisig=reuse_multisig,
1174
+ )
1175
+
756
1176
  def terminate(self, service_id: int, token: t.Optional[str] = None) -> None:
757
1177
  """Terminate service."""
758
1178
  logging.info(f"Terminating service {service_id}...")
@@ -787,29 +1207,6 @@ class OnChainManager(_ChainUtil):
787
1207
  token=token,
788
1208
  ).unbond_service()
789
1209
 
790
- def staking_slots_available(self, staking_contract: str) -> bool:
791
- """Check if there are available slots on the staking contract"""
792
- self._patch()
793
- return StakingManager(
794
- key=self.wallet.key_path,
795
- password=self.wallet.password,
796
- chain_type=self.chain_type,
797
- ).slots_available(
798
- staking_contract=staking_contract,
799
- )
800
-
801
- def staking_rewards_available(self, staking_contract: str) -> bool:
802
- """Check if there are available staking rewards on the staking contract"""
803
- self._patch()
804
- available_rewards = StakingManager(
805
- key=self.wallet.key_path,
806
- password=self.wallet.password,
807
- chain_type=self.chain_type,
808
- ).available_rewards(
809
- staking_contract=staking_contract,
810
- )
811
- return available_rewards > 0
812
-
813
1210
  def stake(
814
1211
  self,
815
1212
  service_id: int,
@@ -819,35 +1216,33 @@ class OnChainManager(_ChainUtil):
819
1216
  """Stake service."""
820
1217
  self._patch()
821
1218
  StakingManager(
822
- key=self.wallet.key_path,
823
- password=self.wallet.password,
824
- chain_type=self.chain_type,
1219
+ chain=OperateChain(self.chain_type.value),
825
1220
  ).stake(
826
1221
  service_id=service_id,
827
1222
  service_registry=service_registry,
828
1223
  staking_contract=staking_contract,
1224
+ key=self.wallet.key_path,
1225
+ password=self.wallet.password,
829
1226
  )
830
1227
 
831
1228
  def unstake(self, service_id: int, staking_contract: str) -> None:
832
1229
  """Unstake service."""
833
1230
  self._patch()
834
1231
  StakingManager(
835
- key=self.wallet.key_path,
836
- password=self.wallet.password,
837
- chain_type=self.chain_type,
1232
+ chain=OperateChain(self.chain_type.value),
838
1233
  ).unstake(
839
1234
  service_id=service_id,
840
1235
  staking_contract=staking_contract,
1236
+ key=self.wallet.key_path,
1237
+ password=self.wallet.password,
841
1238
  )
842
1239
 
843
1240
  def staking_status(self, service_id: int, staking_contract: str) -> StakingState:
844
1241
  """Stake the service"""
845
1242
  self._patch()
846
1243
  return StakingManager(
847
- key=self.wallet.key_path,
848
- password=self.wallet.password,
849
- chain_type=self.chain_type,
850
- ).status(
1244
+ chain=OperateChain(self.chain_type.value),
1245
+ ).staking_state(
851
1246
  service_id=service_id,
852
1247
  staking_contract=staking_contract,
853
1248
  )
@@ -856,13 +1251,28 @@ class OnChainManager(_ChainUtil):
856
1251
  class EthSafeTxBuilder(_ChainUtil):
857
1252
  """Safe Transaction builder."""
858
1253
 
859
- def new_tx(self) -> GnosisSafeTransaction:
1254
+ @classmethod
1255
+ def _new_tx(
1256
+ cls, ledger_api: LedgerApi, crypto: Crypto, chain_type: ChainType, safe: str
1257
+ ) -> GnosisSafeTransaction:
860
1258
  """Create a new GnosisSafeTransaction instance."""
861
1259
  return GnosisSafeTransaction(
862
- ledger_api=self.ledger_api,
1260
+ ledger_api=ledger_api,
1261
+ crypto=crypto,
1262
+ chain_type=chain_type,
1263
+ safe=safe,
1264
+ )
1265
+
1266
+ def new_tx(self) -> GnosisSafeTransaction:
1267
+ """Create a new GnosisSafeTransaction instance."""
1268
+ return EthSafeTxBuilder._new_tx(
1269
+ ledger_api=self.wallet.ledger_api(
1270
+ chain=OperateChain.from_string(self.chain_type.value),
1271
+ rpc=self.rpc,
1272
+ ),
863
1273
  crypto=self.crypto,
864
1274
  chain_type=self.chain_type,
865
- safe=t.cast(str, self.wallet.safe),
1275
+ safe=t.cast(str, self.safe),
866
1276
  )
867
1277
 
868
1278
  def get_mint_tx_data( # pylint: disable=too-many-arguments
@@ -875,6 +1285,8 @@ class EthSafeTxBuilder(_ChainUtil):
875
1285
  nft: Optional[Union[Path, IPFSHash]],
876
1286
  update_token: t.Optional[int] = None,
877
1287
  token: t.Optional[str] = None,
1288
+ metadata_description: t.Optional[str] = None,
1289
+ skip_depencency_check: t.Optional[bool] = False,
878
1290
  ) -> t.Dict:
879
1291
  """Build mint transaction."""
880
1292
  # TODO: Support for update
@@ -889,56 +1301,73 @@ class EthSafeTxBuilder(_ChainUtil):
889
1301
  sleep=ON_CHAIN_INTERACT_SLEEP,
890
1302
  )
891
1303
  # Prepare for minting
1304
+
892
1305
  (
893
1306
  manager.load_package_configuration(
894
1307
  package_path=package_path, package_type=PackageType.SERVICE
895
1308
  )
896
1309
  .load_metadata()
1310
+ .set_metadata_fields(description=metadata_description)
897
1311
  .verify_nft(nft=nft)
898
- .verify_service_dependencies(agent_id=agent_id)
899
- .publish_metadata()
900
- )
901
- instance = registry_contracts.service_manager.get_instance(
902
- ledger_api=self.ledger_api,
903
- contract_address=self.contracts["service_manager"],
904
1312
  )
905
1313
 
906
- txd = instance.encodeABI(
907
- fn_name="create" if update_token is None else "update",
908
- args=[
909
- self.wallet.safe,
910
- token or ETHEREUM_ERC20,
911
- manager.metadata_hash,
912
- [agent_id],
913
- [[number_of_slots, cost_of_bond]],
914
- threshold,
915
- ],
916
- )
1314
+ if skip_depencency_check is False:
1315
+ logging.warning("Skipping depencencies check")
1316
+ manager.verify_service_dependencies(agent_id=agent_id)
1317
+
1318
+ manager.publish_metadata()
1319
+
1320
+ instance = self.service_manager_instance
1321
+ if update_token is None:
1322
+ safe = self.safe
1323
+ txd = instance.encodeABI(
1324
+ fn_name="create",
1325
+ args=[
1326
+ safe,
1327
+ token or ETHEREUM_ERC20,
1328
+ manager.metadata_hash,
1329
+ [agent_id],
1330
+ [[number_of_slots, cost_of_bond]],
1331
+ threshold,
1332
+ ],
1333
+ )
1334
+ else:
1335
+ txd = instance.encodeABI(
1336
+ fn_name="update",
1337
+ args=[
1338
+ token or ETHEREUM_ERC20,
1339
+ manager.metadata_hash,
1340
+ [agent_id],
1341
+ [[number_of_slots, cost_of_bond]],
1342
+ threshold,
1343
+ update_token,
1344
+ ],
1345
+ )
917
1346
 
918
1347
  return {
919
- "to": self.contracts["service_manager"],
1348
+ "to": self.service_manager_address,
920
1349
  "data": txd[2:],
921
1350
  "operation": MultiSendOperation.CALL,
922
1351
  "value": 0,
923
1352
  }
924
1353
 
925
- def get_olas_approval_data(
1354
+ def get_erc20_approval_data(
926
1355
  self,
927
1356
  spender: str,
928
1357
  amount: int,
929
- olas_contract: str,
1358
+ erc20_contract: str,
930
1359
  ) -> t.Dict:
931
1360
  """Get activate tx data."""
932
1361
  instance = registry_contracts.erc20.get_instance(
933
1362
  ledger_api=self.ledger_api,
934
- contract_address=olas_contract,
1363
+ contract_address=erc20_contract,
935
1364
  )
936
1365
  txd = instance.encodeABI(
937
1366
  fn_name="approve",
938
1367
  args=[spender, amount],
939
1368
  )
940
1369
  return {
941
- "to": olas_contract,
1370
+ "to": erc20_contract,
942
1371
  "data": txd[2:],
943
1372
  "operation": MultiSendOperation.CALL,
944
1373
  "value": 0,
@@ -948,15 +1377,15 @@ class EthSafeTxBuilder(_ChainUtil):
948
1377
  """Get activate tx data."""
949
1378
  instance = registry_contracts.service_manager.get_instance(
950
1379
  ledger_api=self.ledger_api,
951
- contract_address=self.contracts["service_manager"],
1380
+ contract_address=self.service_manager_address,
952
1381
  )
953
1382
  txd = instance.encodeABI(
954
1383
  fn_name="activateRegistration",
955
1384
  args=[service_id],
956
1385
  )
957
1386
  return {
958
- "from": self.wallet.safe,
959
- "to": self.contracts["service_manager"],
1387
+ "from": self.safe,
1388
+ "to": self.service_manager_address,
960
1389
  "data": txd[2:],
961
1390
  "operation": MultiSendOperation.CALL,
962
1391
  "value": cost_of_bond,
@@ -972,7 +1401,7 @@ class EthSafeTxBuilder(_ChainUtil):
972
1401
  """Get register instances tx data."""
973
1402
  instance = registry_contracts.service_manager.get_instance(
974
1403
  ledger_api=self.ledger_api,
975
- contract_address=self.contracts["service_manager"],
1404
+ contract_address=self.service_manager_address,
976
1405
  )
977
1406
  txd = instance.encodeABI(
978
1407
  fn_name="registerAgents",
@@ -983,43 +1412,73 @@ class EthSafeTxBuilder(_ChainUtil):
983
1412
  ],
984
1413
  )
985
1414
  return {
986
- "from": self.wallet.safe,
987
- "to": self.contracts["service_manager"],
1415
+ "from": self.safe,
1416
+ "to": self.service_manager_address,
988
1417
  "data": txd[2:],
989
1418
  "operation": MultiSendOperation.CALL,
990
1419
  "value": cost_of_bond,
991
1420
  }
992
1421
 
993
- def get_deploy_data(
1422
+ def get_deploy_data_from_safe(
994
1423
  self,
995
1424
  service_id: int,
1425
+ master_safe: str,
996
1426
  reuse_multisig: bool = False,
997
- ) -> t.Dict:
998
- """Get deploy tx data."""
999
- instance = registry_contracts.service_manager.get_instance(
1427
+ use_recovery_module: bool = True,
1428
+ ) -> t.List[t.Dict[str, t.Any]]:
1429
+ """Get the deploy data instructions for a safe"""
1430
+ registry_instance = registry_contracts.service_manager.get_instance(
1000
1431
  ledger_api=self.ledger_api,
1001
- contract_address=self.contracts["service_manager"],
1432
+ contract_address=self.service_manager_address,
1002
1433
  )
1434
+ approve_hash_message = None
1003
1435
  if reuse_multisig:
1004
- _deployment_payload, error = get_reuse_multisig_payload(
1005
- ledger_api=self.ledger_api,
1006
- crypto=self.crypto,
1007
- chain_type=self.chain_type,
1008
- service_id=service_id,
1009
- )
1010
- if _deployment_payload is None:
1011
- raise ValueError(error)
1012
- deployment_payload = _deployment_payload
1013
- gnosis_safe_multisig = ContractConfigs.get(
1014
- GNOSIS_SAFE_SAME_ADDRESS_MULTISIG_CONTRACT.name
1015
- ).contracts[self.chain_type]
1016
- else:
1017
- deployment_payload = get_delployment_payload()
1018
- gnosis_safe_multisig = ContractConfigs.get(
1019
- GNOSIS_SAFE_PROXY_FACTORY_CONTRACT.name
1020
- ).contracts[self.chain_type]
1021
-
1022
- txd = instance.encodeABI(
1436
+ if not use_recovery_module:
1437
+ (
1438
+ _deployment_payload,
1439
+ approve_hash_message,
1440
+ error,
1441
+ ) = get_reuse_multisig_from_safe_payload(
1442
+ ledger_api=self.ledger_api,
1443
+ chain_type=self.chain_type,
1444
+ service_id=service_id,
1445
+ master_safe=master_safe,
1446
+ )
1447
+ if _deployment_payload is None:
1448
+ raise ValueError(error)
1449
+ deployment_payload = _deployment_payload
1450
+ gnosis_safe_multisig = ContractConfigs.get(
1451
+ GNOSIS_SAFE_SAME_ADDRESS_MULTISIG_CONTRACT.name
1452
+ ).contracts[self.chain_type]
1453
+ else:
1454
+ (
1455
+ _deployment_payload,
1456
+ error,
1457
+ ) = get_reuse_multisig_with_recovery_from_safe_payload(
1458
+ ledger_api=self.ledger_api,
1459
+ chain_type=self.chain_type,
1460
+ service_id=service_id,
1461
+ master_safe=master_safe,
1462
+ )
1463
+ if _deployment_payload is None:
1464
+ raise ValueError(error)
1465
+ deployment_payload = _deployment_payload
1466
+ gnosis_safe_multisig = ContractConfigs.get(
1467
+ RECOVERY_MODULE_CONTRACT.name
1468
+ ).contracts[self.chain_type]
1469
+ else: # Deploy a new multisig
1470
+ if not use_recovery_module:
1471
+ deployment_payload = get_deployment_payload()
1472
+ gnosis_safe_multisig = ContractConfigs.get(
1473
+ GNOSIS_SAFE_PROXY_FACTORY_CONTRACT.name
1474
+ ).contracts[self.chain_type]
1475
+ else:
1476
+ deployment_payload = get_deployment_with_recovery_payload()
1477
+ gnosis_safe_multisig = ContractConfigs.get(
1478
+ SAFE_MULTISIG_WITH_RECOVERY_MODULE_CONTRACT.name
1479
+ ).contracts[self.chain_type]
1480
+
1481
+ deploy_data = registry_instance.encodeABI(
1023
1482
  fn_name="deploy",
1024
1483
  args=[
1025
1484
  service_id,
@@ -1027,25 +1486,204 @@ class EthSafeTxBuilder(_ChainUtil):
1027
1486
  deployment_payload,
1028
1487
  ],
1029
1488
  )
1030
- return {
1031
- "to": self.contracts["service_manager"],
1032
- "data": txd[2:],
1489
+ deploy_message = {
1490
+ "to": self.service_manager_address,
1491
+ "data": deploy_data[2:],
1492
+ "operation": MultiSendOperation.CALL,
1493
+ "value": 0,
1494
+ }
1495
+ if approve_hash_message is None:
1496
+ return [deploy_message]
1497
+ return [approve_hash_message, deploy_message]
1498
+
1499
+ def get_safe_b_native_transfer_messages( # pylint: disable=too-many-locals
1500
+ self,
1501
+ safe_b_address: str,
1502
+ to: str,
1503
+ amount: int,
1504
+ ) -> t.Tuple[t.Dict, t.Dict]:
1505
+ """
1506
+ Build the two messages (Safe calls) to withdraw native ETH from Safe B via this Safe (owner of Safe B).
1507
+
1508
+ Builds the messages to be settled by this Safe:
1509
+ 1) approveHash(inner_tx_hash)
1510
+ 2) execTransaction(...) to transfer ETH
1511
+ """
1512
+ safe_b_instance = registry_contracts.gnosis_safe.get_instance(
1513
+ ledger_api=self.ledger_api,
1514
+ contract_address=safe_b_address,
1515
+ )
1516
+
1517
+ txs = []
1518
+ txs.append(
1519
+ {
1520
+ "to": to,
1521
+ "data": b"",
1522
+ "operation": MultiSendOperation.CALL,
1523
+ "value": amount,
1524
+ }
1525
+ )
1526
+
1527
+ multisend_address = ContractConfigs.get(MULTISEND_CONTRACT.name).contracts[
1528
+ self.chain_type
1529
+ ]
1530
+ multisend_tx = registry_contracts.multisend.get_multisend_tx(
1531
+ ledger_api=self.ledger_api,
1532
+ contract_address=multisend_address,
1533
+ txs=txs,
1534
+ )
1535
+
1536
+ # Compute inner Safe transaction hash
1537
+ safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
1538
+ ledger_api=self.ledger_api,
1539
+ contract_address=safe_b_address,
1540
+ to_address=multisend_address,
1541
+ value=multisend_tx["value"],
1542
+ data=multisend_tx["data"],
1543
+ operation=SafeOperation.CALL.value,
1544
+ ).get("tx_hash")
1545
+
1546
+ # Build approveHash message
1547
+ approve_hash_data = safe_b_instance.encodeABI(
1548
+ fn_name="approveHash",
1549
+ args=[safe_tx_hash],
1550
+ )
1551
+ approve_hash_message = {
1552
+ "to": safe_b_address,
1553
+ "data": approve_hash_data[2:],
1033
1554
  "operation": MultiSendOperation.CALL,
1034
1555
  "value": 0,
1035
1556
  }
1036
1557
 
1558
+ # Build execTransaction message
1559
+ exec_data = safe_b_instance.encodeABI(
1560
+ fn_name="execTransaction",
1561
+ args=[
1562
+ multisend_address,
1563
+ multisend_tx["value"],
1564
+ multisend_tx["data"],
1565
+ SafeOperation.DELEGATE_CALL.value,
1566
+ 0, # safeTxGas
1567
+ 0, # baseGas
1568
+ 0, # gasPrice
1569
+ ZERO_ADDRESS, # gasToken
1570
+ ZERO_ADDRESS, # refundReceiver
1571
+ get_packed_signature_for_approved_hash(owners=(self.safe,)),
1572
+ ],
1573
+ )
1574
+ exec_message = {
1575
+ "to": safe_b_address,
1576
+ "data": exec_data[2:],
1577
+ "operation": MultiSendOperation.CALL,
1578
+ "value": 0,
1579
+ }
1580
+
1581
+ return approve_hash_message, exec_message
1582
+
1583
+ def get_safe_b_erc20_transfer_messages( # pylint: disable=too-many-locals
1584
+ self,
1585
+ safe_b_address: str,
1586
+ token: str,
1587
+ to: str,
1588
+ amount: int,
1589
+ ) -> t.Tuple[t.Dict, t.Dict]:
1590
+ """
1591
+ Build the two messages (Safe calls) to withdraw ERC20 from Safe B via this Safe (owner of Safe B).
1592
+
1593
+ Builds the messages to be settled by this Safe:
1594
+ 1) approveHash(inner_tx_hash)
1595
+ 2) execTransaction(...) to transfer ERC20 tokens
1596
+ """
1597
+ safe_b_instance = registry_contracts.gnosis_safe.get_instance(
1598
+ ledger_api=self.ledger_api,
1599
+ contract_address=safe_b_address,
1600
+ )
1601
+ erc20_instance = registry_contracts.erc20.get_instance(
1602
+ ledger_api=self.ledger_api,
1603
+ contract_address=token,
1604
+ )
1605
+
1606
+ txs = []
1607
+ txs.append(
1608
+ {
1609
+ "to": token,
1610
+ "data": erc20_instance.encodeABI(
1611
+ fn_name="transfer",
1612
+ args=[to, amount],
1613
+ ),
1614
+ "operation": MultiSendOperation.CALL,
1615
+ "value": 0,
1616
+ }
1617
+ )
1618
+
1619
+ multisend_address = ContractConfigs.get(MULTISEND_CONTRACT.name).contracts[
1620
+ self.chain_type
1621
+ ]
1622
+ multisend_tx = registry_contracts.multisend.get_multisend_tx(
1623
+ ledger_api=self.ledger_api,
1624
+ contract_address=multisend_address,
1625
+ txs=txs,
1626
+ )
1627
+
1628
+ # Compute inner Safe transaction hash
1629
+ safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
1630
+ ledger_api=self.ledger_api,
1631
+ contract_address=safe_b_address,
1632
+ to_address=multisend_address,
1633
+ value=multisend_tx["value"],
1634
+ data=multisend_tx["data"],
1635
+ operation=SafeOperation.CALL.value,
1636
+ ).get("tx_hash")
1637
+
1638
+ # Build approveHash message
1639
+ approve_hash_data = safe_b_instance.encodeABI(
1640
+ fn_name="approveHash",
1641
+ args=[safe_tx_hash],
1642
+ )
1643
+ approve_hash_message = {
1644
+ "to": safe_b_address,
1645
+ "data": approve_hash_data[2:],
1646
+ "operation": MultiSendOperation.CALL,
1647
+ "value": 0,
1648
+ }
1649
+
1650
+ # Build execTransaction message
1651
+ exec_data = safe_b_instance.encodeABI(
1652
+ fn_name="execTransaction",
1653
+ args=[
1654
+ multisend_address,
1655
+ multisend_tx["value"],
1656
+ multisend_tx["data"],
1657
+ SafeOperation.DELEGATE_CALL.value,
1658
+ 0, # safeTxGas
1659
+ 0, # baseGas
1660
+ 0, # gasPrice
1661
+ ZERO_ADDRESS, # gasToken
1662
+ ZERO_ADDRESS, # refundReceiver
1663
+ get_packed_signature_for_approved_hash(owners=(self.safe,)),
1664
+ ],
1665
+ )
1666
+ exec_message = {
1667
+ "to": safe_b_address,
1668
+ "data": exec_data[2:],
1669
+ "operation": MultiSendOperation.CALL,
1670
+ "value": 0,
1671
+ }
1672
+
1673
+ return approve_hash_message, exec_message
1674
+
1037
1675
  def get_terminate_data(self, service_id: int) -> t.Dict:
1038
1676
  """Get terminate tx data."""
1039
1677
  instance = registry_contracts.service_manager.get_instance(
1040
1678
  ledger_api=self.ledger_api,
1041
- contract_address=self.contracts["service_manager"],
1679
+ contract_address=self.service_manager_address,
1042
1680
  )
1043
1681
  txd = instance.encodeABI(
1044
1682
  fn_name="terminate",
1045
1683
  args=[service_id],
1046
1684
  )
1047
1685
  return {
1048
- "to": self.contracts["service_manager"],
1686
+ "to": self.service_manager_address,
1049
1687
  "data": txd[2:],
1050
1688
  "operation": MultiSendOperation.CALL,
1051
1689
  "value": 0,
@@ -1055,14 +1693,14 @@ class EthSafeTxBuilder(_ChainUtil):
1055
1693
  """Get unbond tx data."""
1056
1694
  instance = registry_contracts.service_manager.get_instance(
1057
1695
  ledger_api=self.ledger_api,
1058
- contract_address=self.contracts["service_manager"],
1696
+ contract_address=self.service_manager_address,
1059
1697
  )
1060
1698
  txd = instance.encodeABI(
1061
1699
  fn_name="unbond",
1062
1700
  args=[service_id],
1063
1701
  )
1064
1702
  return {
1065
- "to": self.contracts["service_manager"],
1703
+ "to": self.service_manager_address,
1066
1704
  "data": txd[2:],
1067
1705
  "operation": MultiSendOperation.CALL,
1068
1706
  "value": 0,
@@ -1077,16 +1715,14 @@ class EthSafeTxBuilder(_ChainUtil):
1077
1715
  """Get staking approval data"""
1078
1716
  self._patch()
1079
1717
  txd = StakingManager(
1080
- key=self.wallet.key_path,
1081
- password=self.wallet.password,
1082
- chain_type=self.chain_type,
1718
+ chain=OperateChain(self.chain_type.value),
1083
1719
  ).get_stake_approval_tx_data(
1084
1720
  service_id=service_id,
1085
1721
  service_registry=service_registry,
1086
1722
  staking_contract=staking_contract,
1087
1723
  )
1088
1724
  return {
1089
- "from": self.wallet.safe,
1725
+ "from": self.safe,
1090
1726
  "to": self.contracts["service_registry"],
1091
1727
  "data": txd[2:],
1092
1728
  "operation": MultiSendOperation.CALL,
@@ -1101,9 +1737,7 @@ class EthSafeTxBuilder(_ChainUtil):
1101
1737
  """Get staking tx data"""
1102
1738
  self._patch()
1103
1739
  txd = StakingManager(
1104
- key=self.wallet.key_path,
1105
- password=self.wallet.password,
1106
- chain_type=self.chain_type,
1740
+ chain=OperateChain(self.chain_type.value),
1107
1741
  ).get_stake_tx_data(
1108
1742
  service_id=service_id,
1109
1743
  staking_contract=staking_contract,
@@ -1119,14 +1753,42 @@ class EthSafeTxBuilder(_ChainUtil):
1119
1753
  self,
1120
1754
  service_id: int,
1121
1755
  staking_contract: str,
1756
+ force: bool = False,
1122
1757
  ) -> t.Dict:
1123
1758
  """Get unstaking tx data"""
1124
1759
  self._patch()
1125
- txd = StakingManager(
1126
- key=self.wallet.key_path,
1127
- password=self.wallet.password,
1128
- chain_type=self.chain_type,
1129
- ).get_unstake_tx_data(
1760
+ staking_manager = StakingManager(
1761
+ chain=OperateChain(self.chain_type.value),
1762
+ )
1763
+ txd = (
1764
+ staking_manager.get_forced_unstake_tx_data(
1765
+ service_id=service_id,
1766
+ staking_contract=staking_contract,
1767
+ )
1768
+ if force
1769
+ else staking_manager.get_unstake_tx_data(
1770
+ service_id=service_id,
1771
+ staking_contract=staking_contract,
1772
+ )
1773
+ )
1774
+ return {
1775
+ "to": staking_contract,
1776
+ "data": txd[2:],
1777
+ "operation": MultiSendOperation.CALL,
1778
+ "value": 0,
1779
+ }
1780
+
1781
+ def get_claiming_data(
1782
+ self,
1783
+ service_id: int,
1784
+ staking_contract: str,
1785
+ ) -> t.Dict:
1786
+ """Get claiming tx data"""
1787
+ self._patch()
1788
+ staking_manager = StakingManager(
1789
+ chain=OperateChain(self.chain_type.value),
1790
+ )
1791
+ txd = staking_manager.get_claim_tx_data(
1130
1792
  service_id=service_id,
1131
1793
  staking_contract=staking_contract,
1132
1794
  )
@@ -1141,26 +1803,270 @@ class EthSafeTxBuilder(_ChainUtil):
1141
1803
  """Stake service."""
1142
1804
  self._patch()
1143
1805
  return StakingManager(
1144
- key=self.wallet.key_path,
1145
- password=self.wallet.password,
1146
- chain_type=self.chain_type,
1806
+ chain=OperateChain(self.chain_type.value),
1147
1807
  ).slots_available(
1148
1808
  staking_contract=staking_contract,
1149
1809
  )
1150
1810
 
1151
- def staking_status(self, service_id: int, staking_contract: str) -> StakingState:
1152
- """Stake the service"""
1811
+ def can_unstake(self, service_id: int, staking_contract: str) -> bool:
1812
+ """Can unstake the service?"""
1153
1813
  self._patch()
1154
- return StakingManager(
1155
- key=self.wallet.key_path,
1156
- password=self.wallet.password,
1157
- chain_type=self.chain_type,
1158
- ).status(
1159
- service_id=service_id,
1160
- staking_contract=staking_contract,
1161
- )
1814
+ try:
1815
+ StakingManager(
1816
+ chain=OperateChain(self.chain_type.value),
1817
+ ).check_if_unstaking_possible(
1818
+ service_id=service_id,
1819
+ staking_contract=staking_contract,
1820
+ )
1821
+ return True
1822
+ except ValueError:
1823
+ return False
1162
1824
 
1163
1825
  def get_swap_data(self, service_id: int, multisig: str, owner_key: str) -> t.Dict:
1164
1826
  """Swap safe owner."""
1165
1827
  # TODO: Discuss implementation
1166
1828
  raise NotImplementedError()
1829
+
1830
+ def get_recover_access_data(self, service_id: int) -> t.Dict:
1831
+ """Get recover access tx data."""
1832
+ instance = t.cast(
1833
+ RecoveryModule,
1834
+ RecoveryModule.from_dir(
1835
+ directory=str(DATA_DIR / "contracts" / "recovery_module"),
1836
+ ),
1837
+ ).get_instance(
1838
+ ledger_api=self.ledger_api,
1839
+ contract_address=self.contracts["recovery_module"],
1840
+ )
1841
+ # TODO Replace the line above by this one once the recovery_module is
1842
+ # included in the release of OpenAutonomy.
1843
+ # instance = registry_contracts.recovery_module.get_instance( # noqa: E800
1844
+ # ledger_api=self.ledger_api, # noqa: E800
1845
+ # contract_address=self.contracts["recovery_module"], # noqa: E800
1846
+ # ) # noqa: E800
1847
+ txd = instance.encodeABI(
1848
+ fn_name="recoverAccess",
1849
+ args=[service_id],
1850
+ )
1851
+ return {
1852
+ "to": self.contracts["recovery_module"],
1853
+ "data": txd[2:],
1854
+ "operation": MultiSendOperation.CALL,
1855
+ "value": 0,
1856
+ }
1857
+
1858
+ def get_enable_module_data(
1859
+ self,
1860
+ safe_address: str,
1861
+ module_address: str,
1862
+ ) -> t.Dict:
1863
+ """Get enable module tx data"""
1864
+ self._patch()
1865
+ instance = registry_contracts.gnosis_safe.get_instance(
1866
+ ledger_api=self.ledger_api,
1867
+ contract_address=safe_address,
1868
+ )
1869
+ txd = instance.encodeABI(
1870
+ fn_name="enableModule",
1871
+ args=[module_address],
1872
+ )
1873
+ return {
1874
+ "to": safe_address,
1875
+ "data": txd[2:],
1876
+ "operation": MultiSendOperation.CALL,
1877
+ "value": 0,
1878
+ }
1879
+
1880
+
1881
+ def get_packed_signature_for_approved_hash(owners: t.Tuple[str]) -> bytes:
1882
+ """Get the packed signatures."""
1883
+ sorted_owners = sorted(owners, key=str.lower)
1884
+ signatures = b""
1885
+ for owner in sorted_owners:
1886
+ # Convert address to bytes and ensure it is 32 bytes long (left-padded with zeros)
1887
+ r_bytes = to_bytes(hexstr=owner[2:].rjust(64, "0"))
1888
+
1889
+ # `s` as 32 zero bytes
1890
+ s_bytes = b"\x00" * 32
1891
+
1892
+ # `v` as a single byte
1893
+ v_bytes = to_bytes(1)
1894
+
1895
+ # Concatenate r, s, and v to form the packed signature
1896
+ packed_signature = r_bytes + s_bytes + v_bytes
1897
+ signatures += packed_signature
1898
+
1899
+ return signatures
1900
+
1901
+
1902
+ def get_reuse_multisig_from_safe_payload( # pylint: disable=too-many-locals
1903
+ ledger_api: LedgerApi,
1904
+ chain_type: ChainType,
1905
+ service_id: int,
1906
+ master_safe: str,
1907
+ ) -> t.Tuple[Optional[str], Optional[t.Dict[str, t.Any]], Optional[str]]:
1908
+ """Reuse multisig."""
1909
+ _, multisig_address, _, threshold, *_ = get_service_info(
1910
+ ledger_api=ledger_api,
1911
+ chain_type=chain_type,
1912
+ token_id=service_id,
1913
+ )
1914
+ if multisig_address == ZERO_ADDRESS:
1915
+ return None, None, "Cannot reuse multisig, No previous deployment exist!"
1916
+
1917
+ multisend_address = ContractConfigs.get(MULTISEND_CONTRACT.name).contracts[
1918
+ chain_type
1919
+ ]
1920
+ multisig_instance = registry_contracts.gnosis_safe.get_instance(
1921
+ ledger_api=ledger_api,
1922
+ contract_address=multisig_address,
1923
+ )
1924
+
1925
+ # Verify if the service was terminated properly or not
1926
+ old_owners = multisig_instance.functions.getOwners().call()
1927
+ if len(old_owners) != 1 or master_safe not in old_owners:
1928
+ return (
1929
+ None,
1930
+ None,
1931
+ "Service was not terminated properly, the service owner should be the only owner of the safe",
1932
+ )
1933
+
1934
+ # Build multisend tx to add new instances as owners
1935
+ txs = []
1936
+ new_owners = t.cast(
1937
+ t.List[str],
1938
+ get_agent_instances(
1939
+ ledger_api=ledger_api,
1940
+ chain_type=chain_type,
1941
+ token_id=service_id,
1942
+ ).get("agentInstances"),
1943
+ )
1944
+
1945
+ for _owner in new_owners:
1946
+ txs.append(
1947
+ {
1948
+ "to": multisig_address,
1949
+ "data": HexBytes(
1950
+ bytes.fromhex(
1951
+ multisig_instance.encodeABI(
1952
+ fn_name="addOwnerWithThreshold",
1953
+ args=[_owner, 1],
1954
+ )[2:]
1955
+ )
1956
+ ),
1957
+ "operation": MultiSendOperation.CALL,
1958
+ "value": 0,
1959
+ }
1960
+ )
1961
+
1962
+ txs.append(
1963
+ {
1964
+ "to": multisig_address,
1965
+ "data": HexBytes(
1966
+ bytes.fromhex(
1967
+ multisig_instance.encodeABI(
1968
+ fn_name="removeOwner",
1969
+ args=[new_owners[0], master_safe, 1],
1970
+ )[2:]
1971
+ )
1972
+ ),
1973
+ "operation": MultiSendOperation.CALL,
1974
+ "value": 0,
1975
+ }
1976
+ )
1977
+
1978
+ txs.append(
1979
+ {
1980
+ "to": multisig_address,
1981
+ "data": HexBytes(
1982
+ bytes.fromhex(
1983
+ multisig_instance.encodeABI(
1984
+ fn_name="changeThreshold",
1985
+ args=[threshold],
1986
+ )[2:]
1987
+ )
1988
+ ),
1989
+ "operation": MultiSendOperation.CALL,
1990
+ "value": 0,
1991
+ }
1992
+ )
1993
+
1994
+ multisend_tx = registry_contracts.multisend.get_multisend_tx(
1995
+ ledger_api=ledger_api,
1996
+ contract_address=multisend_address,
1997
+ txs=txs,
1998
+ )
1999
+ signature_bytes = get_packed_signature_for_approved_hash(owners=(master_safe,))
2000
+
2001
+ safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
2002
+ ledger_api=ledger_api,
2003
+ contract_address=multisig_address,
2004
+ to_address=multisend_address,
2005
+ value=multisend_tx["value"],
2006
+ data=multisend_tx["data"],
2007
+ operation=1,
2008
+ ).get("tx_hash")
2009
+ approve_hash_data = multisig_instance.encodeABI(
2010
+ fn_name="approveHash",
2011
+ args=[
2012
+ safe_tx_hash,
2013
+ ],
2014
+ )
2015
+ approve_hash_message = {
2016
+ "to": multisig_address,
2017
+ "data": approve_hash_data[2:],
2018
+ "operation": MultiSendOperation.CALL,
2019
+ "value": 0,
2020
+ }
2021
+
2022
+ safe_exec_data = multisig_instance.encodeABI(
2023
+ fn_name="execTransaction",
2024
+ args=[
2025
+ multisend_address, # to address
2026
+ multisend_tx["value"], # value
2027
+ multisend_tx["data"], # data
2028
+ 1, # operation
2029
+ 0, # safe tx gas
2030
+ 0, # bas gas
2031
+ 0, # safe gas price
2032
+ ZERO_ADDRESS, # gas token
2033
+ ZERO_ADDRESS, # refund receiver
2034
+ signature_bytes, # signatures
2035
+ ],
2036
+ )
2037
+ payload = multisig_address + safe_exec_data[2:]
2038
+ return payload, approve_hash_message, None
2039
+
2040
+
2041
+ def get_reuse_multisig_with_recovery_from_safe_payload( # pylint: disable=too-many-locals
2042
+ ledger_api: LedgerApi,
2043
+ chain_type: ChainType,
2044
+ service_id: int,
2045
+ master_safe: str,
2046
+ ) -> t.Tuple[Optional[str], Optional[str]]:
2047
+ """Reuse multisig."""
2048
+ _, multisig_address, _, _, *_ = get_service_info(
2049
+ ledger_api=ledger_api,
2050
+ chain_type=chain_type,
2051
+ token_id=service_id,
2052
+ )
2053
+ if multisig_address == ZERO_ADDRESS:
2054
+ return None, "Cannot reuse multisig, No previous deployment exist!"
2055
+
2056
+ service_owner = master_safe
2057
+
2058
+ multisig_instance = registry_contracts.gnosis_safe.get_instance(
2059
+ ledger_api=ledger_api,
2060
+ contract_address=multisig_address,
2061
+ )
2062
+
2063
+ # Verify if the service was terminated properly or not
2064
+ old_owners = multisig_instance.functions.getOwners().call()
2065
+ if len(old_owners) != 1 or service_owner not in old_owners:
2066
+ return (
2067
+ None,
2068
+ "Service was not terminated properly, the service owner should be the only owner of the safe",
2069
+ )
2070
+
2071
+ payload = "0x" + int(service_id).to_bytes(32, "big").hex()
2072
+ return payload, None