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
@@ -19,67 +19,107 @@
19
19
  # ------------------------------------------------------------------------------
20
20
  """Service manager."""
21
21
 
22
- import asyncio
22
+ import json
23
23
  import logging
24
+ import os
25
+ import time
24
26
  import traceback
25
27
  import typing as t
26
- from concurrent.futures import ThreadPoolExecutor
28
+ from collections import Counter, defaultdict
29
+ from contextlib import suppress
30
+ from http import HTTPStatus
27
31
  from pathlib import Path
28
32
 
29
- import aiohttp # type: ignore
33
+ import requests
30
34
  from aea.helpers.base import IPFSHash
31
- from aea.helpers.logging import setup_logger
35
+ from aea_ledger_ethereum import LedgerApi
32
36
  from autonomy.chain.base import registry_contracts
33
-
34
- from operate.keys import Key, KeysManager
35
- from operate.ledger import PUBLIC_RPCS
36
- from operate.ledger.profiles import CONTRACTS, OLAS, STAKING
37
- from operate.services.protocol import EthSafeTxBuilder, OnChainManager, StakingState
37
+ from autonomy.chain.config import CHAIN_PROFILES, ChainType
38
+ from autonomy.chain.metadata import IPFS_URI_PREFIX
39
+
40
+ from operate.constants import (
41
+ AGENT_LOG_DIR,
42
+ AGENT_LOG_ENV_VAR,
43
+ AGENT_PERSISTENT_STORAGE_DIR,
44
+ AGENT_PERSISTENT_STORAGE_ENV_VAR,
45
+ DEFAULT_TOPUP_THRESHOLD,
46
+ IPFS_ADDRESS,
47
+ MIN_AGENT_BOND,
48
+ MIN_SECURITY_DEPOSIT,
49
+ ZERO_ADDRESS,
50
+ )
51
+ from operate.data import DATA_DIR
52
+ from operate.data.contracts.mech_activity.contract import MechActivityContract
53
+ from operate.data.contracts.requester_activity_checker.contract import (
54
+ RequesterActivityCheckerContract,
55
+ )
56
+ from operate.keys import KeysManager
57
+ from operate.ledger import get_default_rpc
58
+ from operate.ledger.profiles import (
59
+ CONTRACTS,
60
+ DEFAULT_EOA_THRESHOLD,
61
+ DEFAULT_EOA_TOPUPS,
62
+ DEFAULT_PRIORITY_MECH,
63
+ OLAS,
64
+ WRAPPED_NATIVE_ASSET,
65
+ get_staking_contract,
66
+ )
67
+ from operate.operate_types import (
68
+ Chain,
69
+ ChainAmounts,
70
+ FundingValues,
71
+ LedgerConfig,
72
+ MechMarketplaceConfig,
73
+ OnChainState,
74
+ ServiceEnvProvisionType,
75
+ ServiceTemplate,
76
+ )
77
+ from operate.services.funding_manager import FundingManager
78
+ from operate.services.protocol import (
79
+ EthSafeTxBuilder,
80
+ OnChainManager,
81
+ StakingManager,
82
+ StakingState,
83
+ )
38
84
  from operate.services.service import (
85
+ ChainConfig,
39
86
  Deployment,
87
+ NON_EXISTENT_MULTISIG,
88
+ NON_EXISTENT_TOKEN,
40
89
  OnChainData,
41
- OnChainState,
42
- OnChainUserParams,
90
+ SERVICE_CONFIG_PREFIX,
91
+ SERVICE_CONFIG_VERSION,
43
92
  Service,
44
93
  )
45
- from operate.wallet.master import MasterWalletManager
94
+ from operate.services.utils.mech import deploy_mech
95
+ from operate.utils.gnosis import (
96
+ get_asset_balance,
97
+ get_assets_balances,
98
+ transfer_erc20_from_safe,
99
+ )
100
+ from operate.wallet.master import InsufficientFundsException, MasterWalletManager
46
101
 
47
102
 
48
103
  # pylint: disable=redefined-builtin
49
104
 
50
- OPERATE = ".operate"
51
- CONFIG = "config.json"
52
- SERVICES = "services"
53
- KEYS = "keys"
54
- DEPLOYMENT = "deployment"
55
- CONFIG = "config.json"
56
- KEY = "master-key.txt"
57
- KEYS_JSON = "keys.json"
58
- DOCKER_COMPOSE_YAML = "docker-compose.yaml"
59
- SERVICE_YAML = "service.yaml"
60
- HTTP_OK = 200
61
-
105
+ # At the moment, we only support running one agent per service locally on a machine.
106
+ # If multiple agents are provided in the service.yaml file, only the 0th index config will be used.
107
+ NUM_LOCAL_AGENT_INSTANCES = 1
62
108
 
63
- async def check_service_health() -> bool:
64
- """Check the service health"""
65
- async with aiohttp.ClientSession() as session:
66
- async with session.get("http://localhost:8716/healthcheck") as resp:
67
- status = resp.status
68
- response_json = await resp.json()
69
- return status == HTTP_OK and response_json.get(
70
- "is_transitioning_fast", False
71
- )
109
+ RPC_SYNC_TIMEOUT = 15
72
110
 
73
111
 
74
112
  class ServiceManager:
75
113
  """Service manager."""
76
114
 
77
- def __init__(
115
+ def __init__( # pylint: disable=too-many-arguments
78
116
  self,
79
117
  path: Path,
80
118
  keys_manager: KeysManager,
81
119
  wallet_manager: MasterWalletManager,
82
- logger: t.Optional[logging.Logger] = None,
120
+ funding_manager: FundingManager,
121
+ logger: logging.Logger,
122
+ skip_dependency_check: t.Optional[bool] = False,
83
123
  ) -> None:
84
124
  """
85
125
  Initialze service manager
@@ -92,136 +132,274 @@ class ServiceManager:
92
132
  self.path = path
93
133
  self.keys_manager = keys_manager
94
134
  self.wallet_manager = wallet_manager
95
- self.logger = logger or setup_logger(name="operate.manager")
135
+ self.funding_manager = funding_manager
136
+ self.logger = logger
137
+ self.skip_depencency_check = skip_dependency_check
96
138
 
97
139
  def setup(self) -> None:
98
140
  """Setup service manager."""
99
141
  self.path.mkdir(exist_ok=True)
100
142
 
143
+ def get_all_service_ids(self) -> t.List[str]:
144
+ """
145
+ Get all service ids.
146
+
147
+ :return: List of service ids.
148
+ """
149
+ return [
150
+ path.name
151
+ for path in self.path.iterdir()
152
+ if path.is_dir() and path.name.startswith(SERVICE_CONFIG_PREFIX)
153
+ ]
154
+
155
+ def get_all_services(self) -> t.Tuple[t.List[Service], bool]:
156
+ """Get all services."""
157
+ services = []
158
+ success = True
159
+ for path in self.path.iterdir():
160
+ if not path.name.startswith(SERVICE_CONFIG_PREFIX):
161
+ continue
162
+ try:
163
+ service = Service.load(path=path)
164
+ if service.version != SERVICE_CONFIG_VERSION:
165
+ self.logger.warning(
166
+ f"Service {path.name} has an unsupported version: {service.version}."
167
+ )
168
+ success = False
169
+ continue
170
+
171
+ services.append(service)
172
+ except Exception as e: # pylint: disable=broad-except
173
+ self.logger.error(
174
+ f"Failed to load service: {path.name}. Exception {e}: {traceback.format_exc()}"
175
+ )
176
+ success = False
177
+
178
+ return services, success
179
+
180
+ def validate_services(self) -> bool:
181
+ """
182
+ Validate all services.
183
+
184
+ :return: True if all services are valid, False otherwise.
185
+ """
186
+ _, success = self.get_all_services()
187
+ return success
188
+
101
189
  @property
102
190
  def json(self) -> t.List[t.Dict]:
103
191
  """Returns the list of available services."""
104
- data = []
105
- for path in self.path.iterdir():
106
- if not path.name.startswith("bafybei"):
107
- continue
108
- service = Service.load(path=path)
109
- data.append(service.json)
110
- return data
192
+ services, _ = self.get_all_services()
193
+ return [service.json for service in services]
111
194
 
112
- def exists(self, service: str) -> bool:
195
+ def exists(self, service_config_id: str) -> bool:
113
196
  """Check if service exists."""
114
- return (self.path / service).exists()
197
+ return (self.path / service_config_id).exists()
115
198
 
116
- def get_on_chain_manager(self, service: Service) -> OnChainManager:
199
+ def get_on_chain_manager(self, ledger_config: LedgerConfig) -> OnChainManager:
117
200
  """Get OnChainManager instance."""
118
201
  return OnChainManager(
119
- rpc=service.ledger_config.rpc,
120
- wallet=self.wallet_manager.load(service.ledger_config.type),
121
- contracts=CONTRACTS[service.ledger_config.chain],
202
+ rpc=ledger_config.rpc,
203
+ wallet=self.wallet_manager.load(ledger_config.chain.ledger_type),
204
+ contracts=CONTRACTS[ledger_config.chain],
205
+ chain_type=ChainType(ledger_config.chain.value),
122
206
  )
123
207
 
124
- def get_eth_safe_tx_builder(self, service: Service) -> EthSafeTxBuilder:
208
+ def get_eth_safe_tx_builder(self, ledger_config: LedgerConfig) -> EthSafeTxBuilder:
125
209
  """Get EthSafeTxBuilder instance."""
126
210
  return EthSafeTxBuilder(
127
- rpc=service.ledger_config.rpc,
128
- wallet=self.wallet_manager.load(service.ledger_config.type),
129
- contracts=CONTRACTS[service.ledger_config.chain],
211
+ rpc=ledger_config.rpc,
212
+ wallet=self.wallet_manager.load(ledger_config.chain.ledger_type),
213
+ contracts=CONTRACTS[ledger_config.chain],
214
+ chain_type=ChainType(ledger_config.chain.value),
130
215
  )
131
216
 
132
- def create_or_load(
217
+ def load_or_create(
133
218
  self,
134
219
  hash: str,
135
- rpc: t.Optional[str] = None,
136
- on_chain_user_params: t.Optional[OnChainUserParams] = None,
137
- keys: t.Optional[t.List[Key]] = None,
220
+ service_template: t.Optional[ServiceTemplate] = None,
221
+ agent_addresses: t.Optional[t.List[str]] = None,
138
222
  ) -> Service:
139
223
  """
140
224
  Create or load a service
141
225
 
142
226
  :param hash: Service hash
143
- :param rpc: RPC string
144
- :param on_chain_user_params: On-chain user parameters
145
- :param keys: Keys
227
+ :param service_template: Service template
228
+ :param agent_addresses: Agents' addresses to be used for the service.
146
229
  :return: Service instance
147
230
  """
148
231
  path = self.path / hash
149
232
  if path.exists():
150
- return Service.load(path=path)
233
+ service = Service.load(path=path)
234
+
235
+ if service_template is not None:
236
+ service.update_user_params_from_template(
237
+ service_template=service_template
238
+ )
151
239
 
152
- if rpc is None:
153
- raise ValueError("RPC cannot be None when creating a new service")
240
+ return service
154
241
 
155
- if on_chain_user_params is None:
242
+ if service_template is None:
156
243
  raise ValueError(
157
- "On-chain user parameters cannot be None when creating a new service"
244
+ "'service_template' cannot be None when creating a new service"
158
245
  )
159
246
 
160
- return Service.new(
161
- hash=hash,
162
- keys=keys or [],
163
- rpc=rpc,
164
- storage=self.path,
165
- on_chain_user_params=on_chain_user_params,
247
+ return self.create(
248
+ service_template=service_template, agent_addresses=agent_addresses
166
249
  )
167
250
 
168
- def deploy_service_onchain( # pylint: disable=too-many-statements
251
+ def load(
169
252
  self,
170
- hash: str,
171
- update: bool = False,
172
- ) -> None:
253
+ service_config_id: str,
254
+ ) -> Service:
173
255
  """
174
- Deploy as service on-chain
256
+ Load a service
175
257
 
176
- :param hash: Service hash
177
- :param update: Update the existing deployment
258
+ :param service_id: Service id
259
+ :return: Service instance
178
260
  """
179
- self.logger.info("Loading service")
180
- service = self.create_or_load(hash=hash)
181
- user_params = service.chain_data.user_params
182
- keys = service.keys or [
183
- self.keys_manager.get(self.keys_manager.create())
184
- for _ in range(service.helper.config.number_of_agents)
185
- ]
186
- instances = [key.address for key in keys]
187
- ocm = self.get_on_chain_manager(service=service)
188
- if user_params.use_staking and not ocm.staking_slots_available(
189
- staking_contract=STAKING[service.ledger_config.chain]
190
- ):
191
- raise ValueError("No staking slots available")
261
+ path = self.path / service_config_id
262
+ return Service.load(path=path)
192
263
 
193
- if user_params.use_staking and not ocm.staking_rewards_available(
194
- staking_contract=STAKING[service.ledger_config.chain]
195
- ):
196
- raise ValueError("No staking rewards available")
264
+ def create(
265
+ self,
266
+ service_template: ServiceTemplate,
267
+ agent_addresses: t.Optional[t.List[str]] = None,
268
+ ) -> Service:
269
+ """
270
+ Create a service
271
+
272
+ :param service_template: Service template
273
+ :param agent_addresses: Agents' addresses to be used for the service.
274
+ :return: Service instance
275
+ """
276
+ service = Service.new(
277
+ agent_addresses=agent_addresses or [],
278
+ storage=self.path,
279
+ service_template=service_template,
280
+ )
281
+
282
+ if not service.agent_addresses:
283
+ service.agent_addresses = [
284
+ self.keys_manager.create() for _ in range(NUM_LOCAL_AGENT_INSTANCES)
285
+ ]
286
+ service.store()
287
+
288
+ return service
289
+
290
+ def _get_on_chain_state(self, service: Service, chain: str) -> OnChainState:
291
+ chain_config = service.chain_configs[chain]
292
+ chain_data = chain_config.chain_data
293
+ ledger_config = chain_config.ledger_config
294
+ if chain_data.token == NON_EXISTENT_TOKEN:
295
+ return OnChainState.NON_EXISTENT
296
+
297
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
298
+ info = sftxb.info(token_id=chain_data.token)
299
+ return OnChainState(info["service_state"])
300
+
301
+ def _get_on_chain_metadata(self, chain_config: ChainConfig) -> t.Dict:
302
+ chain_data = chain_config.chain_data
303
+ ledger_config = chain_config.ledger_config
304
+ if chain_data.token == NON_EXISTENT_TOKEN:
305
+ return {}
306
+
307
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
308
+ info = sftxb.info(token_id=chain_data.token)
309
+ config_hash = info["config_hash"]
310
+ url = IPFS_ADDRESS.format(hash=config_hash)
311
+ self.logger.info(f"Fetching {url=}...")
312
+ res = requests.get(url, timeout=30)
313
+ if res.status_code == HTTPStatus.OK:
314
+ return res.json()
315
+ raise ValueError(
316
+ f"Something went wrong while trying to get the on-chain metadata from IPFS: {res}"
317
+ )
197
318
 
198
- if service.chain_data.token > -1:
319
+ def deploy_service_onchain( # pylint: disable=too-many-statements,too-many-locals
320
+ self,
321
+ service_config_id: str,
322
+ ) -> None:
323
+ """Deploy service on-chain"""
324
+ # TODO This method has not been thoroughly reviewed. Deprecated usage in favour of Safe version.
325
+
326
+ service = self.load(service_config_id=service_config_id)
327
+ for chain in service.chain_configs.keys():
328
+ self._deploy_service_onchain(
329
+ service_config_id=service_config_id,
330
+ chain=chain,
331
+ )
332
+
333
+ def _deploy_service_onchain( # pylint: disable=too-many-statements,too-many-locals
334
+ self,
335
+ service_config_id: str,
336
+ chain: str,
337
+ ) -> None:
338
+ """Deploy as service on-chain"""
339
+ # TODO This method has not been thoroughly reviewed. Deprecated usage in favour of Safe version.
340
+
341
+ self.logger.info(f"_deploy_service_onchain {chain=}")
342
+ service = self.load(service_config_id=service_config_id)
343
+ chain_config = service.chain_configs[chain]
344
+ ledger_config = chain_config.ledger_config
345
+ chain_data = chain_config.chain_data
346
+ user_params = chain_config.chain_data.user_params
347
+ ocm = self.get_on_chain_manager(ledger_config=ledger_config)
348
+
349
+ # TODO fix this
350
+ os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc
351
+
352
+ current_agent_id = None
353
+ on_chain_state = OnChainState.NON_EXISTENT
354
+ if chain_data.token > -1:
199
355
  self.logger.info("Syncing service state")
200
- info = ocm.info(token_id=service.chain_data.token)
201
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
202
- service.chain_data.instances = info["instances"]
203
- service.chain_data.multisig = info["multisig"]
356
+ info = ocm.info(token_id=chain_data.token)
357
+ on_chain_state = OnChainState(info["service_state"])
358
+ chain_data.instances = info["instances"]
359
+ chain_data.multisig = info["multisig"]
204
360
  service.store()
205
- self.logger.info(f"Service state: {service.chain_data.on_chain_state.name}")
361
+ self.logger.info(f"Service state: {on_chain_state.name}")
362
+
363
+ if user_params.use_staking:
364
+ staking_params = ocm.get_staking_params(
365
+ staking_contract=get_staking_contract(
366
+ chain=ledger_config.chain,
367
+ staking_program_id=user_params.staking_program_id,
368
+ ),
369
+ )
370
+ else: # TODO fix this - using pearl beta params
371
+ staking_params = dict( # nosec
372
+ agent_ids=[25],
373
+ service_registry="0x9338b5153AE39BB89f50468E608eD9d764B755fD", # nosec
374
+ staking_token="0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f", # nosec
375
+ service_registry_token_utility="0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8", # nosec
376
+ min_staking_deposit=20000000000000000000,
377
+ activity_checker="0x155547857680A6D51bebC5603397488988DEb1c8", # nosec
378
+ )
206
379
 
207
380
  if user_params.use_staking:
208
381
  self.logger.info("Checking staking compatibility")
209
- if service.chain_data.on_chain_state in (
210
- OnChainState.NOTMINTED,
211
- OnChainState.MINTED,
382
+
383
+ # TODO: Missing check when the service is currently staked in a program, but needs to be staked
384
+ # in a different target program. The In this case, balance = currently staked balance + safe balance
385
+
386
+ if on_chain_state in (
387
+ OnChainState.NON_EXISTENT,
388
+ OnChainState.PRE_REGISTRATION,
212
389
  ):
213
390
  required_olas = (
214
- user_params.olas_cost_of_bond + user_params.olas_required_to_stake
391
+ staking_params["min_staking_deposit"]
392
+ + staking_params["min_staking_deposit"] # bond = staking
215
393
  )
216
- elif service.chain_data.on_chain_state == OnChainState.ACTIVATED:
217
- required_olas = user_params.olas_required_to_stake
394
+ elif on_chain_state == OnChainState.ACTIVE_REGISTRATION:
395
+ required_olas = staking_params["min_staking_deposit"]
218
396
  else:
219
397
  required_olas = 0
220
398
 
221
399
  balance = (
222
400
  registry_contracts.erc20.get_instance(
223
401
  ledger_api=ocm.ledger_api,
224
- contract_address=OLAS[service.ledger_config.chain],
402
+ contract_address=OLAS[ledger_config.chain],
225
403
  )
226
404
  .functions.balanceOf(ocm.crypto.address)
227
405
  .call()
@@ -232,181 +410,547 @@ class ServiceManager:
232
410
  f"required olas: {required_olas}; your balance {balance}"
233
411
  )
234
412
 
235
- if service.chain_data.on_chain_state == OnChainState.NOTMINTED:
413
+ on_chain_metadata = self._get_on_chain_metadata(chain_config=chain_config)
414
+ on_chain_hash = on_chain_metadata.get("code_uri", "")[len(IPFS_URI_PREFIX) :]
415
+ on_chain_description = on_chain_metadata.get("description")
416
+
417
+ current_agent_bond = staking_params[
418
+ "min_staking_deposit"
419
+ ] # TODO fixme, read from service registry token utility contract
420
+ is_first_mint = (
421
+ self._get_on_chain_state(service=service, chain=chain)
422
+ == OnChainState.NON_EXISTENT
423
+ )
424
+ is_update = (
425
+ (not is_first_mint)
426
+ and (on_chain_hash is not None)
427
+ and (
428
+ on_chain_hash != service.hash
429
+ or current_agent_id != staking_params["agent_ids"][0]
430
+ or (
431
+ user_params.use_staking
432
+ and current_agent_bond != staking_params["min_staking_deposit"]
433
+ )
434
+ # TODO Missing complete this check for non-staked services it should compare the current_agent_bond from the protocol, now it's only read for the staking contract.
435
+ or on_chain_description != service.description
436
+ )
437
+ )
438
+ current_staking_program = self._get_current_staking_program(service, chain)
439
+
440
+ self.logger.info(f"{chain_data.token=}")
441
+ self.logger.info(f"{user_params.use_staking=}")
442
+ self.logger.info(f"{current_staking_program=}")
443
+ self.logger.info(f"{user_params.staking_program_id=}")
444
+ self.logger.info(f"{on_chain_hash=}")
445
+ self.logger.info(f"{service.hash=}")
446
+ self.logger.info(f"{current_agent_id=}")
447
+ self.logger.info(f"{staking_params['agent_ids']=}")
448
+ self.logger.info(f"{current_agent_bond=}")
449
+ self.logger.info(f"{staking_params['min_staking_deposit']=}")
450
+ self.logger.info(f"{is_first_mint=}")
451
+ self.logger.info(f"{is_update=}")
452
+
453
+ if on_chain_state == OnChainState.NON_EXISTENT:
236
454
  self.logger.info("Minting service")
237
- service.chain_data.token = t.cast(
455
+ chain_data.token = t.cast(
238
456
  int,
239
457
  ocm.mint(
240
- package_path=service.service_path,
241
- agent_id=user_params.agent_id,
242
- number_of_slots=service.helper.config.number_of_agents,
458
+ package_path=service.package_absolute_path_absolute_path,
459
+ agent_id=staking_params["agent_ids"][0],
460
+ number_of_slots=NUM_LOCAL_AGENT_INSTANCES,
243
461
  cost_of_bond=(
244
- user_params.olas_cost_of_bond
462
+ staking_params["min_staking_deposit"]
245
463
  if user_params.use_staking
246
464
  else user_params.cost_of_bond
247
465
  ),
248
- threshold=user_params.threshold,
466
+ threshold=len(service.agent_addresses),
249
467
  nft=IPFSHash(user_params.nft),
250
- update_token=service.chain_data.token if update else None,
468
+ update_token=chain_data.token if is_update else None,
251
469
  token=(
252
- OLAS[service.ledger_config.chain]
253
- if user_params.use_staking
254
- else None
470
+ OLAS[ledger_config.chain] if user_params.use_staking else None
255
471
  ),
472
+ metadata_description=service.description,
473
+ skip_dependency_check=self.skip_depencency_check,
256
474
  ).get("token"),
257
475
  )
258
- service.chain_data.on_chain_state = OnChainState.MINTED
476
+ on_chain_state = OnChainState.PRE_REGISTRATION
259
477
  service.store()
260
478
 
261
- info = ocm.info(token_id=service.chain_data.token)
262
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
479
+ info = ocm.info(token_id=chain_data.token)
480
+ on_chain_state = OnChainState(info["service_state"])
263
481
 
264
- if service.chain_data.on_chain_state == OnChainState.MINTED:
482
+ if on_chain_state == OnChainState.PRE_REGISTRATION:
265
483
  self.logger.info("Activating service")
266
484
  ocm.activate(
267
- service_id=service.chain_data.token,
268
- token=(
269
- OLAS[service.ledger_config.chain]
270
- if user_params.use_staking
271
- else None
272
- ),
485
+ service_id=chain_data.token,
486
+ token=(OLAS[ledger_config.chain] if user_params.use_staking else None),
273
487
  )
274
- service.chain_data.on_chain_state = OnChainState.ACTIVATED
275
- service.store()
488
+ on_chain_state = OnChainState.ACTIVE_REGISTRATION
276
489
 
277
- info = ocm.info(token_id=service.chain_data.token)
278
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
490
+ info = ocm.info(token_id=chain_data.token)
491
+ on_chain_state = OnChainState(info["service_state"])
279
492
 
280
- if service.chain_data.on_chain_state == OnChainState.ACTIVATED:
281
- self.logger.info("Registering service")
493
+ if on_chain_state == OnChainState.ACTIVE_REGISTRATION:
494
+ self.logger.info("Registering agent instances")
495
+ agent_id = staking_params["agent_ids"][0]
282
496
  ocm.register(
283
- service_id=service.chain_data.token,
284
- instances=instances,
285
- agents=[user_params.agent_id for _ in instances],
286
- token=(
287
- OLAS[service.ledger_config.chain]
288
- if user_params.use_staking
289
- else None
290
- ),
497
+ service_id=chain_data.token,
498
+ instances=service.agent_addresses,
499
+ agents=[agent_id for _ in service.agent_addresses],
500
+ token=(OLAS[ledger_config.chain] if user_params.use_staking else None),
291
501
  )
292
- service.chain_data.on_chain_state = OnChainState.REGISTERED
293
- service.keys = keys
294
- service.store()
502
+ on_chain_state = OnChainState.FINISHED_REGISTRATION
295
503
 
296
- info = ocm.info(token_id=service.chain_data.token)
297
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
504
+ info = ocm.info(token_id=chain_data.token)
505
+ on_chain_state = OnChainState(info["service_state"])
298
506
 
299
- if service.chain_data.on_chain_state == OnChainState.REGISTERED:
507
+ if on_chain_state == OnChainState.FINISHED_REGISTRATION:
300
508
  self.logger.info("Deploying service")
301
509
  ocm.deploy(
302
- service_id=service.chain_data.token,
303
- reuse_multisig=update,
304
- token=(
305
- OLAS[service.ledger_config.chain]
306
- if user_params.use_staking
307
- else None
308
- ),
510
+ service_id=chain_data.token,
511
+ reuse_multisig=is_update,
512
+ token=(OLAS[ledger_config.chain] if user_params.use_staking else None),
309
513
  )
310
- service.chain_data.on_chain_state = OnChainState.DEPLOYED
311
- service.store()
514
+ on_chain_state = OnChainState.DEPLOYED
312
515
 
313
- info = ocm.info(token_id=service.chain_data.token)
314
- service.keys = keys
315
- service.chain_data = OnChainData(
316
- token=service.chain_data.token,
516
+ info = ocm.info(token_id=chain_data.token)
517
+ chain_data = OnChainData(
518
+ token=chain_data.token,
317
519
  instances=info["instances"],
318
520
  multisig=info["multisig"],
319
- staked=False,
320
- on_chain_state=service.chain_data.on_chain_state,
321
- user_params=service.chain_data.user_params,
521
+ user_params=chain_data.user_params,
322
522
  )
323
523
  service.store()
324
524
 
325
525
  def deploy_service_onchain_from_safe( # pylint: disable=too-many-statements,too-many-locals
326
526
  self,
327
- hash: str,
328
- update: bool = False,
527
+ service_config_id: str,
329
528
  ) -> None:
330
- """
331
- Deploy as service on-chain
529
+ """Deploy as service on-chain"""
332
530
 
333
- :param hash: Service hash
334
- :param update: Update the existing deployment
335
- """
336
- self.logger.info("Loading service")
337
- service = self.create_or_load(hash=hash)
338
- user_params = service.chain_data.user_params
339
- keys = service.keys or [
340
- self.keys_manager.get(self.keys_manager.create())
341
- for _ in range(service.helper.config.number_of_agents)
342
- ]
343
- instances = [key.address for key in keys]
344
- wallet = self.wallet_manager.load(service.ledger_config.type)
345
- sftxb = self.get_eth_safe_tx_builder(service=service)
346
- if user_params.use_staking and not sftxb.staking_slots_available(
347
- staking_contract=STAKING[service.ledger_config.chain]
348
- ):
349
- raise ValueError("No staking slots available")
531
+ service = self.load(service_config_id=service_config_id)
532
+ for chain in service.chain_configs.keys():
533
+ self._deploy_service_onchain_from_safe(
534
+ service_config_id=service_config_id,
535
+ chain=chain,
536
+ )
537
+
538
+ def get_mech_configs(
539
+ self,
540
+ chain: str,
541
+ ledger_api: LedgerApi,
542
+ staking_program_id: str | None = None,
543
+ ) -> MechMarketplaceConfig:
544
+ """Get the mech configs."""
545
+ sftxb = self.get_eth_safe_tx_builder(
546
+ ledger_config=LedgerConfig(
547
+ chain=Chain(chain),
548
+ rpc=ledger_api.api.provider.endpoint_uri,
549
+ )
550
+ )
551
+ staking_contract = get_staking_contract(
552
+ chain=chain,
553
+ staking_program_id=staking_program_id,
554
+ )
555
+ if staking_contract is None:
556
+ return MechMarketplaceConfig(
557
+ use_mech_marketplace=False,
558
+ mech_marketplace_address=ZERO_ADDRESS,
559
+ priority_mech_address=ZERO_ADDRESS,
560
+ priority_mech_service_id=0,
561
+ )
562
+
563
+ target_staking_params = sftxb.get_staking_params(
564
+ staking_contract=get_staking_contract(
565
+ chain=chain,
566
+ staking_program_id=staking_program_id,
567
+ ),
568
+ )
569
+
570
+ try:
571
+ # Try if activity checker is a MechActivityChecker contract
572
+ mech_activity_contract = t.cast(
573
+ MechActivityContract,
574
+ MechActivityContract.from_dir(
575
+ directory=str(DATA_DIR / "contracts" / "mech_activity")
576
+ ),
577
+ )
578
+
579
+ priority_mech_address = (
580
+ mech_activity_contract.get_instance(
581
+ ledger_api=ledger_api,
582
+ contract_address=target_staking_params["activity_checker"],
583
+ )
584
+ .functions.agentMech()
585
+ .call()
586
+ )
587
+ use_mech_marketplace = False
588
+ mech_marketplace_address = ZERO_ADDRESS
589
+ priority_mech_service_id = 0
590
+
591
+ except Exception: # pylint: disable=broad-except
592
+ # Try if activity checker is a RequesterActivityChecker contract
593
+ try:
594
+ requester_activity_checker = t.cast(
595
+ RequesterActivityCheckerContract,
596
+ RequesterActivityCheckerContract.from_dir(
597
+ directory=str(
598
+ DATA_DIR / "contracts" / "requester_activity_checker"
599
+ )
600
+ ),
601
+ )
602
+
603
+ mech_marketplace_address = (
604
+ requester_activity_checker.get_instance(
605
+ ledger_api=ledger_api,
606
+ contract_address=target_staking_params["activity_checker"],
607
+ )
608
+ .functions.mechMarketplace()
609
+ .call()
610
+ )
350
611
 
351
- if service.chain_data.token > -1:
612
+ use_mech_marketplace = True
613
+ priority_mech_address, priority_mech_service_id = DEFAULT_PRIORITY_MECH[
614
+ mech_marketplace_address
615
+ ]
616
+
617
+ except Exception as e: # pylint: disable=broad-except
618
+ self.logger.debug(f"{e}: {traceback.format_exc()}")
619
+ self.logger.warning(
620
+ "Cannot determine type of activity checker contract. Using default parameters. "
621
+ "NOTE: This will be an exception in the future!"
622
+ )
623
+ priority_mech_address = "0x77af31De935740567Cf4fF1986D04B2c964A786a"
624
+ use_mech_marketplace = False
625
+ mech_marketplace_address = ZERO_ADDRESS
626
+ priority_mech_service_id = 0
627
+
628
+ return MechMarketplaceConfig(
629
+ use_mech_marketplace=use_mech_marketplace,
630
+ mech_marketplace_address=mech_marketplace_address,
631
+ priority_mech_address=priority_mech_address,
632
+ priority_mech_service_id=priority_mech_service_id,
633
+ )
634
+
635
+ def _deploy_service_onchain_from_safe( # pylint: disable=too-many-statements,too-many-locals
636
+ self,
637
+ service_config_id: str,
638
+ chain: str,
639
+ ) -> None:
640
+ """Deploy service on-chain"""
641
+
642
+ self.logger.info(f"_deploy_service_onchain_from_safe {chain=}")
643
+ service = self.load(service_config_id=service_config_id)
644
+ service.remove_latest_healthcheck()
645
+ chain_config = service.chain_configs[chain]
646
+ ledger_config = chain_config.ledger_config
647
+ chain_data = chain_config.chain_data
648
+ user_params = chain_config.chain_data.user_params
649
+ wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
650
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
651
+ safe = wallet.safes[Chain(chain)]
652
+
653
+ # TODO fix this
654
+ os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc
655
+
656
+ self._enable_recovery_module(service_config_id=service_config_id, chain=chain)
657
+
658
+ current_agent_id = None
659
+ on_chain_state = OnChainState.NON_EXISTENT
660
+ if chain_data.token > -1:
352
661
  self.logger.info("Syncing service state")
353
- info = sftxb.info(token_id=service.chain_data.token)
354
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
355
- service.chain_data.instances = info["instances"]
356
- service.chain_data.multisig = info["multisig"]
662
+ info = sftxb.info(token_id=chain_data.token)
663
+ on_chain_state = OnChainState(info["service_state"])
664
+ chain_data.instances = info["instances"]
665
+ chain_data.multisig = info["multisig"]
666
+ current_agent_id = info["canonical_agents"][0] # TODO Allow multiple agents
357
667
  service.store()
358
- self.logger.info(f"Service state: {service.chain_data.on_chain_state.name}")
668
+ self.logger.info(f"Service state: {on_chain_state.name}")
669
+
670
+ current_staking_program = self._get_current_staking_program(service, chain)
671
+ fallback_params = dict( # nosec
672
+ staking_contract=ZERO_ADDRESS,
673
+ agent_ids=[user_params.agent_id],
674
+ service_registry="0x9338b5153AE39BB89f50468E608eD9d764B755fD", # nosec
675
+ staking_token=ZERO_ADDRESS, # nosec
676
+ service_registry_token_utility="0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8", # nosec
677
+ min_staking_deposit=20000000000000000000,
678
+ activity_checker=ZERO_ADDRESS, # nosec
679
+ )
680
+
681
+ current_staking_params = sftxb.get_staking_params(
682
+ fallback_params=fallback_params,
683
+ staking_contract=get_staking_contract(
684
+ chain=ledger_config.chain,
685
+ staking_program_id=current_staking_program,
686
+ ),
687
+ )
688
+ target_staking_params = sftxb.get_staking_params(
689
+ fallback_params=fallback_params,
690
+ staking_contract=get_staking_contract(
691
+ chain=ledger_config.chain,
692
+ staking_program_id=user_params.staking_program_id,
693
+ ),
694
+ )
695
+
696
+ # TODO A customized, arbitrary computation mechanism should be devised.
697
+ env_var_to_value = {}
698
+ if chain == service.home_chain:
699
+ mech_configs: MechMarketplaceConfig = self.get_mech_configs(
700
+ chain=chain,
701
+ ledger_api=sftxb.ledger_api,
702
+ staking_program_id=user_params.staking_program_id,
703
+ )
704
+
705
+ if (
706
+ "PRIORITY_MECH_ADDRESS" in service.env_variables
707
+ and service.env_variables["PRIORITY_MECH_ADDRESS"]["provision_type"]
708
+ == ServiceEnvProvisionType.USER
709
+ ):
710
+ mech_configs.priority_mech_address = service.env_variables[
711
+ "PRIORITY_MECH_ADDRESS"
712
+ ]["value"]
713
+
714
+ if (
715
+ "PRIORITY_MECH_SERVICE_ID" in service.env_variables
716
+ and service.env_variables["PRIORITY_MECH_SERVICE_ID"]["provision_type"]
717
+ == ServiceEnvProvisionType.USER
718
+ ):
719
+ mech_configs.priority_mech_service_id = service.env_variables[
720
+ "PRIORITY_MECH_SERVICE_ID"
721
+ ]["value"]
722
+
723
+ env_var_to_value.update(
724
+ {
725
+ "ARBITRUM_ONE_LEDGER_RPC": get_default_rpc(Chain.ARBITRUM_ONE),
726
+ "BASE_LEDGER_RPC": get_default_rpc(Chain.BASE),
727
+ "CELO_LEDGER_RPC": get_default_rpc(Chain.CELO),
728
+ "ETHEREUM_LEDGER_RPC": get_default_rpc(Chain.ETHEREUM),
729
+ "GNOSIS_LEDGER_RPC": get_default_rpc(Chain.GNOSIS),
730
+ "MODE_LEDGER_RPC": get_default_rpc(Chain.MODE),
731
+ "OPTIMISM_LEDGER_RPC": get_default_rpc(Chain.OPTIMISM),
732
+ "POLYGON_LEDGER_RPC": get_default_rpc(Chain.POLYGON),
733
+ "SOLANA_LEDGER_RPC": get_default_rpc(Chain.SOLANA),
734
+ f"{chain.upper()}_LEDGER_RPC": ledger_config.rpc,
735
+ "STAKING_CONTRACT_ADDRESS": target_staking_params.get(
736
+ "staking_contract"
737
+ ),
738
+ "STAKING_TOKEN_CONTRACT_ADDRESS": target_staking_params.get(
739
+ "staking_contract"
740
+ ),
741
+ "MECH_MARKETPLACE_CONFIG": (
742
+ f'{{"mech_marketplace_address":"{mech_configs.mech_marketplace_address}",'
743
+ f'"priority_mech_address":"{mech_configs.priority_mech_address}",'
744
+ f'"priority_mech_staking_instance_address":"0x998dEFafD094817EF329f6dc79c703f1CF18bC90",'
745
+ f'"priority_mech_service_id":{mech_configs.priority_mech_service_id},'
746
+ f'"requester_staking_instance_address":"{target_staking_params.get("staking_contract")}",'
747
+ f'"response_timeout":300}}'
748
+ ),
749
+ "ACTIVITY_CHECKER_CONTRACT_ADDRESS": target_staking_params.get(
750
+ "activity_checker"
751
+ ),
752
+ "MECH_ACTIVITY_CHECKER_CONTRACT": target_staking_params.get(
753
+ "activity_checker"
754
+ ),
755
+ "MECH_CONTRACT_ADDRESS": mech_configs.priority_mech_address,
756
+ "MECH_REQUEST_PRICE": "10000000000000000",
757
+ "USE_MECH_MARKETPLACE": mech_configs.use_mech_marketplace,
758
+ }
759
+ )
760
+
761
+ # Set environment variables for the service
762
+ for dir_name, env_var_name in (
763
+ (AGENT_PERSISTENT_STORAGE_DIR, AGENT_PERSISTENT_STORAGE_ENV_VAR),
764
+ (AGENT_LOG_DIR, AGENT_LOG_ENV_VAR),
765
+ ):
766
+ dir_path = service.path / dir_name
767
+ dir_path.mkdir(parents=True, exist_ok=True)
768
+ env_var_to_value.update({env_var_name: str(dir_path)})
769
+
770
+ service.update_env_variables_values(env_var_to_value)
359
771
 
360
772
  if user_params.use_staking:
361
773
  self.logger.info("Checking staking compatibility")
362
- if service.chain_data.on_chain_state in (
363
- OnChainState.NOTMINTED,
364
- OnChainState.MINTED,
774
+
775
+ # TODO: Missing check when the service is currently staked in a program, but needs to be staked
776
+ # in a different target program. The In this case, balance = currently staked balance + safe balance
777
+
778
+ if on_chain_state in (
779
+ OnChainState.NON_EXISTENT,
780
+ OnChainState.PRE_REGISTRATION,
365
781
  ):
366
- required_olas = (
367
- user_params.olas_cost_of_bond + user_params.olas_required_to_stake
782
+ protocol_asset_requirements = self._compute_protocol_asset_requirements(
783
+ service_config_id, chain
784
+ )
785
+ elif on_chain_state == OnChainState.ACTIVE_REGISTRATION:
786
+ protocol_asset_requirements = self._compute_protocol_asset_requirements(
787
+ service_config_id, chain
788
+ )
789
+ protocol_asset_requirements[target_staking_params["staking_token"]] = (
790
+ target_staking_params["min_staking_deposit"]
791
+ * NUM_LOCAL_AGENT_INSTANCES
368
792
  )
369
- elif service.chain_data.on_chain_state == OnChainState.ACTIVATED:
370
- required_olas = user_params.olas_required_to_stake
371
793
  else:
372
- required_olas = 0
794
+ protocol_asset_requirements = {}
373
795
 
374
- balance = (
375
- registry_contracts.erc20.get_instance(
796
+ for asset, amount in protocol_asset_requirements.items():
797
+ balance = get_asset_balance(
376
798
  ledger_api=sftxb.ledger_api,
377
- contract_address=OLAS[service.ledger_config.chain],
799
+ asset_address=asset,
800
+ address=safe,
378
801
  )
379
- .functions.balanceOf(wallet.safe)
380
- .call()
802
+ if balance < amount:
803
+ raise ValueError(
804
+ f"Address {safe} has insufficient balance for asset {asset}: "
805
+ f"required {amount}, available {balance}."
806
+ )
807
+
808
+ # TODO Handle this in a more graceful way.
809
+ agent_id = (
810
+ target_staking_params["agent_ids"][0]
811
+ if target_staking_params["agent_ids"]
812
+ else user_params.agent_id
813
+ )
814
+ target_staking_params["agent_ids"] = [agent_id]
815
+
816
+ on_chain_metadata = self._get_on_chain_metadata(chain_config=chain_config)
817
+ on_chain_hash = on_chain_metadata.get("code_uri", "")[len(IPFS_URI_PREFIX) :]
818
+ on_chain_description = on_chain_metadata.get("description")
819
+ needs_update_agent_addresses = set(chain_data.instances) != set(
820
+ service.agent_addresses
821
+ )
822
+
823
+ current_agent_bond = sftxb.get_agent_bond(
824
+ service_id=chain_data.token, agent_id=target_staking_params["agent_ids"][0]
825
+ )
826
+
827
+ is_first_mint = (
828
+ self._get_on_chain_state(service=service, chain=chain)
829
+ == OnChainState.NON_EXISTENT
830
+ )
831
+ current_staking_program = self._get_current_staking_program(service, chain)
832
+
833
+ is_update = (
834
+ (not is_first_mint)
835
+ and (on_chain_hash is not None)
836
+ and (
837
+ # TODO Discuss how to manage on-chain hash updates with staking programs.
838
+ # on_chain_hash != service.hash or # noqa
839
+ current_agent_id != target_staking_params["agent_ids"][0]
840
+ # TODO This has to be removed for Optimus (needs to be properly implemented). Needs to be put back for Trader!
841
+ or (
842
+ user_params.use_staking
843
+ and current_agent_bond
844
+ != target_staking_params["min_staking_deposit"]
845
+ )
846
+ # TODO Missing complete this check for non-staked services it should compare the current_agent_bond from the protocol, now it's only read for the staking contract.
847
+ or current_staking_params["staking_token"]
848
+ != target_staking_params["staking_token"]
849
+ or on_chain_description != service.description
850
+ or needs_update_agent_addresses
381
851
  )
382
- if balance < required_olas:
383
- raise ValueError(
384
- "You don't have enough olas to stake, "
385
- f"address: {wallet.safe}; required olas: {required_olas}; your balance: {balance}"
852
+ )
853
+
854
+ self.logger.info(f"{chain_data.token=}")
855
+ self.logger.info(f"{user_params.use_staking=}")
856
+ self.logger.info(f"{current_staking_program=}")
857
+ self.logger.info(f"{user_params.staking_program_id=}")
858
+ self.logger.info(f"{on_chain_hash=}")
859
+ self.logger.info(f"{service.hash=}")
860
+ self.logger.info(f"{current_agent_id=}")
861
+ self.logger.info(f"{target_staking_params['agent_ids']=}")
862
+ self.logger.info(f"{current_agent_bond=}")
863
+ self.logger.info(f"{target_staking_params['min_staking_deposit']=}")
864
+ self.logger.info(f"{is_first_mint=}")
865
+ self.logger.info(f"{is_update=}")
866
+
867
+ if is_update:
868
+ self.terminate_service_on_chain_from_safe(
869
+ service_config_id=service_config_id, chain=chain
870
+ )
871
+ # Update service
872
+ if (
873
+ self._get_on_chain_state(service=service, chain=chain)
874
+ == OnChainState.PRE_REGISTRATION
875
+ ):
876
+ self.logger.info("Execute recovery module operations")
877
+ self._execute_recovery_module_flow_from_safe(
878
+ service_config_id=service_config_id, chain=chain
879
+ )
880
+
881
+ self.logger.info("Updating service")
882
+ receipt = (
883
+ sftxb.new_tx()
884
+ .add(
885
+ sftxb.get_mint_tx_data(
886
+ package_path=service.package_absolute_path,
887
+ agent_id=agent_id,
888
+ number_of_slots=NUM_LOCAL_AGENT_INSTANCES,
889
+ cost_of_bond=(
890
+ target_staking_params["min_staking_deposit"]
891
+ if user_params.use_staking
892
+ else user_params.cost_of_bond
893
+ ),
894
+ threshold=len(service.agent_addresses),
895
+ nft=IPFSHash(user_params.nft),
896
+ update_token=chain_data.token,
897
+ token=(
898
+ target_staking_params["staking_token"]
899
+ if user_params.use_staking
900
+ else None
901
+ ),
902
+ metadata_description=service.description,
903
+ skip_depencency_check=self.skip_depencency_check,
904
+ )
905
+ )
906
+ .settle()
907
+ )
908
+ event_data, *_ = t.cast(
909
+ t.Tuple,
910
+ registry_contracts.service_registry.process_receipt(
911
+ ledger_api=sftxb.ledger_api,
912
+ contract_address=target_staking_params["service_registry"],
913
+ event="UpdateService",
914
+ receipt=receipt,
915
+ ).get("events"),
386
916
  )
387
917
 
388
- if service.chain_data.on_chain_state == OnChainState.NOTMINTED:
918
+ # Mint service
919
+ if (
920
+ self._get_on_chain_state(service=service, chain=chain)
921
+ == OnChainState.NON_EXISTENT
922
+ ):
923
+ if user_params.use_staking and not sftxb.staking_slots_available(
924
+ staking_contract=get_staking_contract(
925
+ chain=ledger_config.chain,
926
+ staking_program_id=user_params.staking_program_id,
927
+ ),
928
+ ):
929
+ raise ValueError("No staking slots available")
930
+
389
931
  self.logger.info("Minting service")
390
932
  receipt = (
391
933
  sftxb.new_tx()
392
934
  .add(
393
935
  sftxb.get_mint_tx_data(
394
- package_path=service.service_path,
395
- agent_id=user_params.agent_id,
396
- number_of_slots=service.helper.config.number_of_agents,
936
+ package_path=service.package_absolute_path,
937
+ agent_id=agent_id,
938
+ number_of_slots=NUM_LOCAL_AGENT_INSTANCES,
397
939
  cost_of_bond=(
398
- user_params.olas_cost_of_bond
940
+ target_staking_params["min_staking_deposit"]
399
941
  if user_params.use_staking
400
942
  else user_params.cost_of_bond
401
943
  ),
402
- threshold=user_params.threshold,
944
+ threshold=len(service.agent_addresses),
403
945
  nft=IPFSHash(user_params.nft),
404
- update_token=service.chain_data.token if update else None,
946
+ update_token=None,
405
947
  token=(
406
- OLAS[service.ledger_config.chain]
948
+ target_staking_params["staking_token"]
407
949
  if user_params.use_staking
408
950
  else None
409
951
  ),
952
+ metadata_description=service.description,
953
+ skip_depencency_check=self.skip_depencency_check,
410
954
  )
411
955
  )
412
956
  .settle()
@@ -415,39 +959,42 @@ class ServiceManager:
415
959
  t.Tuple,
416
960
  registry_contracts.service_registry.process_receipt(
417
961
  ledger_api=sftxb.ledger_api,
418
- contract_address="0x9338b5153AE39BB89f50468E608eD9d764B755fD",
962
+ contract_address=target_staking_params["service_registry"],
419
963
  event="CreateService",
420
964
  receipt=receipt,
421
965
  ).get("events"),
422
966
  )
423
- service.chain_data.token = event_data["args"]["serviceId"]
424
- service.chain_data.on_chain_state = OnChainState.MINTED
967
+ chain_data.token = event_data["args"]["serviceId"]
425
968
  service.store()
426
969
 
427
- info = sftxb.info(token_id=service.chain_data.token)
428
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
970
+ if is_first_mint: # Hotfix to prevent RPC out-of-sync issues
971
+ time.sleep(RPC_SYNC_TIMEOUT)
429
972
 
430
- if service.chain_data.on_chain_state == OnChainState.MINTED:
973
+ # Activate service
974
+ if (
975
+ self._get_on_chain_state(service=service, chain=chain)
976
+ == OnChainState.PRE_REGISTRATION
977
+ ):
431
978
  cost_of_bond = user_params.cost_of_bond
432
979
  if user_params.use_staking:
433
- token_utility = "0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8" # nosec
434
- olas_token = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f" # nosec
980
+ token_utility = target_staking_params["service_registry_token_utility"]
981
+ olas_token = target_staking_params["staking_token"]
435
982
  self.logger.info(
436
- f"Approving OLAS as bonding token from {wallet.safe} to {token_utility}"
983
+ f"Approving OLAS as bonding token from {safe} to {token_utility}"
437
984
  )
438
985
  cost_of_bond = (
439
986
  registry_contracts.service_registry_token_utility.get_agent_bond(
440
987
  ledger_api=sftxb.ledger_api,
441
988
  contract_address=token_utility,
442
- service_id=service.chain_data.token,
443
- agent_id=user_params.agent_id,
989
+ service_id=chain_data.token,
990
+ agent_id=agent_id,
444
991
  ).get("bond")
445
992
  )
446
993
  sftxb.new_tx().add(
447
- sftxb.get_olas_approval_data(
994
+ sftxb.get_erc20_approval_data(
448
995
  spender=token_utility,
449
996
  amount=cost_of_bond,
450
- olas_contract=olas_token,
997
+ erc20_contract=olas_token,
451
998
  )
452
999
  ).settle()
453
1000
  token_utility_allowance = (
@@ -456,50 +1003,66 @@ class ServiceManager:
456
1003
  contract_address=olas_token,
457
1004
  )
458
1005
  .functions.allowance(
459
- wallet.safe,
1006
+ safe,
460
1007
  token_utility,
461
1008
  )
462
1009
  .call()
463
1010
  )
464
1011
  self.logger.info(
465
- f"Approved {token_utility_allowance} OLAS from {wallet.safe} to {token_utility}"
1012
+ f"Approved {token_utility_allowance} OLAS from {safe} to {token_utility}"
466
1013
  )
467
- cost_of_bond = 1
1014
+ cost_of_bond = MIN_AGENT_BOND
468
1015
 
469
1016
  self.logger.info("Activating service")
1017
+
1018
+ native_balance = get_asset_balance(
1019
+ ledger_api=sftxb.ledger_api,
1020
+ asset_address=ZERO_ADDRESS,
1021
+ address=safe,
1022
+ )
1023
+
1024
+ if (
1025
+ native_balance < cost_of_bond
1026
+ ): # TODO check that this is the security deposit
1027
+ message = f"Cannot activate service: address {safe} {native_balance=} < {cost_of_bond=}."
1028
+ self.logger.error(message)
1029
+ raise ValueError(message)
1030
+
470
1031
  sftxb.new_tx().add(
471
1032
  sftxb.get_activate_data(
472
- service_id=service.chain_data.token,
1033
+ service_id=chain_data.token,
473
1034
  cost_of_bond=cost_of_bond,
474
1035
  )
475
1036
  ).settle()
476
- service.chain_data.on_chain_state = OnChainState.ACTIVATED
477
- service.store()
478
1037
 
479
- info = sftxb.info(token_id=service.chain_data.token)
480
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
1038
+ if is_first_mint: # Hotfix to prevent RPC out-of-sync issues
1039
+ time.sleep(RPC_SYNC_TIMEOUT)
481
1040
 
482
- if service.chain_data.on_chain_state == OnChainState.ACTIVATED:
1041
+ # Register agent instances
1042
+ if (
1043
+ self._get_on_chain_state(service=service, chain=chain)
1044
+ == OnChainState.ACTIVE_REGISTRATION
1045
+ ):
483
1046
  cost_of_bond = user_params.cost_of_bond
484
1047
  if user_params.use_staking:
485
- token_utility = "0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8" # nosec
486
- olas_token = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f" # nosec
1048
+ token_utility = target_staking_params["service_registry_token_utility"]
1049
+ olas_token = target_staking_params["staking_token"]
487
1050
  self.logger.info(
488
- f"Approving OLAS as bonding token from {wallet.safe} to {token_utility}"
1051
+ f"Approving OLAS as bonding token from {safe} to {token_utility}"
489
1052
  )
490
1053
  cost_of_bond = (
491
1054
  registry_contracts.service_registry_token_utility.get_agent_bond(
492
1055
  ledger_api=sftxb.ledger_api,
493
1056
  contract_address=token_utility,
494
- service_id=service.chain_data.token,
495
- agent_id=user_params.agent_id,
1057
+ service_id=chain_data.token,
1058
+ agent_id=agent_id,
496
1059
  ).get("bond")
497
1060
  )
498
1061
  sftxb.new_tx().add(
499
- sftxb.get_olas_approval_data(
1062
+ sftxb.get_erc20_approval_data(
500
1063
  spender=token_utility,
501
1064
  amount=cost_of_bond,
502
- olas_contract=olas_token,
1065
+ erc20_contract=olas_token,
503
1066
  )
504
1067
  ).settle()
505
1068
  token_utility_allowance = (
@@ -508,534 +1071,1707 @@ class ServiceManager:
508
1071
  contract_address=olas_token,
509
1072
  )
510
1073
  .functions.allowance(
511
- wallet.safe,
1074
+ safe,
512
1075
  token_utility,
513
1076
  )
514
1077
  .call()
515
1078
  )
516
1079
  self.logger.info(
517
- f"Approved {token_utility_allowance} OLAS from {wallet.safe} to {token_utility}"
1080
+ f"Approved {token_utility_allowance} OLAS from {safe} to {token_utility}"
518
1081
  )
519
- cost_of_bond = 1
1082
+ cost_of_bond = MIN_AGENT_BOND
520
1083
 
521
1084
  self.logger.info(
522
- f"Registering service: {service.chain_data.token} -> {instances}"
1085
+ f"Registering agent instances: {chain_data.token} -> {service.agent_addresses}"
523
1086
  )
1087
+
1088
+ native_balance = get_asset_balance(
1089
+ ledger_api=sftxb.ledger_api,
1090
+ asset_address=ZERO_ADDRESS,
1091
+ address=safe,
1092
+ )
1093
+
1094
+ if native_balance < cost_of_bond * len(service.agent_addresses):
1095
+ message = f"Cannot register agent instances: address {safe} {native_balance=} < {cost_of_bond=}."
1096
+ self.logger.error(message)
1097
+ raise ValueError(message)
1098
+
524
1099
  sftxb.new_tx().add(
525
1100
  sftxb.get_register_instances_data(
526
- service_id=service.chain_data.token,
527
- instances=instances,
528
- agents=[user_params.agent_id for _ in instances],
1101
+ service_id=chain_data.token,
1102
+ instances=service.agent_addresses,
1103
+ agents=[agent_id for _ in service.agent_addresses],
529
1104
  cost_of_bond=cost_of_bond,
530
1105
  )
531
1106
  ).settle()
532
- service.chain_data.on_chain_state = OnChainState.REGISTERED
533
- service.keys = keys
534
- service.store()
535
1107
 
536
- info = sftxb.info(token_id=service.chain_data.token)
537
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
1108
+ if is_first_mint: # Hotfix to prevent RPC out-of-sync issues
1109
+ time.sleep(RPC_SYNC_TIMEOUT)
538
1110
 
539
- if service.chain_data.on_chain_state == OnChainState.REGISTERED:
1111
+ # Deploy service
1112
+ is_initial_funding = False
1113
+ if (
1114
+ self._get_on_chain_state(service=service, chain=chain)
1115
+ == OnChainState.FINISHED_REGISTRATION
1116
+ ):
540
1117
  self.logger.info("Deploying service")
541
- sftxb.new_tx().add(
542
- sftxb.get_deploy_data(
543
- service_id=service.chain_data.token,
544
- reuse_multisig=update,
1118
+
1119
+ info = sftxb.info(token_id=chain_data.token)
1120
+ service_safe_address = info["multisig"]
1121
+ if service_safe_address == ZERO_ADDRESS:
1122
+ reuse_multisig = False
1123
+ is_initial_funding = True
1124
+ is_recovery_module_enabled = True
1125
+ else:
1126
+ reuse_multisig = True
1127
+ is_recovery_module_enabled = (
1128
+ registry_contracts.gnosis_safe.is_module_enabled(
1129
+ ledger_api=sftxb.ledger_api,
1130
+ contract_address=service_safe_address,
1131
+ module_address=CONTRACTS[Chain(chain)]["recovery_module"],
1132
+ ).get("enabled")
545
1133
  )
546
- ).settle()
547
- service.chain_data.on_chain_state = OnChainState.DEPLOYED
548
- service.store()
549
1134
 
550
- info = sftxb.info(token_id=service.chain_data.token)
551
- service.keys = keys
552
- service.chain_data = OnChainData(
553
- token=service.chain_data.token,
554
- instances=info["instances"],
555
- multisig=info["multisig"],
556
- staked=False,
557
- on_chain_state=service.chain_data.on_chain_state,
558
- user_params=service.chain_data.user_params,
1135
+ self.logger.info(f"{reuse_multisig=}")
1136
+ self.logger.info(f"{is_recovery_module_enabled=}")
1137
+
1138
+ messages = sftxb.get_deploy_data_from_safe(
1139
+ service_id=chain_data.token,
1140
+ reuse_multisig=reuse_multisig,
1141
+ master_safe=safe,
1142
+ use_recovery_module=is_recovery_module_enabled,
1143
+ )
1144
+ tx = sftxb.new_tx()
1145
+ for message in messages:
1146
+ tx.add(message)
1147
+ tx.settle()
1148
+
1149
+ # Update local Service
1150
+ info = sftxb.info(token_id=chain_data.token)
1151
+ chain_data.instances = info["instances"]
1152
+ chain_data.multisig = info["multisig"]
1153
+
1154
+ if is_initial_funding:
1155
+ self.funding_manager.fund_service_initial(service)
1156
+
1157
+ # TODO: yet another agent specific logic for mech, which should be abstracted
1158
+ if all(
1159
+ var in service.env_variables
1160
+ for var in [
1161
+ "AGENT_ID",
1162
+ "MECH_TO_CONFIG",
1163
+ "ON_CHAIN_SERVICE_ID",
1164
+ "ETHEREUM_LEDGER_RPC_0",
1165
+ "GNOSIS_LEDGER_RPC_0",
1166
+ "MECH_MARKETPLACE_ADDRESS",
1167
+ ]
1168
+ ):
1169
+ if (
1170
+ not service.env_variables["AGENT_ID"]["value"]
1171
+ or not service.env_variables["MECH_TO_CONFIG"]["value"]
1172
+ ):
1173
+ mech_address, agent_id = deploy_mech(sftxb=sftxb, service=service)
1174
+ service.update_env_variables_values(
1175
+ {
1176
+ "AGENT_ID": agent_id,
1177
+ "MECH_TO_CONFIG": json.dumps(
1178
+ {
1179
+ mech_address: {
1180
+ "use_dynamic_pricing": False,
1181
+ "is_marketplace_mech": True,
1182
+ }
1183
+ },
1184
+ separators=(",", ":"),
1185
+ ),
1186
+ "MECH_TO_MAX_DELIVERY_RATE": json.dumps(
1187
+ {
1188
+ mech_address: service.env_variables.get(
1189
+ "MECH_REQUEST_PRICE", {}
1190
+ ).get("value", 10000000000000000)
1191
+ },
1192
+ separators=(",", ":"),
1193
+ ),
1194
+ }
1195
+ )
1196
+
1197
+ service.update_env_variables_values(
1198
+ {
1199
+ "ON_CHAIN_SERVICE_ID": chain_data.token,
1200
+ "ETHEREUM_LEDGER_RPC_0": service.env_variables["GNOSIS_LEDGER_RPC"][
1201
+ "value"
1202
+ ],
1203
+ "GNOSIS_LEDGER_RPC_0": service.env_variables["GNOSIS_LEDGER_RPC"][
1204
+ "value"
1205
+ ],
1206
+ }
1207
+ )
1208
+
1209
+ # TODO: this is a patch for modius, to be standardized
1210
+ staking_chain = None
1211
+ for chain_, config in service.chain_configs.items():
1212
+ if config.chain_data.user_params.use_staking:
1213
+ staking_chain = chain_
1214
+ break
1215
+
1216
+ service.update_env_variables_values(
1217
+ {
1218
+ "SAFE_CONTRACT_ADDRESSES": json.dumps(
1219
+ {
1220
+ chain: config.chain_data.multisig
1221
+ for chain, config in service.chain_configs.items()
1222
+ },
1223
+ separators=(",", ":"),
1224
+ ),
1225
+ "STAKING_CHAIN": staking_chain,
1226
+ }
559
1227
  )
560
1228
  service.store()
561
1229
 
562
- def terminate_service_on_chain(self, hash: str) -> None:
563
- """
564
- Terminate service on-chain
1230
+ if user_params.use_staking:
1231
+ self.stake_service_on_chain_from_safe(
1232
+ service_config_id=service_config_id, chain=chain
1233
+ )
565
1234
 
566
- :param hash: Service hash
567
- """
568
- service = self.create_or_load(hash=hash)
569
- ocm = self.get_on_chain_manager(service=service)
570
- info = ocm.info(token_id=service.chain_data.token)
571
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
1235
+ def terminate_service_on_chain(
1236
+ self, service_config_id: str, chain: t.Optional[str] = None
1237
+ ) -> None:
1238
+ """Terminate service on-chain"""
1239
+ # TODO This method has not been thoroughly reviewed. Deprecated usage in favour of Safe version.
1240
+
1241
+ self.logger.info("terminate_service_on_chain")
1242
+ service = self.load(service_config_id=service_config_id)
572
1243
 
573
- if service.chain_data.on_chain_state != OnChainState.DEPLOYED:
1244
+ chain_config = service.chain_configs[chain or service.home_chain]
1245
+ ledger_config = chain_config.ledger_config
1246
+ chain_data = chain_config.chain_data
1247
+ ocm = self.get_on_chain_manager(ledger_config=ledger_config)
1248
+ info = ocm.info(token_id=chain_data.token)
1249
+
1250
+ if OnChainState(info["service_state"]) != OnChainState.DEPLOYED:
574
1251
  self.logger.info("Cannot terminate service")
575
1252
  return
576
1253
 
577
1254
  self.logger.info("Terminating service")
578
1255
  ocm.terminate(
579
- service_id=service.chain_data.token,
1256
+ service_id=chain_data.token,
580
1257
  token=(
581
- OLAS[service.ledger_config.chain]
582
- if service.chain_data.user_params.use_staking
1258
+ OLAS[ledger_config.chain]
1259
+ if chain_data.user_params.use_staking
583
1260
  else None
584
1261
  ),
585
1262
  )
586
- service.chain_data.on_chain_state = OnChainState.TERMINATED
587
- service.store()
588
-
589
- def terminate_service_on_chain_from_safe(self, hash: str) -> None:
590
- """
591
- Terminate service on-chain
592
1263
 
593
- :param hash: Service hash
594
- """
595
- service = self.create_or_load(hash=hash)
596
- sftxb = self.get_eth_safe_tx_builder(service=service)
597
- info = sftxb.info(token_id=service.chain_data.token)
598
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
599
-
600
- if service.chain_data.on_chain_state != OnChainState.DEPLOYED:
601
- self.logger.info("Cannot terminate service")
602
- return
603
-
604
- self.logger.info("Terminating service")
605
- sftxb.new_tx().add(
606
- sftxb.get_terminate_data(
607
- service_id=service.chain_data.token,
608
- )
609
- ).settle()
610
- service.chain_data.on_chain_state = OnChainState.TERMINATED
611
- service.store()
1264
+ def terminate_service_on_chain_from_safe( # pylint: disable=too-many-locals
1265
+ self,
1266
+ service_config_id: str,
1267
+ chain: str,
1268
+ ) -> None:
1269
+ """Terminate service on-chain"""
612
1270
 
613
- def unbond_service_on_chain(self, hash: str) -> None:
614
- """
615
- Unbond service on-chain
1271
+ self.logger.info("terminate_service_on_chain_from_safe")
1272
+ service = self.load(service_config_id=service_config_id)
1273
+ chain_config = service.chain_configs[chain]
1274
+ ledger_config = chain_config.ledger_config
1275
+ chain_data = chain_config.chain_data
1276
+ wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
1277
+ master_safe = wallet.safes[Chain(chain)] # type: ignore
616
1278
 
617
- :param hash: Service hash
618
- """
619
- service = self.create_or_load(hash=hash)
620
- ocm = self.get_on_chain_manager(service=service)
621
- info = ocm.info(token_id=service.chain_data.token)
622
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
1279
+ # TODO fixme
1280
+ os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc
623
1281
 
624
- if service.chain_data.on_chain_state != OnChainState.TERMINATED:
625
- self.logger.info("Cannot unbond service")
626
- return
1282
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
627
1283
 
628
- self.logger.info("Unbonding service")
629
- ocm.unbond(
630
- service_id=service.chain_data.token,
631
- token=(
632
- OLAS[service.ledger_config.chain]
633
- if service.chain_data.user_params.use_staking
634
- else None
635
- ),
1284
+ # Determine if the service is staked in a known staking program
1285
+ current_staking_program = self._get_current_staking_program(
1286
+ service,
1287
+ chain,
636
1288
  )
637
- service.chain_data.on_chain_state = OnChainState.UNBONDED
638
- service.store()
639
-
640
- def unbond_service_on_chain_from_safe(self, hash: str) -> None:
641
- """
642
- Terminate service on-chain
643
-
644
- :param hash: Service hash
645
- """
646
- service = self.create_or_load(hash=hash)
647
- sftxb = self.get_eth_safe_tx_builder(service=service)
648
- info = sftxb.info(token_id=service.chain_data.token)
649
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
1289
+ is_staked = current_staking_program is not None
1290
+
1291
+ can_unstake = False
1292
+ if current_staking_program is not None:
1293
+ can_unstake = sftxb.can_unstake(
1294
+ service_id=chain_data.token,
1295
+ staking_contract=get_staking_contract(
1296
+ chain=ledger_config.chain,
1297
+ staking_program_id=current_staking_program,
1298
+ ),
1299
+ )
650
1300
 
651
- if service.chain_data.on_chain_state != OnChainState.TERMINATED:
652
- self.logger.info("Cannot unbond service")
1301
+ # Cannot unstake, terminate flow.
1302
+ if is_staked and not can_unstake:
1303
+ self.logger.info("Service cannot be terminated on-chain: cannot unstake.")
653
1304
  return
654
-
655
- self.logger.info("Unbonding service")
656
- sftxb.new_tx().add(
657
- sftxb.get_unbond_data(
658
- service_id=service.chain_data.token,
1305
+ # Unstake the service if applies
1306
+ if is_staked and can_unstake:
1307
+ self.unstake_service_on_chain_from_safe(
1308
+ service_config_id=service_config_id,
1309
+ chain=chain,
1310
+ staking_program_id=current_staking_program,
659
1311
  )
660
- ).settle()
661
- service.chain_data.on_chain_state = OnChainState.TERMINATED
662
- service.store()
1312
+ # At least claim the rewards if we cannot unstake yet
1313
+ elif is_staked:
1314
+ self.claim_on_chain_from_safe(
1315
+ service_config_id=service_config_id,
1316
+ chain=chain,
1317
+ )
1318
+ return
663
1319
 
664
- def stake_service_on_chain(self, hash: str) -> None:
665
- """
666
- Stake service on-chain
1320
+ if self._get_on_chain_state(service=service, chain=chain) in (
1321
+ OnChainState.ACTIVE_REGISTRATION,
1322
+ OnChainState.FINISHED_REGISTRATION,
1323
+ OnChainState.DEPLOYED,
1324
+ ):
1325
+ self.logger.info("Terminating service")
1326
+ sftxb.new_tx().add(
1327
+ sftxb.get_terminate_data(
1328
+ service_id=chain_data.token,
1329
+ )
1330
+ ).settle()
667
1331
 
668
- :param hash: Service hash
669
- """
670
- service = self.create_or_load(hash=hash)
671
- if not service.chain_data.user_params.use_staking:
672
- self.logger.info("Cannot stake service, `use_staking` is set to false")
1332
+ if (
1333
+ self._get_on_chain_state(service=service, chain=chain)
1334
+ == OnChainState.TERMINATED_BONDED
1335
+ ):
1336
+ self.logger.info("Unbonding service")
1337
+ sftxb.new_tx().add(
1338
+ sftxb.get_unbond_data(
1339
+ service_id=chain_data.token,
1340
+ )
1341
+ ).settle()
1342
+
1343
+ # Swap service safe
1344
+ current_safe_owners = sftxb.get_service_safe_owners(service_id=chain_data.token)
1345
+ counter_current_safe_owners = Counter(s.lower() for s in current_safe_owners)
1346
+ counter_instances = Counter(s.lower() for s in service.agent_addresses)
1347
+
1348
+ if counter_current_safe_owners == counter_instances:
1349
+ requirements = ChainAmounts(
1350
+ {
1351
+ chain: {
1352
+ current_safe_owners[0]: {
1353
+ ZERO_ADDRESS: chain_data.user_params.fund_requirements[
1354
+ ZERO_ADDRESS
1355
+ ].agent
1356
+ }
1357
+ }
1358
+ }
1359
+ )
1360
+ balances = ChainAmounts(
1361
+ {
1362
+ chain: {
1363
+ current_safe_owners[0]: {
1364
+ ZERO_ADDRESS: get_asset_balance(
1365
+ ledger_api=sftxb.ledger_api,
1366
+ asset_address=ZERO_ADDRESS,
1367
+ address=service.agent_addresses[0],
1368
+ )
1369
+ }
1370
+ }
1371
+ }
1372
+ )
1373
+ if balances < requirements * DEFAULT_EOA_THRESHOLD:
1374
+ self.logger.info("[SERVICE MANAGER] Funding agent EOA for Safe swap.")
1375
+ shortfalls = ChainAmounts.shortfalls(
1376
+ requirements=requirements, balances=balances
1377
+ )
1378
+ try:
1379
+ self.funding_manager.fund_chain_amounts(shortfalls)
1380
+ except InsufficientFundsException as e:
1381
+ recovery_module_address = CONTRACTS[Chain(chain)]["recovery_module"]
1382
+ is_recovery_module_enabled = (
1383
+ registry_contracts.gnosis_safe.is_module_enabled(
1384
+ ledger_api=sftxb.ledger_api,
1385
+ contract_address=chain_data.multisig,
1386
+ module_address=recovery_module_address,
1387
+ ).get("enabled")
1388
+ )
1389
+ if is_recovery_module_enabled:
1390
+ self.logger.info(
1391
+ "[SERVICE MANAGER] Could not fund Agent EOA for service swap, but recovery module is enabled."
1392
+ )
1393
+ return
1394
+ raise e
1395
+
1396
+ self._enable_recovery_module(
1397
+ service_config_id=service_config_id, chain=chain
1398
+ )
1399
+ self.logger.info("[SERVICE MANAGER] Swapping Safe owners")
1400
+ owner_crypto = self.keys_manager.get_crypto_instance(
1401
+ address=current_safe_owners[0]
1402
+ )
1403
+ sftxb.swap(
1404
+ service_id=chain_data.token,
1405
+ multisig=chain_data.multisig, # TODO this can be read from the registry
1406
+ owner_cryptos=[owner_crypto], # TODO allow multiple owners
1407
+ new_owner_address=(
1408
+ master_safe if master_safe else wallet.crypto.address
1409
+ ), # TODO it should always be safe address
1410
+ )
1411
+
1412
+ def _execute_recovery_module_flow_from_safe( # pylint: disable=too-many-locals
1413
+ self,
1414
+ service_config_id: str,
1415
+ chain: str,
1416
+ ) -> None:
1417
+ """Execute recovery module operations from Safe"""
1418
+ self.logger.info(f"_execute_recovery_module_operations_from_safe {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.")
673
1429
  return
674
1430
 
675
- ocm = self.get_on_chain_manager(service=service)
676
- info = ocm.info(token_id=service.chain_data.token)
677
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
1431
+ info = sftxb.info(token_id=chain_data.token)
1432
+ service_safe_address = info["multisig"]
1433
+ on_chain_state = OnChainState(info["service_state"])
678
1434
 
679
- if service.chain_data.on_chain_state != OnChainState.DEPLOYED:
680
- self.logger.info("Cannot stake service, it's not in deployed state")
1435
+ if service_safe_address == ZERO_ADDRESS:
1436
+ self.logger.info("Service Safe is not deployed.")
681
1437
  return
682
1438
 
683
- state = ocm.staking_status(
684
- service_id=service.chain_data.token,
685
- staking_contract=STAKING[service.ledger_config.chain],
686
- )
687
- self.logger.info(f"Checking staking status for: {service.chain_data.token}")
688
- if state == StakingState.STAKED:
689
- self.logger.info(f"{service.chain_data.token} is already staked")
690
- service.chain_data.staked = True
691
- service.store()
1439
+ recovery_module_address = CONTRACTS[Chain(chain)]["recovery_module"]
1440
+ is_recovery_module_enabled = registry_contracts.gnosis_safe.is_module_enabled(
1441
+ ledger_api=sftxb.ledger_api,
1442
+ contract_address=service_safe_address,
1443
+ module_address=recovery_module_address,
1444
+ ).get("enabled")
1445
+
1446
+ service_safe_owners = sftxb.get_service_safe_owners(service_id=chain_data.token)
1447
+ master_safe_is_service_safe_owner = service_safe_owners == [safe]
1448
+
1449
+ self.logger.info(f"{is_recovery_module_enabled=}")
1450
+ self.logger.info(f"{master_safe_is_service_safe_owner=}")
1451
+
1452
+ if not is_recovery_module_enabled and not master_safe_is_service_safe_owner:
1453
+ self.logger.info(
1454
+ "Recovery module is not enabled and Master Safe is not service Safe owner. Skipping recovery operations."
1455
+ )
692
1456
  return
693
1457
 
694
- if state == StakingState.EVICTED:
695
- self.logger.info(f"{service.chain_data.token} has been evicted")
696
- service.chain_data.staked = True
697
- service.store()
698
- self.unstake_service_on_chain(hash=hash)
1458
+ if not is_recovery_module_enabled:
1459
+ self._enable_recovery_module(
1460
+ service_config_id=service_config_id, chain=chain
1461
+ )
1462
+
1463
+ if (
1464
+ not master_safe_is_service_safe_owner
1465
+ and on_chain_state == OnChainState.PRE_REGISTRATION
1466
+ ):
1467
+ self.logger.info("Recovering service Safe access through recovery module.")
1468
+ sftxb.new_tx().add(
1469
+ sftxb.get_recover_access_data(
1470
+ service_id=chain_data.token,
1471
+ )
1472
+ ).settle()
1473
+ self.logger.info("Recovering service Safe done.")
699
1474
 
700
- self.logger.info(f"Staking service: {service.chain_data.token}")
701
- ocm.stake(
702
- service_id=service.chain_data.token,
703
- service_registry=CONTRACTS[service.ledger_config.chain]["service_registry"],
704
- staking_contract=STAKING[service.ledger_config.chain],
1475
+ def _enable_recovery_module( # pylint: disable=too-many-locals
1476
+ self,
1477
+ service_config_id: str,
1478
+ chain: str,
1479
+ ) -> None:
1480
+ """Enable recovery module"""
1481
+ self.logger.info(f"_enable_recovery_module {chain=}")
1482
+ service = self.load(service_config_id=service_config_id)
1483
+ chain_config = service.chain_configs[chain]
1484
+ chain_data = chain_config.chain_data
1485
+ ledger_config = chain_config.ledger_config
1486
+ wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
1487
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
1488
+ safe = wallet.safes[Chain(chain)]
1489
+
1490
+ if chain_data.token == NON_EXISTENT_TOKEN:
1491
+ self.logger.info("Service is not minted.")
1492
+ return
1493
+
1494
+ info = sftxb.info(token_id=chain_data.token)
1495
+ service_safe_address = info["multisig"]
1496
+
1497
+ if service_safe_address == ZERO_ADDRESS:
1498
+ self.logger.info("Service Safe is not deployed.")
1499
+ return
1500
+
1501
+ recovery_module_address = CONTRACTS[Chain(chain)]["recovery_module"]
1502
+ is_recovery_module_enabled = registry_contracts.gnosis_safe.is_module_enabled(
1503
+ ledger_api=sftxb.ledger_api,
1504
+ contract_address=service_safe_address,
1505
+ module_address=recovery_module_address,
1506
+ ).get("enabled")
1507
+
1508
+ if is_recovery_module_enabled:
1509
+ self.logger.info("Recovery module is already enabled in service Safe.")
1510
+ return
1511
+
1512
+ self.logger.info("Recovery module is not enabled.")
1513
+
1514
+ # NOTE Recovery from agent only works for single-agent services
1515
+ agent_address = service.agent_addresses[0]
1516
+ service_safe_owners = sftxb.get_service_safe_owners(service_id=chain_data.token)
1517
+ agent_is_service_safe_owner = service_safe_owners == [agent_address]
1518
+ master_safe_is_service_safe_owner = service_safe_owners == [safe]
1519
+
1520
+ if agent_is_service_safe_owner:
1521
+ self.logger.info("(Agent) Enabling recovery module in service Safe.")
1522
+ try:
1523
+ crypto = self.keys_manager.get_crypto_instance(address=agent_address)
1524
+ EthSafeTxBuilder._new_tx( # pylint: disable=protected-access
1525
+ ledger_api=sftxb.ledger_api,
1526
+ crypto=crypto,
1527
+ chain_type=ChainType(chain),
1528
+ safe=service_safe_address,
1529
+ ).add(
1530
+ sftxb.get_enable_module_data(
1531
+ module_address=recovery_module_address,
1532
+ safe_address=service_safe_address,
1533
+ )
1534
+ ).settle()
1535
+ self.logger.info(
1536
+ "(Agent) Recovery module enabled successfully in service Safe."
1537
+ )
1538
+ except Exception as e: # pylint: disable=broad-except
1539
+ self.logger.error(
1540
+ f"Failed to enable recovery module in service Safe. Exception {e}: {traceback.format_exc()}"
1541
+ )
1542
+ elif master_safe_is_service_safe_owner:
1543
+ # TODO Enable recovery module when Safe owner = master Safe.
1544
+ # This should be similar to the above code, but
1545
+ # requires implement a transaction where the owner is another Safe.
1546
+ self.logger.info(
1547
+ "(Service owner) Enabling recovery module in service Safe. [Not implemented]"
1548
+ )
1549
+ else:
1550
+ self.logger.error(
1551
+ f"Cannot enable recovery module. Safe {service_safe_address} has inconsistent owners."
1552
+ )
1553
+
1554
+ def _get_current_staking_program( # pylint: disable=no-self-use
1555
+ self, service: Service, chain: str
1556
+ ) -> t.Optional[str]:
1557
+ staking_manager = StakingManager(Chain(chain))
1558
+ return staking_manager.get_current_staking_program(
1559
+ service_id=service.chain_configs[chain].chain_data.token
705
1560
  )
706
- service.chain_data.staked = True
707
- service.store()
708
1561
 
709
- def stake_service_on_chain_from_safe(self, hash: str) -> None:
1562
+ def unbond_service_on_chain(
1563
+ self, service_config_id: str, chain: t.Optional[str] = None
1564
+ ) -> None:
1565
+ """Unbond service on-chain"""
1566
+ # TODO This method has not been thoroughly reviewed. Deprecated usage in favour of Safe version.
1567
+
1568
+ service = self.load(service_config_id=service_config_id)
1569
+
1570
+ chain_config = service.chain_configs[chain or service.home_chain]
1571
+ ledger_config = chain_config.ledger_config
1572
+ chain_data = chain_config.chain_data
1573
+ ocm = self.get_on_chain_manager(ledger_config=ledger_config)
1574
+ info = ocm.info(token_id=chain_data.token)
1575
+
1576
+ if OnChainState(info["service_state"]) != OnChainState.TERMINATED_BONDED:
1577
+ self.logger.info("Cannot unbond service")
1578
+ return
1579
+
1580
+ self.logger.info("Unbonding service")
1581
+ ocm.unbond(
1582
+ service_id=chain_data.token,
1583
+ token=(
1584
+ OLAS[ledger_config.chain]
1585
+ if chain_data.user_params.use_staking
1586
+ else None
1587
+ ),
1588
+ )
1589
+
1590
+ def stake_service_on_chain(self, hash: str) -> None:
710
1591
  """
711
1592
  Stake service on-chain
712
1593
 
713
1594
  :param hash: Service hash
714
1595
  """
715
- service = self.create_or_load(hash=hash)
716
- if not service.chain_data.user_params.use_staking:
717
- self.logger.info("Cannot stake service, `use_staking` is set to false")
718
- return
1596
+ raise NotImplementedError
719
1597
 
720
- sftxb = self.get_eth_safe_tx_builder(service=service)
721
- info = sftxb.info(token_id=service.chain_data.token)
722
- service.chain_data.on_chain_state = OnChainState(info["service_state"])
1598
+ def stake_service_on_chain_from_safe( # pylint: disable=too-many-statements,too-many-locals
1599
+ self, service_config_id: str, chain: str
1600
+ ) -> None:
1601
+ """Stake service on-chain"""
1602
+ self.logger.info("stake_service_on_chain_from_safe")
1603
+ service = self.load(service_config_id=service_config_id)
1604
+ chain_config = service.chain_configs[chain]
1605
+ ledger_config = chain_config.ledger_config
1606
+ chain_data = chain_config.chain_data
1607
+ user_params = chain_data.user_params
1608
+ target_staking_program = user_params.staking_program_id
1609
+ target_staking_contract = get_staking_contract(
1610
+ chain=ledger_config.chain,
1611
+ staking_program_id=target_staking_program,
1612
+ )
1613
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
723
1614
 
724
- if service.chain_data.on_chain_state != OnChainState.DEPLOYED:
725
- self.logger.info("Cannot stake service, it's not in deployed state")
726
- return
1615
+ # TODO fixme
1616
+ os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc
727
1617
 
728
- state = sftxb.staking_status(
729
- service_id=service.chain_data.token,
730
- staking_contract=STAKING[service.ledger_config.chain],
731
- )
732
- self.logger.info(f"Checking staking status for: {service.chain_data.token}")
733
- if state == StakingState.STAKED:
734
- self.logger.info(f"{service.chain_data.token} is already staked")
735
- service.chain_data.staked = True
736
- service.store()
1618
+ on_chain_state = self._get_on_chain_state(service=service, chain=chain)
1619
+ if on_chain_state != OnChainState.DEPLOYED:
1620
+ self.logger.info(
1621
+ f"Cannot perform staking operations. Service {chain_config.chain_data.token} is not on DEPLOYED state"
1622
+ )
737
1623
  return
738
1624
 
739
- if state == StakingState.EVICTED:
740
- self.logger.info(f"{service.chain_data.token} has been evicted")
741
- service.chain_data.staked = True
742
- service.store()
743
- self.unstake_service_on_chain_from_safe(hash=hash)
1625
+ # Determine if the service is staked in a known staking program
1626
+ current_staking_program = self._get_current_staking_program(
1627
+ service,
1628
+ chain,
1629
+ )
1630
+ current_staking_contract = get_staking_contract(
1631
+ chain=ledger_config.chain,
1632
+ staking_program_id=current_staking_program,
1633
+ )
744
1634
 
745
- self.logger.info(f"Approving staking: {service.chain_data.token}")
746
- sftxb.new_tx().add(
747
- sftxb.get_staking_approval_data(
748
- service_id=service.chain_data.token,
749
- service_registry=CONTRACTS[service.ledger_config.chain][
750
- "service_registry"
751
- ],
752
- staking_contract=STAKING[service.ledger_config.chain],
1635
+ # perform the unstaking flow if necessary
1636
+ staking_state = StakingState.UNSTAKED
1637
+ if current_staking_program is not None:
1638
+ can_unstake = sftxb.can_unstake(
1639
+ chain_config.chain_data.token, current_staking_contract
753
1640
  )
754
- ).settle()
1641
+ if not chain_config.chain_data.user_params.use_staking and can_unstake:
1642
+ self.logger.info(
1643
+ f"Use staking is set to false, but service {chain_config.chain_data.token} is staked and can be unstaked. Unstaking..."
1644
+ )
1645
+ self.unstake_service_on_chain_from_safe(
1646
+ service_config_id=service_config_id,
1647
+ chain=chain,
1648
+ staking_program_id=current_staking_program,
1649
+ )
755
1650
 
756
- self.logger.info(f"Staking service: {service.chain_data.token}")
757
- sftxb.new_tx().add(
758
- sftxb.get_staking_data(
759
- service_id=service.chain_data.token,
760
- staking_contract=STAKING[service.ledger_config.chain],
1651
+ staking_state = sftxb.staking_status(
1652
+ service_id=chain_data.token,
1653
+ staking_contract=current_staking_contract,
761
1654
  )
762
- ).settle()
763
- service.chain_data.staked = True
764
- service.store()
765
1655
 
766
- def unstake_service_on_chain(self, hash: str) -> None:
767
- """
768
- Unbond service on-chain
1656
+ if staking_state == StakingState.EVICTED and can_unstake:
1657
+ self.logger.info(
1658
+ f"Service {chain_config.chain_data.token} has been evicted and can be unstaked. Unstaking..."
1659
+ )
1660
+ self.unstake_service_on_chain_from_safe(
1661
+ service_config_id=service_config_id,
1662
+ chain=chain,
1663
+ staking_program_id=current_staking_program,
1664
+ )
769
1665
 
770
- :param hash: Service hash
771
- """
772
- service = self.create_or_load(hash=hash)
773
- if not service.chain_data.user_params.use_staking:
774
- self.logger.info("Cannot unstake service, `use_staking` is set to false")
775
- return
1666
+ if (
1667
+ staking_state == StakingState.STAKED
1668
+ and can_unstake
1669
+ and not sftxb.staking_rewards_available(current_staking_contract)
1670
+ ):
1671
+ self.logger.info(
1672
+ f"There are no rewards available, service {chain_config.chain_data.token} "
1673
+ "is already staked and can be unstaked."
1674
+ )
1675
+ self.logger.info("Skipping unstaking for no rewards available.")
776
1676
 
777
- ocm = self.get_on_chain_manager(service=service)
778
- state = ocm.staking_status(
779
- service_id=service.chain_data.token,
780
- staking_contract=STAKING[service.ledger_config.chain],
1677
+ if (
1678
+ staking_state == StakingState.STAKED
1679
+ and current_staking_program != target_staking_program
1680
+ and can_unstake
1681
+ ):
1682
+ self.logger.info(
1683
+ f"{chain_config.chain_data.token} is staked in a different staking program. Unstaking..."
1684
+ )
1685
+ self.unstake_service_on_chain_from_safe(
1686
+ service_config_id=service_config_id,
1687
+ chain=chain,
1688
+ staking_program_id=current_staking_program,
1689
+ )
1690
+
1691
+ staking_state = sftxb.staking_status(
1692
+ service_id=chain_config.chain_data.token,
1693
+ staking_contract=current_staking_contract,
1694
+ )
1695
+
1696
+ target_program_staking_state = sftxb.staking_status(
1697
+ service_id=chain_config.chain_data.token,
1698
+ staking_contract=target_staking_contract,
781
1699
  )
1700
+ self.logger.info("Checking conditions to stake.")
1701
+
1702
+ staking_rewards_available = sftxb.staking_rewards_available(
1703
+ target_staking_contract
1704
+ )
1705
+ staking_slots_available = sftxb.staking_slots_available(target_staking_contract)
1706
+ current_staking_program = self._get_current_staking_program(
1707
+ service,
1708
+ chain,
1709
+ )
1710
+
782
1711
  self.logger.info(
783
- f"Staking status for service {service.chain_data.token}: {state}"
1712
+ f"use_staking={chain_config.chain_data.user_params.use_staking}"
1713
+ )
1714
+ self.logger.info(f"{on_chain_state=}")
1715
+ self.logger.info(f"{current_staking_program=}")
1716
+ self.logger.info(f"{staking_state=}")
1717
+ self.logger.info(f"{target_staking_program=}")
1718
+ self.logger.info(f"{target_program_staking_state=}")
1719
+ self.logger.info(f"{staking_rewards_available=}")
1720
+ self.logger.info(f"{staking_slots_available=}")
1721
+
1722
+ if (
1723
+ chain_config.chain_data.user_params.use_staking # pylint: disable=too-many-boolean-expressions
1724
+ and staking_state == StakingState.UNSTAKED
1725
+ and target_program_staking_state == StakingState.UNSTAKED
1726
+ and staking_rewards_available
1727
+ and staking_slots_available
1728
+ and on_chain_state == OnChainState.DEPLOYED
1729
+ ):
1730
+ self.logger.info(f"Approving staking: {chain_config.chain_data.token}")
1731
+ sftxb.new_tx().add(
1732
+ sftxb.get_staking_approval_data(
1733
+ service_id=chain_config.chain_data.token,
1734
+ service_registry=CONTRACTS[ledger_config.chain]["service_registry"],
1735
+ staking_contract=target_staking_contract,
1736
+ )
1737
+ ).settle()
1738
+
1739
+ # Approve additional_staking_tokens.
1740
+ staking_params = sftxb.get_staking_params(
1741
+ staking_contract=target_staking_contract
1742
+ )
1743
+
1744
+ for token_contract, min_staking_amount in staking_params[
1745
+ "additional_staking_tokens"
1746
+ ].items():
1747
+ sftxb.new_tx().add(
1748
+ sftxb.get_erc20_approval_data(
1749
+ spender=target_staking_contract,
1750
+ amount=min_staking_amount,
1751
+ erc20_contract=token_contract,
1752
+ )
1753
+ ).settle()
1754
+ staking_contract_allowance = (
1755
+ registry_contracts.erc20.get_instance(
1756
+ ledger_api=sftxb.ledger_api,
1757
+ contract_address=token_contract,
1758
+ )
1759
+ .functions.allowance(
1760
+ sftxb.safe,
1761
+ target_staking_contract,
1762
+ )
1763
+ .call()
1764
+ )
1765
+ self.logger.info(
1766
+ f"Approved {staking_contract_allowance} (token {token_contract}) from {sftxb.safe} to {target_staking_contract}"
1767
+ )
1768
+
1769
+ self.logger.info(f"Staking service: {chain_config.chain_data.token}")
1770
+ sftxb.new_tx().add(
1771
+ sftxb.get_staking_data(
1772
+ service_id=chain_config.chain_data.token,
1773
+ staking_contract=target_staking_contract,
1774
+ )
1775
+ ).settle()
1776
+
1777
+ current_staking_program = self._get_current_staking_program(
1778
+ service,
1779
+ chain,
784
1780
  )
1781
+ self.logger.info(f"{target_staking_program=}")
1782
+ self.logger.info(f"{current_staking_program=}")
1783
+
1784
+ def unstake_service_on_chain(
1785
+ self, service_config_id: str, chain: t.Optional[str] = None
1786
+ ) -> None:
1787
+ """Unbond service on-chain"""
1788
+ # TODO This method has not been thoroughly reviewed. Deprecated usage in favour of Safe version.
1789
+
1790
+ service = self.load(service_config_id=service_config_id)
1791
+ chain_config = service.chain_configs[chain or service.home_chain]
1792
+ ledger_config = chain_config.ledger_config
1793
+ chain_data = chain_config.chain_data
1794
+ ocm = self.get_on_chain_manager(ledger_config=ledger_config)
1795
+
1796
+ state = ocm.staking_status(
1797
+ service_id=chain_data.token,
1798
+ staking_contract=get_staking_contract(
1799
+ chain=ledger_config.chain,
1800
+ staking_program_id=chain_data.user_params.staking_program_id,
1801
+ ),
1802
+ )
1803
+ self.logger.info(f"Staking status for service {chain_data.token}: {state}")
785
1804
  if state not in {StakingState.STAKED, StakingState.EVICTED}:
786
1805
  self.logger.info("Cannot unstake service, it's not staked")
787
- service.chain_data.staked = False
788
- service.store()
789
1806
  return
790
1807
 
791
- self.logger.info(f"Unstaking service: {service.chain_data.token}")
1808
+ self.logger.info(f"Unstaking service: {chain_data.token}")
792
1809
  ocm.unstake(
793
- service_id=service.chain_data.token,
794
- staking_contract=STAKING[service.ledger_config.chain],
1810
+ service_id=chain_data.token,
1811
+ staking_contract=get_staking_contract(
1812
+ chain=ledger_config.chain,
1813
+ staking_program_id=chain_data.user_params.staking_program_id,
1814
+ ),
795
1815
  )
796
- service.chain_data.staked = False
797
- service.store()
798
1816
 
799
- def unstake_service_on_chain_from_safe(self, hash: str) -> None:
800
- """
801
- Unbond service on-chain
1817
+ def unstake_service_on_chain_from_safe(
1818
+ self,
1819
+ service_config_id: str,
1820
+ chain: str,
1821
+ staking_program_id: t.Optional[str] = None,
1822
+ force: bool = False,
1823
+ ) -> None:
1824
+ """Unstake service on-chain"""
1825
+ # Claim the rewards first so that they are moved to the Master Safe
1826
+ self.claim_on_chain_from_safe(
1827
+ service_config_id=service_config_id,
1828
+ chain=chain,
1829
+ )
802
1830
 
803
- :param hash: Service hash
804
- """
805
- service = self.create_or_load(hash=hash)
806
- if not service.chain_data.user_params.use_staking:
807
- self.logger.info("Cannot unstake service, `use_staking` is set to false")
1831
+ self.logger.info("unstake_service_on_chain_from_safe")
1832
+ service = self.load(service_config_id=service_config_id)
1833
+ chain_config = service.chain_configs[chain]
1834
+ ledger_config = chain_config.ledger_config
1835
+ chain_data = chain_config.chain_data
1836
+
1837
+ if staking_program_id is None:
1838
+ self.logger.info(
1839
+ "Cannot unstake service, `staking_program_id` is set to None"
1840
+ )
808
1841
  return
809
1842
 
810
- sftxb = self.get_eth_safe_tx_builder(service=service)
1843
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
811
1844
  state = sftxb.staking_status(
812
- service_id=service.chain_data.token,
813
- staking_contract=STAKING[service.ledger_config.chain],
814
- )
815
- self.logger.info(
816
- f"Staking status for service {service.chain_data.token}: {state}"
1845
+ service_id=chain_data.token,
1846
+ staking_contract=get_staking_contract(
1847
+ chain=ledger_config.chain,
1848
+ staking_program_id=staking_program_id,
1849
+ ),
817
1850
  )
1851
+ self.logger.info(f"Staking status for service {chain_data.token}: {state}")
818
1852
  if state not in {StakingState.STAKED, StakingState.EVICTED}:
819
1853
  self.logger.info("Cannot unstake service, it's not staked")
820
- service.chain_data.staked = False
821
- service.store()
822
1854
  return
823
1855
 
824
- self.logger.info(f"Unstaking service: {service.chain_data.token}")
1856
+ self.logger.info(f"Unstaking service: {chain_data.token}")
825
1857
  sftxb.new_tx().add(
826
1858
  sftxb.get_unstaking_data(
827
- service_id=service.chain_data.token,
828
- staking_contract=STAKING[service.ledger_config.chain],
1859
+ service_id=chain_data.token,
1860
+ staking_contract=get_staking_contract(
1861
+ chain=ledger_config.chain,
1862
+ staking_program_id=staking_program_id,
1863
+ ),
1864
+ force=force,
829
1865
  )
830
1866
  ).settle()
831
- service.chain_data.staked = False
832
- service.store()
833
1867
 
834
- def fund_service( # pylint: disable=too-many-arguments
1868
+ def claim_all_on_chain_from_safe(self) -> None:
1869
+ """Claim rewards from all services and chains"""
1870
+ self.logger.info("claim_all_on_chain_from_safe")
1871
+ services, _ = self.get_all_services()
1872
+ for service in services:
1873
+ self.claim_on_chain_from_safe(
1874
+ service_config_id=service.service_config_id,
1875
+ chain=service.home_chain,
1876
+ )
1877
+
1878
+ def claim_on_chain_from_safe(
835
1879
  self,
836
- hash: str,
1880
+ service_config_id: str,
1881
+ chain: str,
1882
+ ) -> int:
1883
+ """Claim rewards from staking and returns the claimed amount"""
1884
+ self.logger.info("claim_on_chain_from_safe")
1885
+ service = self.load(service_config_id=service_config_id)
1886
+ chain_config = service.chain_configs[chain]
1887
+ ledger_config = chain_config.ledger_config
1888
+ wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
1889
+ ledger_api = wallet.ledger_api(chain=ledger_config.chain, rpc=ledger_config.rpc)
1890
+
1891
+ if (
1892
+ chain_config.chain_data.token == NON_EXISTENT_TOKEN
1893
+ or chain_config.chain_data.multisig == ZERO_ADDRESS
1894
+ ):
1895
+ self.logger.info("Service is not minted or Safe not deployed.")
1896
+ return 0
1897
+
1898
+ self.logger.info(
1899
+ f"OLAS Balance on service Safe {chain_config.chain_data.multisig}: "
1900
+ f"{get_asset_balance(ledger_api, OLAS[Chain(chain)], chain_config.chain_data.multisig)}"
1901
+ )
1902
+ current_staking_program = self._get_current_staking_program(
1903
+ service=service, chain=chain
1904
+ )
1905
+ staking_contract = get_staking_contract(
1906
+ chain=ledger_config.chain,
1907
+ staking_program_id=current_staking_program,
1908
+ )
1909
+ if staking_contract is None:
1910
+ self.logger.warning(
1911
+ "No staking contract found for the "
1912
+ f"{current_staking_program=}. Not claiming the rewards."
1913
+ )
1914
+ return 0
1915
+
1916
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
1917
+ if not sftxb.staking_rewards_claimable(
1918
+ service_id=chain_config.chain_data.token,
1919
+ staking_contract=staking_contract,
1920
+ ):
1921
+ self.logger.info("No staking rewards claimable")
1922
+ return 0
1923
+
1924
+ receipt = (
1925
+ sftxb.new_tx()
1926
+ .add(
1927
+ sftxb.get_claiming_data(
1928
+ service_id=chain_config.chain_data.token,
1929
+ staking_contract=staking_contract,
1930
+ )
1931
+ )
1932
+ .settle()
1933
+ )
1934
+
1935
+ if receipt.status != 1:
1936
+ self.logger.error(
1937
+ f"Failed to claim staking rewards. Tx hash: {receipt.tx_hash}"
1938
+ )
1939
+ return 0
1940
+
1941
+ # transfer claimed amount from agents safe to master safe
1942
+ # TODO: remove after staking contract directly starts sending the rewards to master safe
1943
+ amount_claimed = int(receipt["logs"][0]["data"].hex(), 16)
1944
+ self.logger.info(f"Claimed amount: {amount_claimed}")
1945
+ ethereum_crypto = self.keys_manager.get_crypto_instance(
1946
+ service.agent_addresses[0]
1947
+ )
1948
+ transfer_erc20_from_safe(
1949
+ ledger_api=ledger_api,
1950
+ crypto=ethereum_crypto,
1951
+ safe=chain_config.chain_data.multisig,
1952
+ token=receipt["logs"][0]["address"],
1953
+ to=wallet.safes[Chain(chain)],
1954
+ amount=amount_claimed,
1955
+ )
1956
+ return amount_claimed
1957
+
1958
+ def fund_service( # pylint: disable=too-many-arguments,too-many-locals
1959
+ self,
1960
+ service_config_id: str,
1961
+ amounts: ChainAmounts,
1962
+ ) -> None:
1963
+ """Fund service if required."""
1964
+ service = self.load(service_config_id=service_config_id)
1965
+ self.funding_manager.fund_service(service=service, amounts=amounts)
1966
+
1967
+ # TODO deprecate
1968
+ def fund_service_single_chain( # pylint: disable=too-many-arguments,too-many-locals,too-many-statements
1969
+ self,
1970
+ service_config_id: str,
1971
+ rpc: t.Optional[str] = None,
1972
+ funding_values: t.Optional[FundingValues] = None,
1973
+ from_safe: bool = True,
1974
+ chain: str = "gnosis",
1975
+ ) -> None:
1976
+ """Fund service if required."""
1977
+
1978
+ service = self.load(service_config_id=service_config_id)
1979
+ chain_config = service.chain_configs[chain]
1980
+ ledger_config = chain_config.ledger_config
1981
+ chain_data = chain_config.chain_data
1982
+ wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
1983
+ ledger_api = wallet.ledger_api(
1984
+ chain=ledger_config.chain, rpc=rpc or ledger_config.rpc
1985
+ )
1986
+
1987
+ for (
1988
+ asset_address,
1989
+ fund_requirements,
1990
+ ) in chain_data.user_params.fund_requirements.items():
1991
+ on_chain_operations_buffer = 0
1992
+ if asset_address == ZERO_ADDRESS:
1993
+ on_chain_state = self._get_on_chain_state(service=service, chain=chain)
1994
+ if on_chain_state != OnChainState.DEPLOYED:
1995
+ if chain_data.user_params.use_staking:
1996
+ on_chain_operations_buffer = 1 + len(service.agent_addresses)
1997
+ else:
1998
+ on_chain_operations_buffer = (
1999
+ chain_data.user_params.cost_of_bond
2000
+ * (
2001
+ MIN_SECURITY_DEPOSIT
2002
+ + MIN_AGENT_BOND * len(service.agent_addresses)
2003
+ )
2004
+ )
2005
+
2006
+ asset_funding_values = (
2007
+ funding_values.get(asset_address)
2008
+ if funding_values is not None
2009
+ else None
2010
+ )
2011
+ agent_fund_threshold = (
2012
+ asset_funding_values["agent"]["threshold"]
2013
+ if asset_funding_values is not None
2014
+ else fund_requirements.agent
2015
+ )
2016
+
2017
+ for agent_address in service.agent_addresses:
2018
+ agent_balance = get_asset_balance(
2019
+ ledger_api=ledger_api,
2020
+ asset_address=asset_address,
2021
+ address=agent_address,
2022
+ )
2023
+ self.logger.info(
2024
+ f"[FUNDING_JOB] Agent {agent_address} Asset: {asset_address} balance: {agent_balance}"
2025
+ )
2026
+ if agent_fund_threshold > 0:
2027
+ self.logger.info(
2028
+ f"[FUNDING_JOB] Required balance: {agent_fund_threshold}"
2029
+ )
2030
+ if agent_balance < agent_fund_threshold:
2031
+ self.logger.info(f"[FUNDING_JOB] Funding agent {agent_address}")
2032
+ target_balance = (
2033
+ asset_funding_values["agent"]["topup"]
2034
+ if asset_funding_values is not None
2035
+ else fund_requirements.agent
2036
+ )
2037
+ available_balance = get_asset_balance(
2038
+ ledger_api=ledger_api,
2039
+ asset_address=asset_address,
2040
+ address=wallet.safes[ledger_config.chain],
2041
+ )
2042
+ available_balance = max(
2043
+ available_balance - on_chain_operations_buffer, 0
2044
+ )
2045
+ to_transfer = max(
2046
+ min(available_balance, target_balance - agent_balance), 0
2047
+ )
2048
+ if to_transfer <= 0:
2049
+ continue
2050
+
2051
+ self.logger.info(
2052
+ f"[FUNDING_JOB] Transferring {to_transfer} units (asset {asset_address}) to agent {agent_address}"
2053
+ )
2054
+ wallet.transfer(
2055
+ asset=asset_address,
2056
+ to=agent_address,
2057
+ amount=int(to_transfer),
2058
+ chain=ledger_config.chain,
2059
+ from_safe=from_safe,
2060
+ rpc=rpc or ledger_config.rpc,
2061
+ )
2062
+
2063
+ if chain_data.multisig == NON_EXISTENT_MULTISIG:
2064
+ self.logger.info("[FUNDING_JOB] Service Safe not deployed")
2065
+ continue
2066
+
2067
+ safe_balance = get_asset_balance(
2068
+ ledger_api=ledger_api,
2069
+ asset_address=asset_address,
2070
+ address=chain_data.multisig,
2071
+ )
2072
+ if asset_address == ZERO_ADDRESS and chain in WRAPPED_NATIVE_ASSET:
2073
+ # also count the balance of the wrapped native asset
2074
+ safe_balance += get_asset_balance(
2075
+ ledger_api=ledger_api,
2076
+ asset_address=WRAPPED_NATIVE_ASSET[Chain(chain)],
2077
+ address=chain_data.multisig,
2078
+ )
2079
+
2080
+ safe_fund_treshold = (
2081
+ asset_funding_values["safe"]["threshold"]
2082
+ if asset_funding_values is not None
2083
+ else fund_requirements.safe
2084
+ )
2085
+ self.logger.info(
2086
+ f"[FUNDING_JOB] Safe {chain_data.multisig} Asset: {asset_address} balance: {safe_balance}"
2087
+ )
2088
+ self.logger.info(f"[FUNDING_JOB] Required balance: {safe_fund_treshold}")
2089
+ if safe_balance < safe_fund_treshold:
2090
+ self.logger.info("[FUNDING_JOB] Funding safe")
2091
+ target_balance = (
2092
+ asset_funding_values["safe"]["topup"]
2093
+ if asset_funding_values is not None
2094
+ else fund_requirements.safe
2095
+ )
2096
+ available_balance = get_asset_balance(
2097
+ ledger_api=ledger_api,
2098
+ asset_address=asset_address,
2099
+ address=wallet.safes[ledger_config.chain],
2100
+ )
2101
+ available_balance = max(
2102
+ available_balance - on_chain_operations_buffer, 0
2103
+ )
2104
+ to_transfer = max(
2105
+ min(available_balance, target_balance - safe_balance), 0
2106
+ )
2107
+
2108
+ # TODO Possibly remove this logging
2109
+ self.logger.info(f"{available_balance=}")
2110
+ self.logger.info(f"{target_balance=}")
2111
+ self.logger.info(f"{safe_balance=}")
2112
+ self.logger.info(f"{to_transfer=}")
2113
+
2114
+ if to_transfer > 0:
2115
+ self.logger.info(
2116
+ f"[FUNDING_JOB] Transferring {to_transfer} units (asset {asset_address}) to {chain_data.multisig}"
2117
+ )
2118
+ # TODO: This is a temporary fix
2119
+ # we avoid the error here because there is a seperate prompt on the UI
2120
+ # when not enough funds are present, and the FE doesn't let the user to start the agent.
2121
+ # Ideally this error should be allowed, and then the FE should ask the user for more funds.
2122
+ with suppress(RuntimeError):
2123
+ wallet.transfer(
2124
+ asset=asset_address,
2125
+ to=t.cast(str, chain_data.multisig),
2126
+ amount=int(to_transfer),
2127
+ chain=ledger_config.chain,
2128
+ rpc=rpc or ledger_config.rpc,
2129
+ )
2130
+
2131
+ # TODO Deprecate
2132
+ # TODO This method is possibly not used anymore
2133
+ def fund_service_erc20( # pylint: disable=too-many-arguments,too-many-locals
2134
+ self,
2135
+ service_config_id: str,
2136
+ token: str,
837
2137
  rpc: t.Optional[str] = None,
838
2138
  agent_topup: t.Optional[float] = None,
839
2139
  safe_topup: t.Optional[float] = None,
840
2140
  agent_fund_threshold: t.Optional[float] = None,
841
2141
  safe_fund_treshold: t.Optional[float] = None,
842
2142
  from_safe: bool = True,
2143
+ chain: str = "gnosis",
843
2144
  ) -> None:
844
2145
  """Fund service if required."""
845
- service = self.create_or_load(hash=hash)
846
- wallet = self.wallet_manager.load(ledger_type=service.ledger_config.type)
847
- ledger_api = wallet.ledger_api(chain_type=service.ledger_config.chain, rpc=rpc)
2146
+ service = self.load(service_config_id=service_config_id)
2147
+ chain_config = service.chain_configs[chain]
2148
+ ledger_config = chain_config.ledger_config
2149
+ chain_data = chain_config.chain_data
2150
+ wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
2151
+ ledger_api = wallet.ledger_api(
2152
+ chain=ledger_config.chain, rpc=rpc or ledger_config.rpc
2153
+ )
848
2154
  agent_fund_threshold = (
849
2155
  agent_fund_threshold
850
- or service.chain_data.user_params.fund_requirements.agent
2156
+ or chain_data.user_params.fund_requirements[ZERO_ADDRESS].agent
851
2157
  )
852
2158
 
853
- for key in service.keys:
854
- agent_balance = ledger_api.get_balance(address=key.address)
855
- self.logger.info(f"Agent {key.address} balance: {agent_balance}")
2159
+ for agent_address in service.agent_addresses:
2160
+ agent_balance = ledger_api.get_balance(address=agent_address)
2161
+ self.logger.info(f"Agent {agent_address} balance: {agent_balance}")
856
2162
  self.logger.info(f"Required balance: {agent_fund_threshold}")
857
2163
  if agent_balance < agent_fund_threshold:
858
2164
  self.logger.info("Funding agents")
859
2165
  to_transfer = (
860
2166
  agent_topup
861
- or service.chain_data.user_params.fund_requirements.agent
2167
+ or chain_data.user_params.fund_requirements[ZERO_ADDRESS].agent
862
2168
  )
863
- self.logger.info(f"Transferring {to_transfer} units to {key.address}")
2169
+ if to_transfer <= 0:
2170
+ continue
2171
+
2172
+ self.logger.info(f"Transferring {to_transfer} units to {agent_address}")
864
2173
  wallet.transfer(
865
- to=key.address,
2174
+ asset=token,
2175
+ to=agent_address,
866
2176
  amount=int(to_transfer),
867
- chain_type=service.ledger_config.chain,
2177
+ chain=ledger_config.chain,
868
2178
  from_safe=from_safe,
2179
+ rpc=rpc or ledger_config.rpc,
869
2180
  )
870
2181
 
871
- safe_balanace = ledger_api.get_balance(service.chain_data.multisig)
2182
+ safe_balance = (
2183
+ registry_contracts.erc20.get_instance(ledger_api, token)
2184
+ .functions.balanceOf(chain_data.multisig)
2185
+ .call()
2186
+ )
872
2187
  safe_fund_treshold = (
873
- safe_fund_treshold or service.chain_data.user_params.fund_requirements.safe
2188
+ safe_fund_treshold
2189
+ or chain_data.user_params.fund_requirements[ZERO_ADDRESS].safe
874
2190
  )
875
- self.logger.info(f"Safe {service.chain_data.multisig} balance: {safe_balanace}")
2191
+ self.logger.info(f"Safe {chain_data.multisig} balance: {safe_balance}")
876
2192
  self.logger.info(f"Required balance: {safe_fund_treshold}")
877
- if safe_balanace < safe_fund_treshold:
2193
+ if safe_balance < safe_fund_treshold:
878
2194
  self.logger.info("Funding safe")
879
2195
  to_transfer = (
880
- safe_topup or service.chain_data.user_params.fund_requirements.safe
2196
+ safe_topup
2197
+ or chain_data.user_params.fund_requirements[ZERO_ADDRESS].safe
881
2198
  )
2199
+ if to_transfer <= 0:
2200
+ return
2201
+
882
2202
  self.logger.info(
883
- f"Transferring {to_transfer} units to {service.chain_data.multisig}"
2203
+ f"Transferring {to_transfer} units to {chain_data.multisig}"
884
2204
  )
885
2205
  wallet.transfer(
886
- to=t.cast(str, service.chain_data.multisig),
2206
+ asset=token,
2207
+ to=t.cast(str, chain_data.multisig),
887
2208
  amount=int(to_transfer),
888
- chain_type=service.ledger_config.chain,
2209
+ chain=ledger_config.chain,
2210
+ rpc=rpc or ledger_config.rpc,
889
2211
  )
890
2212
 
891
- async def funding_job(
892
- self,
893
- hash: str,
894
- loop: t.Optional[asyncio.AbstractEventLoop] = None,
895
- from_safe: bool = True,
2213
+ def drain(
2214
+ self, service_config_id: str, chain_str: str, withdrawal_address: str
896
2215
  ) -> None:
897
- """Start a background funding job."""
898
- loop = loop or asyncio.get_event_loop()
899
- service = self.create_or_load(hash=hash)
900
- with ThreadPoolExecutor() as executor:
901
- while True:
902
- try:
903
- await loop.run_in_executor(
904
- executor,
905
- self.fund_service,
906
- hash, # Service hash
907
- PUBLIC_RPCS[service.ledger_config.chain], # RPC
908
- 100000000000000000, # agent_topup
909
- 2000000000000000000, # safe_topup
910
- 50000000000000000, # agent_fund_threshold
911
- 500000000000000000, # safe_fund_treshold
912
- from_safe,
913
- )
914
- except Exception: # pylint: disable=broad-except
915
- logging.info(
916
- f"Error occured while funding the service\n{traceback.format_exc()}"
917
- )
918
- await asyncio.sleep(60)
2216
+ """Drain service safe and agent EOAs."""
2217
+ service = self.load(service_config_id=service_config_id)
2218
+ chain = Chain(chain_str)
2219
+ self.funding_manager.drain_service_safe(
2220
+ service=service,
2221
+ withdrawal_address=withdrawal_address,
2222
+ chain=chain,
2223
+ )
2224
+ self.funding_manager.drain_agents_eoas(
2225
+ service=service,
2226
+ withdrawal_address=withdrawal_address,
2227
+ chain=chain,
2228
+ )
919
2229
 
920
- async def healthcheck_job(
2230
+ def deploy_service_locally( # pylint: disable=too-many-arguments
921
2231
  self,
922
- hash: str,
923
- ) -> None:
924
- """Start a background funding job."""
925
- failed_health_checks = 0
926
-
927
- while True:
928
- try:
929
- # Check the service health
930
- healthy = await check_service_health()
931
- # Restart the service if the health failed 5 times in a row
932
- if not healthy:
933
- failed_health_checks += 1
934
- else:
935
- failed_health_checks = 0
936
- if failed_health_checks >= 4:
937
- self.stop_service_locally(hash=hash)
938
- self.deploy_service_locally(hash=hash)
939
-
940
- except Exception: # pylint: disable=broad-except
941
- logging.info(
942
- f"Error occured while checking the service health\n{traceback.format_exc()}"
943
- )
944
- await asyncio.sleep(30)
945
-
946
- def deploy_service_locally(self, hash: str, force: bool = True) -> Deployment:
2232
+ service_config_id: str,
2233
+ chain: t.Optional[str] = None,
2234
+ use_docker: bool = False,
2235
+ use_kubernetes: bool = False,
2236
+ build_only: bool = False,
2237
+ ) -> Deployment:
947
2238
  """
948
2239
  Deploy service locally
949
2240
 
950
2241
  :param hash: Service hash
951
- :param force: Remove previous deployment and start a new one.
2242
+ :param chain: Chain to set runtime parameters on the deployment (home_chain if not provided).
2243
+ :param use_docker: Use a Docker Compose deployment (True) or Host deployment (False).
2244
+ :param use_kubernetes: Use Kubernetes for deployment
2245
+ :param build_only: Only build the deployment without starting it
952
2246
  :return: Deployment instance
953
2247
  """
954
- deployment = self.create_or_load(hash=hash).deployment
955
- deployment.build(force=force)
956
- deployment.start()
2248
+ service = self.load(service_config_id=service_config_id)
2249
+
2250
+ deployment = service.deployment
2251
+ deployment.build(
2252
+ use_docker=use_docker,
2253
+ use_kubernetes=use_kubernetes,
2254
+ force=True,
2255
+ chain=chain or service.home_chain,
2256
+ keys_manager=self.keys_manager,
2257
+ )
2258
+ if build_only:
2259
+ return deployment
2260
+ deployment.start(
2261
+ password=self.wallet_manager.password,
2262
+ use_docker=use_docker,
2263
+ is_aea=service.agent_release["is_aea"],
2264
+ )
957
2265
  return deployment
958
2266
 
959
- def stop_service_locally(self, hash: str, delete: bool = False) -> Deployment:
2267
+ def stop_service_locally(
2268
+ self,
2269
+ service_config_id: str,
2270
+ delete: bool = False,
2271
+ use_docker: bool = False,
2272
+ force: bool = False,
2273
+ ) -> Deployment:
960
2274
  """
961
2275
  Stop service locally
962
2276
 
963
- :param hash: Service hash
2277
+ :param service_id: Service id
964
2278
  :param delete: Delete local deployment.
965
2279
  :return: Deployment instance
966
2280
  """
967
- deployment = self.create_or_load(hash=hash).deployment
968
- deployment.stop()
2281
+ service = self.load(service_config_id=service_config_id)
2282
+ service.remove_latest_healthcheck()
2283
+ deployment = service.deployment
2284
+ deployment.stop(
2285
+ use_docker=use_docker,
2286
+ force=force,
2287
+ is_aea=service.agent_release["is_aea"],
2288
+ )
969
2289
  if delete:
970
2290
  deployment.delete()
971
2291
  return deployment
972
2292
 
973
- def update_service(
2293
+ def update(
974
2294
  self,
975
- old_hash: str,
976
- new_hash: str,
977
- rpc: t.Optional[str] = None,
978
- on_chain_user_params: t.Optional[OnChainUserParams] = None,
979
- from_safe: bool = True, # pylint: disable=unused-argument
2295
+ service_config_id: str,
2296
+ service_template: ServiceTemplate,
2297
+ allow_different_service_public_id: bool = False,
2298
+ partial_update: bool = True,
980
2299
  ) -> Service:
981
2300
  """Update a service."""
982
- old_service = self.create_or_load(
983
- hash=old_hash,
2301
+
2302
+ self.logger.info(f"Updating {service_config_id=}")
2303
+ service = self.load(service_config_id=service_config_id)
2304
+ service.update(
2305
+ service_template=service_template,
2306
+ allow_different_service_public_id=allow_different_service_public_id,
2307
+ partial_update=partial_update,
984
2308
  )
985
- # TODO code for updating service commented until safe swap transaction is implemented
986
- # This is a temporary fix that will only work for services that have not started the
987
- # update flow. Services having started the update flow must need to manually change
988
- # the Safe owner to the Operator.
989
- # ( # noqa: E800
990
- # self.unstake_service_on_chain_from_safe # noqa: E800
991
- # if from_safe # noqa: E800
992
- # else self.unstake_service_on_chain # noqa: E800
993
- # )( # noqa: E800
994
- # hash=old_hash, # noqa: E800
995
- # ) # noqa: E800
996
- # ( # noqa: E800
997
- # self.terminate_service_on_chain_from_safe # noqa: E800
998
- # if from_safe # noqa: E800
999
- # else self.terminate_service_on_chain # noqa: E800
1000
- # )( # noqa: E800
1001
- # hash=old_hash, # noqa: E800
1002
- # ) # noqa: E800
1003
- # ( # noqa: E800
1004
- # self.unbond_service_on_chain_from_safe # noqa: E800
1005
- # if from_safe # noqa: E800
1006
- # else self.unbond_service_on_chain # noqa: E800
1007
- # )( # noqa: E800
1008
- # hash=old_hash, # noqa: E800
1009
- # ) # noqa: E800
1010
-
1011
- # owner, *_ = old_service.chain_data.instances # noqa: E800
1012
- # if from_safe: # noqa: E800
1013
- # sftx = self.get_eth_safe_tx_builder(service=old_service) # noqa: E800
1014
- # sftx.new_tx().add( # noqa: E800
1015
- # sftx.get_swap_data( # noqa: E800
1016
- # service_id=old_service.chain_data.token, # noqa: E800
1017
- # multisig=old_service.chain_data.multisig, # noqa: E800
1018
- # owner_key=str(self.keys_manager.get(key=owner).private_key), # noqa: E800
1019
- # ) # noqa: E800
1020
- # ).settle() # noqa: E800
1021
- # else: # noqa: E800
1022
- # ocm = self.get_on_chain_manager(service=old_service) # noqa: E800
1023
- # ocm.swap( # noqa: E800
1024
- # service_id=old_service.chain_data.token, # noqa: E800
1025
- # multisig=old_service.chain_data.multisig, # noqa: E800
1026
- # owner_key=str(self.keys_manager.get(key=owner).private_key), # noqa: E800
1027
- # ) # noqa: E800
1028
-
1029
- new_service = self.create_or_load(
1030
- hash=new_hash,
1031
- rpc=rpc or old_service.ledger_config.rpc,
1032
- on_chain_user_params=on_chain_user_params
1033
- or old_service.chain_data.user_params,
2309
+ return service
2310
+
2311
+ def funding_requirements( # pylint: disable=too-many-locals,too-many-statements,too-many-nested-blocks
2312
+ self, service_config_id: str
2313
+ ) -> t.Dict:
2314
+ """Get the funding requirements for a service."""
2315
+ service = self.load(service_config_id=service_config_id)
2316
+ return self.funding_manager.funding_requirements(service)
2317
+
2318
+ # TODO deprecate
2319
+ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements,too-many-nested-blocks
2320
+ self, service_config_id: str
2321
+ ) -> t.Dict:
2322
+ """Get user refill requirements for a service."""
2323
+ service = self.load(service_config_id=service_config_id)
2324
+
2325
+ balances: t.Dict = {}
2326
+ bonded_assets: t.Dict = {}
2327
+ protocol_asset_requirements: t.Dict = {}
2328
+ refill_requirements: t.Dict = {}
2329
+ total_requirements: t.Dict = {}
2330
+ allow_start_agent = True
2331
+ is_refill_required = False
2332
+
2333
+ for chain, chain_config in service.chain_configs.items():
2334
+ ledger_config = chain_config.ledger_config
2335
+ chain_data = chain_config.chain_data
2336
+ wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
2337
+ ledger_api = wallet.ledger_api(
2338
+ chain=ledger_config.chain, rpc=ledger_config.rpc
2339
+ )
2340
+ os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc
2341
+
2342
+ master_eoa = wallet.address
2343
+ master_safe_exists = wallet.safes.get(Chain(chain)) is not None
2344
+ master_safe = wallet.safes.get(Chain(chain), "master_safe")
2345
+
2346
+ agent_addresses = set(service.agent_addresses)
2347
+ service_safe = (
2348
+ chain_data.multisig if chain_data.multisig else "service_safe"
2349
+ )
2350
+
2351
+ if not master_safe_exists:
2352
+ allow_start_agent = False
2353
+
2354
+ # Protocol asset requirements
2355
+ protocol_asset_requirements[
2356
+ chain
2357
+ ] = self._compute_protocol_asset_requirements(service_config_id, chain)
2358
+ service_asset_requirements = chain_data.user_params.fund_requirements
2359
+
2360
+ # Bonded assets
2361
+ bonded_assets[chain] = self._compute_bonded_assets(service_config_id, chain)
2362
+
2363
+ # Balances
2364
+ addresses = agent_addresses | {service_safe, master_eoa, master_safe}
2365
+ asset_addresses = (
2366
+ {ZERO_ADDRESS}
2367
+ | service_asset_requirements.keys()
2368
+ | protocol_asset_requirements[chain].keys()
2369
+ | bonded_assets[chain].keys()
2370
+ )
2371
+
2372
+ balances[chain] = get_assets_balances(
2373
+ ledger_api=ledger_api,
2374
+ addresses=addresses,
2375
+ asset_addresses=asset_addresses,
2376
+ raise_on_invalid_address=False,
2377
+ )
2378
+
2379
+ # TODO this is a patch for the case when excess balance is in MasterEOA
2380
+ # and MasterSafe is not created (typically for onboarding bridging).
2381
+ # It simulates the "balance in the future" for both addesses when
2382
+ # transfering the excess assets.
2383
+ if master_safe == "master_safe":
2384
+ eoa_funding_values = self.get_master_eoa_native_funding_values(
2385
+ master_safe_exists=master_safe_exists,
2386
+ chain=Chain(chain),
2387
+ balance=balances[chain][master_eoa][ZERO_ADDRESS],
2388
+ )
2389
+
2390
+ for asset in balances[chain][master_safe]:
2391
+ if asset == ZERO_ADDRESS:
2392
+ balances[chain][master_safe][asset] = max(
2393
+ balances[chain][master_eoa][asset]
2394
+ - eoa_funding_values["topup"],
2395
+ 0,
2396
+ )
2397
+ balances[chain][master_eoa][asset] = min(
2398
+ balances[chain][master_eoa][asset],
2399
+ eoa_funding_values["topup"],
2400
+ )
2401
+ else:
2402
+ balances[chain][master_safe][asset] = balances[chain][
2403
+ master_eoa
2404
+ ][asset]
2405
+ balances[chain][master_eoa][asset] = 0
2406
+
2407
+ # TODO this is a balances patch to count wrapped native asset as
2408
+ # native assets for the service safe
2409
+ if Chain(chain) in WRAPPED_NATIVE_ASSET:
2410
+ if WRAPPED_NATIVE_ASSET[Chain(chain)] not in asset_addresses:
2411
+ balances[chain][service_safe][ZERO_ADDRESS] += get_asset_balance(
2412
+ ledger_api=ledger_api,
2413
+ asset_address=WRAPPED_NATIVE_ASSET[Chain(chain)],
2414
+ address=service_safe,
2415
+ raise_on_invalid_address=False,
2416
+ )
2417
+
2418
+ # Refill requirements
2419
+ refill_requirements[chain] = {}
2420
+ total_requirements[chain] = {}
2421
+
2422
+ # Refill requirements for Master Safe
2423
+ for asset_address in (
2424
+ service_asset_requirements.keys()
2425
+ | protocol_asset_requirements[chain].keys()
2426
+ ):
2427
+ agent_asset_funding_values = {}
2428
+ if asset_address in service_asset_requirements:
2429
+ fund_requirements = service_asset_requirements[asset_address]
2430
+ agent_asset_funding_values = {
2431
+ address: {
2432
+ "topup": fund_requirements.agent,
2433
+ "threshold": int(
2434
+ fund_requirements.agent * DEFAULT_TOPUP_THRESHOLD
2435
+ ), # TODO make threshold configurable
2436
+ "balance": balances[chain][address][asset_address],
2437
+ }
2438
+ for address in agent_addresses
2439
+ }
2440
+ agent_asset_funding_values[service_safe] = {
2441
+ "topup": fund_requirements.safe,
2442
+ "threshold": int(
2443
+ fund_requirements.safe * DEFAULT_TOPUP_THRESHOLD
2444
+ ), # TODO make threshold configurable
2445
+ "balance": balances[chain][service_safe][asset_address],
2446
+ }
2447
+
2448
+ recommended_refill = self._compute_refill_requirement(
2449
+ asset_funding_values=agent_asset_funding_values,
2450
+ sender_topup=protocol_asset_requirements[chain].get(
2451
+ asset_address, 0
2452
+ ),
2453
+ sender_threshold=protocol_asset_requirements[chain].get(
2454
+ asset_address, 0
2455
+ ),
2456
+ sender_balance=balances[chain][master_safe][asset_address]
2457
+ + bonded_assets[chain].get(asset_address, 0),
2458
+ )["recommended_refill"]
2459
+
2460
+ refill_requirements[chain].setdefault(master_safe, {})[
2461
+ asset_address
2462
+ ] = recommended_refill
2463
+
2464
+ total_requirements[chain].setdefault(master_safe, {})[
2465
+ asset_address
2466
+ ] = sum(
2467
+ agent_asset_funding_values[address]["topup"]
2468
+ for address in agent_asset_funding_values
2469
+ ) + protocol_asset_requirements[
2470
+ chain
2471
+ ].get(
2472
+ asset_address, 0
2473
+ )
2474
+
2475
+ if asset_address == ZERO_ADDRESS and any(
2476
+ balances[chain][master_safe][asset_address] == 0
2477
+ and balances[chain][address][asset_address] == 0
2478
+ and agent_asset_funding_values[address]["threshold"] > 0
2479
+ for address in agent_asset_funding_values
2480
+ ):
2481
+ allow_start_agent = False
2482
+
2483
+ # Refill requirements for Master EOA
2484
+ eoa_funding_values = self.get_master_eoa_native_funding_values(
2485
+ master_safe_exists=master_safe_exists,
2486
+ chain=Chain(chain),
2487
+ balance=balances[chain][master_eoa][ZERO_ADDRESS],
2488
+ )
2489
+
2490
+ eoa_recommended_refill = self._compute_refill_requirement(
2491
+ asset_funding_values={},
2492
+ sender_topup=eoa_funding_values["topup"],
2493
+ sender_threshold=eoa_funding_values["threshold"],
2494
+ sender_balance=balances[chain][master_eoa][ZERO_ADDRESS],
2495
+ )["recommended_refill"]
2496
+
2497
+ refill_requirements[chain].setdefault(master_eoa, {})[
2498
+ ZERO_ADDRESS
2499
+ ] = eoa_recommended_refill
2500
+
2501
+ total_requirements[chain].setdefault(master_eoa, {})[
2502
+ ZERO_ADDRESS
2503
+ ] = eoa_funding_values["topup"]
2504
+
2505
+ is_refill_required = any(
2506
+ amount > 0
2507
+ for chain in refill_requirements.values()
2508
+ for asset in chain.values()
2509
+ for amount in asset.values()
2510
+ )
2511
+
2512
+ return {
2513
+ "balances": balances,
2514
+ "bonded_assets": bonded_assets,
2515
+ "total_requirements": total_requirements,
2516
+ "refill_requirements": refill_requirements,
2517
+ "protocol_asset_requirements": protocol_asset_requirements,
2518
+ "is_refill_required": is_refill_required,
2519
+ "allow_start_agent": allow_start_agent,
2520
+ }
2521
+
2522
+ # TODO deprecate
2523
+ def _compute_bonded_assets( # pylint: disable=too-many-locals
2524
+ self, service_config_id: str, chain: str
2525
+ ) -> t.Dict:
2526
+ """Computes the bonded tokens: current agent bonds and current security deposit"""
2527
+
2528
+ service = self.load(service_config_id=service_config_id)
2529
+ chain_config = service.chain_configs[chain]
2530
+ ledger_config = chain_config.ledger_config
2531
+ user_params = chain_config.chain_data.user_params
2532
+ wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
2533
+ bonded_assets: defaultdict = defaultdict(int)
2534
+
2535
+ if Chain(chain) not in wallet.safes:
2536
+ return dict(bonded_assets)
2537
+
2538
+ master_safe = wallet.safes[Chain(chain)]
2539
+
2540
+ ledger_api = wallet.ledger_api(chain=ledger_config.chain, rpc=ledger_config.rpc)
2541
+
2542
+ service_id = chain_config.chain_data.token
2543
+ if service_id == NON_EXISTENT_TOKEN:
2544
+ return dict(bonded_assets)
2545
+
2546
+ os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc
2547
+
2548
+ # Determine bonded native amount
2549
+ service_registry_address = CHAIN_PROFILES[chain]["service_registry"]
2550
+ service_registry = registry_contracts.service_registry.get_instance(
2551
+ ledger_api=ledger_api,
2552
+ contract_address=service_registry_address,
2553
+ )
2554
+ service_info = service_registry.functions.getService(service_id).call()
2555
+ security_deposit = service_info[0]
2556
+ service_state = service_info[6]
2557
+ agent_ids = service_info[7]
2558
+
2559
+ if (
2560
+ OnChainState.ACTIVE_REGISTRATION
2561
+ <= service_state
2562
+ < OnChainState.TERMINATED_BONDED
2563
+ ):
2564
+ bonded_assets[ZERO_ADDRESS] += security_deposit
2565
+
2566
+ operator_balance = service_registry.functions.getOperatorBalance(
2567
+ master_safe, service_id
2568
+ ).call()
2569
+ bonded_assets[ZERO_ADDRESS] += operator_balance
2570
+
2571
+ # Determine bonded token amount for staking programs
2572
+ current_staking_program = self._get_current_staking_program(service, chain)
2573
+ target_staking_program = user_params.staking_program_id
2574
+ staking_contract = get_staking_contract(
2575
+ chain=ledger_config.chain,
2576
+ staking_program_id=current_staking_program or target_staking_program,
2577
+ )
2578
+
2579
+ if not staking_contract:
2580
+ return dict(bonded_assets)
2581
+
2582
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
2583
+ staking_params = sftxb.get_staking_params(staking_contract=staking_contract)
2584
+ service_registry_token_utility_address = staking_params[
2585
+ "service_registry_token_utility"
2586
+ ]
2587
+ service_registry_token_utility = (
2588
+ registry_contracts.service_registry_token_utility.get_instance(
2589
+ ledger_api=ledger_api,
2590
+ contract_address=service_registry_token_utility_address,
2591
+ )
2592
+ )
2593
+
2594
+ agent_bonds = 0
2595
+ for agent_id in agent_ids:
2596
+ num_agent_instances = service_registry.functions.getInstancesForAgentId(
2597
+ service_id, agent_id
2598
+ ).call()[0]
2599
+ agent_bond = service_registry_token_utility.functions.getAgentBond(
2600
+ service_id, agent_id
2601
+ ).call()
2602
+ agent_bonds += num_agent_instances * agent_bond
2603
+
2604
+ if service_state == OnChainState.TERMINATED_BONDED:
2605
+ num_agent_instances = service_info[5]
2606
+ token_bond = service_registry_token_utility.functions.getOperatorBalance(
2607
+ master_safe,
2608
+ service_id,
2609
+ ).call()
2610
+ agent_bonds += num_agent_instances * token_bond
2611
+
2612
+ security_deposit = 0
2613
+ if (
2614
+ OnChainState.ACTIVE_REGISTRATION
2615
+ <= service_state
2616
+ < OnChainState.TERMINATED_BONDED
2617
+ ):
2618
+ security_deposit = (
2619
+ service_registry_token_utility.functions.mapServiceIdTokenDeposit(
2620
+ service_id
2621
+ ).call()[1]
2622
+ )
2623
+
2624
+ bonded_assets[staking_params["staking_token"]] += agent_bonds
2625
+ bonded_assets[staking_params["staking_token"]] += security_deposit
2626
+
2627
+ staking_state = sftxb.staking_status(
2628
+ service_id=service_id,
2629
+ staking_contract=staking_params["staking_contract"],
2630
+ )
2631
+
2632
+ if staking_state in (StakingState.STAKED, StakingState.EVICTED):
2633
+ for token, amount in staking_params["additional_staking_tokens"].items():
2634
+ bonded_assets[token] += amount
2635
+
2636
+ return dict(bonded_assets)
2637
+
2638
+ # TODO deprecate
2639
+ def _compute_protocol_asset_requirements( # pylint: disable=too-many-locals
2640
+ self, service_config_id: str, chain: str
2641
+ ) -> t.Dict:
2642
+ """Computes the protocol asset requirements to deploy on-chain and stake (if necessary)"""
2643
+ service = self.load(service_config_id=service_config_id)
2644
+ chain_config = service.chain_configs[chain]
2645
+ user_params = chain_config.chain_data.user_params
2646
+ ledger_config = chain_config.ledger_config
2647
+ number_of_agents = NUM_LOCAL_AGENT_INSTANCES
2648
+ os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc
2649
+ sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
2650
+ service_asset_requirements: defaultdict = defaultdict(int)
2651
+
2652
+ if not user_params.use_staking or not user_params.staking_program_id:
2653
+ agent_bonds = user_params.cost_of_bond * number_of_agents
2654
+ security_deposit = user_params.cost_of_bond
2655
+ service_asset_requirements[ZERO_ADDRESS] += agent_bonds
2656
+ service_asset_requirements[ZERO_ADDRESS] += security_deposit
2657
+ return dict(service_asset_requirements)
2658
+
2659
+ agent_bonds = 1 * number_of_agents
2660
+ security_deposit = 1
2661
+ service_asset_requirements[ZERO_ADDRESS] += agent_bonds
2662
+ service_asset_requirements[ZERO_ADDRESS] += security_deposit
2663
+
2664
+ staking_params = sftxb.get_staking_params(
2665
+ staking_contract=get_staking_contract(
2666
+ chain=ledger_config.chain,
2667
+ staking_program_id=user_params.staking_program_id,
2668
+ ),
1034
2669
  )
1035
- new_service.keys = old_service.keys
1036
- new_service.chain_data = old_service.chain_data
1037
- new_service.ledger_config = old_service.ledger_config
1038
- new_service.chain_data.on_chain_state = OnChainState.NOTMINTED
1039
- new_service.store()
1040
- old_service.delete()
1041
- return new_service
2670
+
2671
+ # TODO address this comment in FundingManager
2672
+ # This computation assumes the service will be/has been minted with these
2673
+ # parameters. Otherwise, these values should be retrieved on-chain as follows:
2674
+ # - agent_bonds: by combining the output of ServiceRegistry .getAgentParams .getService
2675
+ # and ServiceRegistryTokenUtility .getAgentBond
2676
+ # - security_deposit: as the maximum agent bond.
2677
+ agent_bonds = staking_params["min_staking_deposit"] * number_of_agents
2678
+ security_deposit = staking_params["min_staking_deposit"]
2679
+ service_asset_requirements[staking_params["staking_token"]] += agent_bonds
2680
+ service_asset_requirements[staking_params["staking_token"]] += security_deposit
2681
+
2682
+ for token, amount in staking_params["additional_staking_tokens"].items():
2683
+ service_asset_requirements[token] = amount
2684
+
2685
+ return dict(service_asset_requirements)
2686
+
2687
+ # TODO deprecate
2688
+ @staticmethod
2689
+ def _compute_refill_requirement(
2690
+ asset_funding_values: t.Dict,
2691
+ sender_topup: int = 0,
2692
+ sender_threshold: int = 0,
2693
+ sender_balance: int = 0,
2694
+ ) -> t.Dict:
2695
+ """
2696
+ Compute refill requirement.
2697
+
2698
+ The `asset_funding_values` dictionary specifies the funding obligations the sender must cover for other parties.
2699
+ Additionally, the sender must ensure its own balance remains above `sender_threshold` (minimum required balance)
2700
+ and ideally reaches `sender_topup` (recommended balance). If no funding is required for the sender after covering
2701
+ the obligations for other parties, set `sender_topup = sender_threshold = 0`.
2702
+
2703
+ Args:
2704
+ asset_funding_values (dict): Maps parties (identifiers) to their funding details:
2705
+ - "topup": Recommended funding balance.
2706
+ - "threshold": Minimum required balance.
2707
+ - "balance": Current balance.
2708
+ sender_topup (int): Recommended balance for the sender after meeting obligations.
2709
+ sender_threshold (int): Minimum balance required for the sender after meeting obligations.
2710
+ sender_balance (int): Sender's current balance.
2711
+
2712
+ Returns:
2713
+ dict: A dictionary with:
2714
+ - "minimum_refill": The minimum amount the sender needs to add.
2715
+ - "recommended_refill": The suggested amount the sender should add.
2716
+ """
2717
+ if 0 > sender_threshold or sender_threshold > sender_topup:
2718
+ raise ValueError(
2719
+ f"Arguments must satisfy 0 <= 'sender_threshold' <= 'sender_topup' ({sender_threshold=}, {sender_topup=})."
2720
+ )
2721
+
2722
+ if 0 > sender_balance:
2723
+ raise ValueError(
2724
+ f"Argument 'sender_balance' must be >= 0 ({sender_balance=})."
2725
+ )
2726
+
2727
+ minimum_obligations_shortfall = 0
2728
+ recommended_obligations_shortfall = 0
2729
+
2730
+ for address, requirements in asset_funding_values.items():
2731
+ topup = requirements["topup"]
2732
+ threshold = requirements["threshold"]
2733
+ balance = requirements["balance"]
2734
+
2735
+ if 0 > threshold or threshold > topup:
2736
+ raise ValueError(
2737
+ f"Arguments must satisfy 0 <= 'threshold' <= 'topup' ({address=}, {threshold=}, {topup=}, {balance=})."
2738
+ )
2739
+ if 0 > balance:
2740
+ raise ValueError(
2741
+ f"Argument 'balance' must be >= 0 ({address=}, {balance=})."
2742
+ )
2743
+
2744
+ if balance < threshold:
2745
+ minimum_obligations_shortfall += threshold - balance
2746
+ recommended_obligations_shortfall += topup - balance
2747
+
2748
+ # Compute sender's remaining balance after covering obligations
2749
+ remaining_balance_minimum = sender_balance - minimum_obligations_shortfall
2750
+ remaining_balance_recommended = (
2751
+ sender_balance - recommended_obligations_shortfall
2752
+ )
2753
+
2754
+ # Determine if the sender needs additional refill
2755
+ minimum_refill = 0
2756
+ recommended_refill = 0
2757
+ if remaining_balance_minimum < sender_threshold:
2758
+ minimum_refill = sender_threshold - remaining_balance_minimum
2759
+
2760
+ if remaining_balance_recommended < sender_threshold:
2761
+ recommended_refill = sender_topup - remaining_balance_recommended
2762
+
2763
+ return {
2764
+ "minimum_refill": minimum_refill,
2765
+ "recommended_refill": recommended_refill,
2766
+ }
2767
+
2768
+ # TODO deprecate
2769
+ @staticmethod
2770
+ def get_master_eoa_native_funding_values(
2771
+ master_safe_exists: bool, chain: Chain, balance: int
2772
+ ) -> t.Dict:
2773
+ """Get Master EOA native funding values."""
2774
+
2775
+ topup = DEFAULT_EOA_TOPUPS[chain][ZERO_ADDRESS]
2776
+ threshold = topup / 2 if master_safe_exists else topup
2777
+ return {"topup": topup, "threshold": threshold, "balance": balance}