olas-operate-middleware 0.10.19__py3-none-any.whl → 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/METADATA +3 -1
  2. {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/RECORD +30 -27
  3. operate/bridge/bridge_manager.py +10 -12
  4. operate/bridge/providers/lifi_provider.py +5 -4
  5. operate/bridge/providers/native_bridge_provider.py +6 -5
  6. operate/bridge/providers/provider.py +22 -87
  7. operate/bridge/providers/relay_provider.py +5 -4
  8. operate/cli.py +446 -168
  9. operate/constants.py +22 -2
  10. operate/keys.py +13 -0
  11. operate/ledger/__init__.py +107 -2
  12. operate/ledger/profiles.py +79 -11
  13. operate/operate_types.py +205 -2
  14. operate/quickstart/run_service.py +6 -10
  15. operate/services/agent_runner.py +5 -3
  16. operate/services/deployment_runner.py +3 -0
  17. operate/services/funding_manager.py +904 -0
  18. operate/services/health_checker.py +4 -4
  19. operate/services/manage.py +183 -310
  20. operate/services/protocol.py +392 -140
  21. operate/services/service.py +81 -5
  22. operate/settings.py +70 -0
  23. operate/utils/__init__.py +0 -29
  24. operate/utils/gnosis.py +79 -24
  25. operate/utils/single_instance.py +226 -0
  26. operate/wallet/master.py +221 -181
  27. operate/wallet/wallet_recovery_manager.py +5 -5
  28. {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/WHEEL +0 -0
  29. {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/entry_points.txt +0 -0
  30. {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -19,18 +19,15 @@
19
19
  # ------------------------------------------------------------------------------
20
20
  """Service manager."""
21
21
 
22
- import asyncio
23
22
  import json
24
23
  import logging
25
24
  import os
26
25
  import traceback
27
26
  import typing as t
28
27
  from collections import Counter, defaultdict
29
- from concurrent.futures import ThreadPoolExecutor
30
28
  from contextlib import suppress
31
29
  from http import HTTPStatus
32
30
  from pathlib import Path
33
- from time import time
34
31
 
35
32
  import requests
36
33
  from aea.helpers.base import IPFSHash
@@ -38,14 +35,16 @@ from aea_ledger_ethereum import LedgerApi
38
35
  from autonomy.chain.base import registry_contracts
39
36
  from autonomy.chain.config import CHAIN_PROFILES, ChainType
40
37
  from autonomy.chain.metadata import IPFS_URI_PREFIX
41
- from web3 import Web3
42
38
 
43
39
  from operate.constants import (
44
40
  AGENT_LOG_DIR,
45
41
  AGENT_LOG_ENV_VAR,
46
42
  AGENT_PERSISTENT_STORAGE_DIR,
47
43
  AGENT_PERSISTENT_STORAGE_ENV_VAR,
44
+ DEFAULT_TOPUP_THRESHOLD,
48
45
  IPFS_ADDRESS,
46
+ MIN_AGENT_BOND,
47
+ MIN_SECURITY_DEPOSIT,
49
48
  ZERO_ADDRESS,
50
49
  )
51
50
  from operate.data import DATA_DIR
@@ -53,21 +52,20 @@ from operate.data.contracts.mech_activity.contract import MechActivityContract
53
52
  from operate.data.contracts.requester_activity_checker.contract import (
54
53
  RequesterActivityCheckerContract,
55
54
  )
56
- from operate.data.contracts.staking_token.contract import StakingTokenContract
57
55
  from operate.keys import KeysManager
58
- from operate.ledger import get_currency_denom, get_default_rpc
56
+ from operate.ledger import get_default_rpc
59
57
  from operate.ledger.profiles import (
60
58
  CONTRACTS,
61
- DEFAULT_MASTER_EOA_FUNDS,
59
+ DEFAULT_EOA_THRESHOLD,
60
+ DEFAULT_EOA_TOPUPS,
62
61
  DEFAULT_PRIORITY_MECH,
63
62
  OLAS,
64
- STAKING,
65
- USDC,
66
63
  WRAPPED_NATIVE_ASSET,
67
64
  get_staking_contract,
68
65
  )
69
66
  from operate.operate_types import (
70
67
  Chain,
68
+ ChainAmounts,
71
69
  FundingValues,
72
70
  LedgerConfig,
73
71
  MechMarketplaceConfig,
@@ -75,7 +73,13 @@ from operate.operate_types import (
75
73
  ServiceEnvProvisionType,
76
74
  ServiceTemplate,
77
75
  )
78
- from operate.services.protocol import EthSafeTxBuilder, OnChainManager, StakingState
76
+ from operate.services.funding_manager import FundingManager
77
+ from operate.services.protocol import (
78
+ EthSafeTxBuilder,
79
+ OnChainManager,
80
+ StakingManager,
81
+ StakingState,
82
+ )
79
83
  from operate.services.service import (
80
84
  ChainConfig,
81
85
  Deployment,
@@ -87,14 +91,16 @@ from operate.services.service import (
87
91
  Service,
88
92
  )
89
93
  from operate.services.utils.mech import deploy_mech
90
- from operate.utils.gnosis import drain_eoa, get_asset_balance, get_assets_balances
91
- from operate.utils.gnosis import transfer as transfer_from_safe
92
- from operate.utils.gnosis import transfer_erc20_from_safe
93
- from operate.wallet.master import MasterWalletManager
94
+ from operate.utils.gnosis import (
95
+ get_asset_balance,
96
+ get_assets_balances,
97
+ transfer_erc20_from_safe,
98
+ )
99
+ from operate.wallet.master import InsufficientFundsException, MasterWalletManager
94
100
 
95
101
 
96
102
  # pylint: disable=redefined-builtin
97
- DEFAULT_TOPUP_THRESHOLD = 0.5
103
+
98
104
  # At the moment, we only support running one agent per service locally on a machine.
99
105
  # If multiple agents are provided in the service.yaml file, only the 0th index config will be used.
100
106
  NUM_LOCAL_AGENT_INSTANCES = 1
@@ -107,6 +113,7 @@ class ServiceManager:
107
113
  self,
108
114
  path: Path,
109
115
  wallet_manager: MasterWalletManager,
116
+ funding_manager: FundingManager,
110
117
  logger: logging.Logger,
111
118
  skip_dependency_check: t.Optional[bool] = False,
112
119
  ) -> None:
@@ -121,6 +128,7 @@ class ServiceManager:
121
128
  self.path = path
122
129
  self.keys_manager = KeysManager()
123
130
  self.wallet_manager = wallet_manager
131
+ self.funding_manager = funding_manager
124
132
  self.logger = logger
125
133
  self.skip_depencency_check = skip_dependency_check
126
134
 
@@ -415,18 +423,26 @@ class ServiceManager:
415
423
  and (
416
424
  on_chain_hash != service.hash
417
425
  or current_agent_id != staking_params["agent_ids"][0]
418
- or current_agent_bond != staking_params["min_staking_deposit"]
426
+ or (
427
+ user_params.use_staking
428
+ and current_agent_bond != staking_params["min_staking_deposit"]
429
+ )
430
+ # 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.
419
431
  or on_chain_description != service.description
420
432
  )
421
433
  )
422
434
  current_staking_program = self._get_current_staking_program(service, chain)
423
435
 
436
+ self.logger.info(f"{chain_data.token=}")
437
+ self.logger.info(f"{user_params.use_staking=}")
424
438
  self.logger.info(f"{current_staking_program=}")
425
439
  self.logger.info(f"{user_params.staking_program_id=}")
426
440
  self.logger.info(f"{on_chain_hash=}")
427
441
  self.logger.info(f"{service.hash=}")
428
442
  self.logger.info(f"{current_agent_id=}")
429
- self.logger.info(f"{staking_params['agent_ids'][0]=}")
443
+ self.logger.info(f"{staking_params['agent_ids']=}")
444
+ self.logger.info(f"{current_agent_bond=}")
445
+ self.logger.info(f"{staking_params['min_staking_deposit']=}")
430
446
  self.logger.info(f"{is_first_mint=}")
431
447
  self.logger.info(f"{is_update=}")
432
448
 
@@ -595,7 +611,7 @@ class ServiceManager:
595
611
  ]
596
612
 
597
613
  except Exception as e: # pylint: disable=broad-except
598
- self.logger.error(f"{e}: {traceback.format_exc()}")
614
+ self.logger.debug(f"{e}: {traceback.format_exc()}")
599
615
  self.logger.warning(
600
616
  "Cannot determine type of activity checker contract. Using default parameters. "
601
617
  "NOTE: This will be an exception in the future!"
@@ -815,7 +831,12 @@ class ServiceManager:
815
831
  # on_chain_hash != service.hash or # noqa
816
832
  current_agent_id != target_staking_params["agent_ids"][0]
817
833
  # TODO This has to be removed for Optimus (needs to be properly implemented). Needs to be put back for Trader!
818
- or current_agent_bond != target_staking_params["min_staking_deposit"]
834
+ or (
835
+ user_params.use_staking
836
+ and current_agent_bond
837
+ != target_staking_params["min_staking_deposit"]
838
+ )
839
+ # 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.
819
840
  or current_staking_params["staking_token"]
820
841
  != target_staking_params["staking_token"]
821
842
  or on_chain_description != service.description
@@ -823,12 +844,13 @@ class ServiceManager:
823
844
  )
824
845
 
825
846
  self.logger.info(f"{chain_data.token=}")
847
+ self.logger.info(f"{user_params.use_staking=}")
826
848
  self.logger.info(f"{current_staking_program=}")
827
849
  self.logger.info(f"{user_params.staking_program_id=}")
828
850
  self.logger.info(f"{on_chain_hash=}")
829
851
  self.logger.info(f"{service.hash=}")
830
852
  self.logger.info(f"{current_agent_id=}")
831
- self.logger.info(f"{target_staking_params['agent_ids'][0]=}")
853
+ self.logger.info(f"{target_staking_params['agent_ids']=}")
832
854
  self.logger.info(f"{current_agent_bond=}")
833
855
  self.logger.info(f"{target_staking_params['min_staking_deposit']=}")
834
856
  self.logger.info(f"{is_first_mint=}")
@@ -941,7 +963,6 @@ class ServiceManager:
941
963
  self._get_on_chain_state(service=service, chain=chain)
942
964
  == OnChainState.PRE_REGISTRATION
943
965
  ):
944
- # TODO Verify that this is incorrect: cost_of_bond = staking_params["min_staking_deposit"]
945
966
  cost_of_bond = user_params.cost_of_bond
946
967
  if user_params.use_staking:
947
968
  token_utility = target_staking_params["service_registry_token_utility"]
@@ -978,7 +999,7 @@ class ServiceManager:
978
999
  self.logger.info(
979
1000
  f"Approved {token_utility_allowance} OLAS from {safe} to {token_utility}"
980
1001
  )
981
- cost_of_bond = 1
1002
+ cost_of_bond = MIN_AGENT_BOND
982
1003
 
983
1004
  self.logger.info("Activating service")
984
1005
 
@@ -988,7 +1009,9 @@ class ServiceManager:
988
1009
  address=safe,
989
1010
  )
990
1011
 
991
- if native_balance < cost_of_bond:
1012
+ if (
1013
+ native_balance < cost_of_bond
1014
+ ): # TODO check that this is the security deposit
992
1015
  message = f"Cannot activate service: address {safe} {native_balance=} < {cost_of_bond=}."
993
1016
  self.logger.error(message)
994
1017
  raise ValueError(message)
@@ -1040,7 +1063,7 @@ class ServiceManager:
1040
1063
  self.logger.info(
1041
1064
  f"Approved {token_utility_allowance} OLAS from {safe} to {token_utility}"
1042
1065
  )
1043
- cost_of_bond = 1 * len(service.agent_addresses)
1066
+ cost_of_bond = MIN_AGENT_BOND
1044
1067
 
1045
1068
  self.logger.info(
1046
1069
  f"Registering agent instances: {chain_data.token} -> {service.agent_addresses}"
@@ -1052,7 +1075,7 @@ class ServiceManager:
1052
1075
  address=safe,
1053
1076
  )
1054
1077
 
1055
- if native_balance < cost_of_bond:
1078
+ if native_balance < cost_of_bond * len(service.agent_addresses):
1056
1079
  message = f"Cannot register agent instances: address {safe} {native_balance=} < {cost_of_bond=}."
1057
1080
  self.logger.error(message)
1058
1081
  raise ValueError(message)
@@ -1067,27 +1090,21 @@ class ServiceManager:
1067
1090
  ).settle()
1068
1091
 
1069
1092
  # Deploy service
1093
+ is_initial_funding = False
1070
1094
  if (
1071
1095
  self._get_on_chain_state(service=service, chain=chain)
1072
1096
  == OnChainState.FINISHED_REGISTRATION
1073
1097
  ):
1074
1098
  self.logger.info("Deploying service")
1075
1099
 
1076
- reuse_multisig = True
1077
1100
  info = sftxb.info(token_id=chain_data.token)
1078
1101
  service_safe_address = info["multisig"]
1079
1102
  if service_safe_address == ZERO_ADDRESS:
1080
1103
  reuse_multisig = False
1081
-
1082
- self.logger.info(f"{reuse_multisig=}")
1083
-
1084
- is_recovery_module_enabled = (
1085
- True # Ensure is true for non-deployed multisigs
1086
- )
1087
- if (
1088
- service_safe_address is not None
1089
- and service_safe_address != ZERO_ADDRESS
1090
- ):
1104
+ is_initial_funding = True
1105
+ is_recovery_module_enabled = True
1106
+ else:
1107
+ reuse_multisig = True
1091
1108
  is_recovery_module_enabled = (
1092
1109
  registry_contracts.gnosis_safe.is_module_enabled(
1093
1110
  ledger_api=sftxb.ledger_api,
@@ -1096,6 +1113,7 @@ class ServiceManager:
1096
1113
  ).get("enabled")
1097
1114
  )
1098
1115
 
1116
+ self.logger.info(f"{reuse_multisig=}")
1099
1117
  self.logger.info(f"{is_recovery_module_enabled=}")
1100
1118
 
1101
1119
  messages = sftxb.get_deploy_data_from_safe(
@@ -1114,6 +1132,9 @@ class ServiceManager:
1114
1132
  chain_data.instances = info["instances"]
1115
1133
  chain_data.multisig = info["multisig"]
1116
1134
 
1135
+ if is_initial_funding:
1136
+ self.funding_manager.fund_service_initial(service)
1137
+
1117
1138
  # TODO: yet another agent specific logic for mech, which should be abstracted
1118
1139
  if all(
1119
1140
  var in service.env_variables
@@ -1225,7 +1246,6 @@ class ServiceManager:
1225
1246
  self,
1226
1247
  service_config_id: str,
1227
1248
  chain: str,
1228
- withdrawal_address: t.Optional[str] = None,
1229
1249
  ) -> None:
1230
1250
  """Terminate service on-chain"""
1231
1251
 
@@ -1235,10 +1255,7 @@ class ServiceManager:
1235
1255
  ledger_config = chain_config.ledger_config
1236
1256
  chain_data = chain_config.chain_data
1237
1257
  wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
1238
- safe = wallet.safes[Chain(chain)] # type: ignore
1239
-
1240
- if withdrawal_address:
1241
- withdrawal_address = Web3.to_checksum_address(withdrawal_address)
1258
+ master_safe = wallet.safes[Chain(chain)] # type: ignore
1242
1259
 
1243
1260
  # TODO fixme
1244
1261
  os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc
@@ -1263,23 +1280,23 @@ class ServiceManager:
1263
1280
  )
1264
1281
 
1265
1282
  # Cannot unstake, terminate flow.
1266
- if is_staked and not can_unstake and withdrawal_address is None:
1283
+ if is_staked and not can_unstake:
1267
1284
  self.logger.info("Service cannot be terminated on-chain: cannot unstake.")
1268
1285
  return
1269
-
1270
1286
  # Unstake the service if applies
1271
- if is_staked and (can_unstake or withdrawal_address is not None):
1287
+ if is_staked and can_unstake:
1272
1288
  self.unstake_service_on_chain_from_safe(
1273
1289
  service_config_id=service_config_id,
1274
1290
  chain=chain,
1275
1291
  staking_program_id=current_staking_program,
1276
1292
  )
1293
+ # At least claim the rewards if we cannot unstake yet
1277
1294
  elif is_staked:
1278
- # at least claim the rewards if we cannot unstake yet
1279
1295
  self.claim_on_chain_from_safe(
1280
1296
  service_config_id=service_config_id,
1281
1297
  chain=chain,
1282
1298
  )
1299
+ return
1283
1300
 
1284
1301
  if self._get_on_chain_state(service=service, chain=chain) in (
1285
1302
  OnChainState.ACTIVE_REGISTRATION,
@@ -1309,38 +1326,58 @@ class ServiceManager:
1309
1326
  counter_current_safe_owners = Counter(s.lower() for s in current_safe_owners)
1310
1327
  counter_instances = Counter(s.lower() for s in service.agent_addresses)
1311
1328
 
1312
- if withdrawal_address is not None:
1313
- # we don't drain signer yet, because the owner swapping tx may need to happen
1314
- self.drain_service_safe(
1315
- service_config_id=service_config_id,
1316
- withdrawal_address=withdrawal_address,
1317
- chain=Chain(chain),
1318
- )
1319
-
1320
1329
  if counter_current_safe_owners == counter_instances:
1321
- if withdrawal_address is None:
1322
- self.logger.info("Service funded for safe swap")
1323
- self.fund_service(
1324
- service_config_id=service_config_id,
1325
- funding_values={
1326
- ZERO_ADDRESS: {
1327
- "agent": {
1328
- "topup": chain_data.user_params.fund_requirements[
1329
- ZERO_ADDRESS
1330
- ].agent,
1331
- "threshold": chain_data.user_params.fund_requirements[
1332
- ZERO_ADDRESS
1333
- ].agent,
1334
- },
1335
- "safe": {"topup": 0, "threshold": 0},
1330
+ requirements = ChainAmounts(
1331
+ {
1332
+ chain: {
1333
+ current_safe_owners[0]: {
1334
+ ZERO_ADDRESS: chain_data.user_params.fund_requirements[
1335
+ ZERO_ADDRESS
1336
+ ].agent
1336
1337
  }
1337
- },
1338
+ }
1339
+ }
1340
+ )
1341
+ balances = ChainAmounts(
1342
+ {
1343
+ chain: {
1344
+ current_safe_owners[0]: {
1345
+ ZERO_ADDRESS: get_asset_balance(
1346
+ ledger_api=sftxb.ledger_api,
1347
+ asset_address=ZERO_ADDRESS,
1348
+ address=service.agent_addresses[0],
1349
+ )
1350
+ }
1351
+ }
1352
+ }
1353
+ )
1354
+ if balances < requirements * DEFAULT_EOA_THRESHOLD:
1355
+ self.logger.info("[SERVICE MANAGER] Funding agent EOA for Safe swap.")
1356
+ shortfalls = ChainAmounts.shortfalls(
1357
+ requirements=requirements, balances=balances
1338
1358
  )
1359
+ try:
1360
+ self.funding_manager.fund_chain_amounts(shortfalls)
1361
+ except InsufficientFundsException as e:
1362
+ recovery_module_address = CONTRACTS[Chain(chain)]["recovery_module"]
1363
+ is_recovery_module_enabled = (
1364
+ registry_contracts.gnosis_safe.is_module_enabled(
1365
+ ledger_api=sftxb.ledger_api,
1366
+ contract_address=chain_data.multisig,
1367
+ module_address=recovery_module_address,
1368
+ ).get("enabled")
1369
+ )
1370
+ if is_recovery_module_enabled:
1371
+ self.logger.info(
1372
+ "[SERVICE MANAGER] Could not fund Agent EOA for service swap, but recovery module is enabled."
1373
+ )
1374
+ return
1375
+ raise e
1339
1376
 
1340
1377
  self._enable_recovery_module(
1341
1378
  service_config_id=service_config_id, chain=chain
1342
1379
  )
1343
- self.logger.info("Swapping Safe owners")
1380
+ self.logger.info("[SERVICE MANAGER] Swapping Safe owners")
1344
1381
  owner_crypto = self.keys_manager.get_crypto_instance(
1345
1382
  address=current_safe_owners[0]
1346
1383
  )
@@ -1349,25 +1386,10 @@ class ServiceManager:
1349
1386
  multisig=chain_data.multisig, # TODO this can be read from the registry
1350
1387
  owner_cryptos=[owner_crypto], # TODO allow multiple owners
1351
1388
  new_owner_address=(
1352
- safe if safe else wallet.crypto.address
1389
+ master_safe if master_safe else wallet.crypto.address
1353
1390
  ), # TODO it should always be safe address
1354
1391
  )
1355
1392
 
1356
- if withdrawal_address is not None:
1357
- ethereum_crypto = KeysManager().get_crypto_instance(
1358
- service.agent_addresses[0]
1359
- )
1360
- # drain all native tokens from service signer key
1361
- drain_eoa(
1362
- ledger_api=self.wallet_manager.load(
1363
- ledger_config.chain.ledger_type
1364
- ).ledger_api(chain=ledger_config.chain, rpc=ledger_config.rpc),
1365
- crypto=ethereum_crypto,
1366
- withdrawal_address=withdrawal_address,
1367
- chain_id=ledger_config.chain.id,
1368
- )
1369
- self.logger.info(f"{service.name} signer drained")
1370
-
1371
1393
  def _execute_recovery_module_flow_from_safe( # pylint: disable=too-many-locals
1372
1394
  self,
1373
1395
  service_config_id: str,
@@ -1510,82 +1532,14 @@ class ServiceManager:
1510
1532
  f"Cannot enable recovery module. Safe {service_safe_address} has inconsistent owners."
1511
1533
  )
1512
1534
 
1513
- def _get_current_staking_program(
1535
+ def _get_current_staking_program( # pylint: disable=no-self-use
1514
1536
  self, service: Service, chain: str
1515
1537
  ) -> t.Optional[str]:
1516
- chain_config = service.chain_configs[chain]
1517
- ledger_config = chain_config.ledger_config
1518
- sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
1519
- service_id = chain_config.chain_data.token
1520
- ledger_api = sftxb.ledger_api
1521
-
1522
- if service_id == NON_EXISTENT_TOKEN:
1523
- return None
1524
-
1525
- service_registry = registry_contracts.service_registry.get_instance(
1526
- ledger_api=ledger_api,
1527
- contract_address=CONTRACTS[ledger_config.chain]["service_registry"],
1538
+ staking_manager = StakingManager(Chain(chain))
1539
+ return staking_manager.get_current_staking_program(
1540
+ service_id=service.chain_configs[chain].chain_data.token
1528
1541
  )
1529
1542
 
1530
- service_owner = service_registry.functions.ownerOf(service_id).call()
1531
-
1532
- # TODO Implement in Staking Manager. Implemented here for performance issues.
1533
- staking_ctr = t.cast(
1534
- StakingTokenContract,
1535
- StakingTokenContract.from_dir(
1536
- directory=str(DATA_DIR / "contracts" / "staking_token")
1537
- ),
1538
- )
1539
-
1540
- try:
1541
- state = StakingState(
1542
- staking_ctr.get_instance(
1543
- ledger_api=ledger_api,
1544
- contract_address=service_owner,
1545
- )
1546
- .functions.getStakingState(service_id)
1547
- .call()
1548
- )
1549
- except Exception: # pylint: disable=broad-except
1550
- # Service owner is not a staking contract
1551
-
1552
- # TODO The exception caught here should be ContractLogicError.
1553
- # This exception is typically raised when the contract reverts with
1554
- # a reason string. However, in some cases, the error message
1555
- # does not contain a reason string, which means web3.py raises
1556
- # a generic ValueError instead. It should be properly analyzed
1557
- # what exceptions might be raised by web3.py in this case. To
1558
- # avoid any issues we are simply catching all exceptions.
1559
- return None
1560
-
1561
- if state == StakingState.UNSTAKED:
1562
- return None
1563
-
1564
- for staking_program_id, val in STAKING[ledger_config.chain].items():
1565
- if val == service_owner:
1566
- return staking_program_id
1567
-
1568
- # Fallback, if not possible to determine staking_program_id it means it's an "inner" staking contract
1569
- # (e.g., in the case of DualStakingToken). Loop trough all the known contracts.
1570
- for staking_program_id, staking_program_address in STAKING[
1571
- ledger_config.chain
1572
- ].items():
1573
- state = StakingState(
1574
- staking_ctr.get_instance(
1575
- ledger_api=ledger_api,
1576
- contract_address=staking_program_address,
1577
- )
1578
- .functions.getStakingState(service_id)
1579
- .call()
1580
- )
1581
-
1582
- if state in (StakingState.STAKED, StakingState.EVICTED):
1583
- return staking_program_id
1584
-
1585
- # it's staked, but we don't know which staking program
1586
- # so the staking_program_id should be an arbitrary staking contract
1587
- return service_owner
1588
-
1589
1543
  def unbond_service_on_chain(
1590
1544
  self, service_config_id: str, chain: t.Optional[str] = None
1591
1545
  ) -> None:
@@ -1892,6 +1846,16 @@ class ServiceManager:
1892
1846
  )
1893
1847
  ).settle()
1894
1848
 
1849
+ def claim_all_on_chain_from_safe(self) -> None:
1850
+ """Claim rewards from all services and chains"""
1851
+ self.logger.info("claim_all_on_chain_from_safe")
1852
+ services, _ = self.get_all_services()
1853
+ for service in services:
1854
+ self.claim_on_chain_from_safe(
1855
+ service_config_id=service.service_config_id,
1856
+ chain=service.home_chain,
1857
+ )
1858
+
1895
1859
  def claim_on_chain_from_safe(
1896
1860
  self,
1897
1861
  service_config_id: str,
@@ -1904,6 +1868,14 @@ class ServiceManager:
1904
1868
  ledger_config = chain_config.ledger_config
1905
1869
  wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
1906
1870
  ledger_api = wallet.ledger_api(chain=ledger_config.chain, rpc=ledger_config.rpc)
1871
+
1872
+ if (
1873
+ chain_config.chain_data.token == NON_EXISTENT_TOKEN
1874
+ or chain_config.chain_data.multisig == ZERO_ADDRESS
1875
+ ):
1876
+ self.logger.info("Service is not minted or Safe not deployed.")
1877
+ return 0
1878
+
1907
1879
  self.logger.info(
1908
1880
  f"OLAS Balance on service Safe {chain_config.chain_data.multisig}: "
1909
1881
  f"{get_asset_balance(ledger_api, OLAS[Chain(chain)], chain_config.chain_data.multisig)}"
@@ -1916,10 +1888,11 @@ class ServiceManager:
1916
1888
  staking_program_id=current_staking_program,
1917
1889
  )
1918
1890
  if staking_contract is None:
1919
- raise RuntimeError(
1891
+ self.logger.warning(
1920
1892
  "No staking contract found for the "
1921
1893
  f"{current_staking_program=}. Not claiming the rewards."
1922
1894
  )
1895
+ return 0
1923
1896
 
1924
1897
  sftxb = self.get_eth_safe_tx_builder(ledger_config=ledger_config)
1925
1898
  if not sftxb.staking_rewards_claimable(
@@ -1964,22 +1937,13 @@ class ServiceManager:
1964
1937
  def fund_service( # pylint: disable=too-many-arguments,too-many-locals
1965
1938
  self,
1966
1939
  service_config_id: str,
1967
- funding_values: t.Optional[FundingValues] = None,
1968
- from_safe: bool = True,
1969
- task_id: t.Optional[str] = None,
1940
+ amounts: ChainAmounts,
1970
1941
  ) -> None:
1971
1942
  """Fund service if required."""
1972
1943
  service = self.load(service_config_id=service_config_id)
1944
+ self.funding_manager.fund_service(service=service, amounts=amounts)
1973
1945
 
1974
- for chain in service.chain_configs.keys():
1975
- self.logger.info(f"[FUNDING_JOB] [{task_id=}] Funding {chain=}")
1976
- self.fund_service_single_chain(
1977
- service_config_id=service_config_id,
1978
- funding_values=funding_values,
1979
- from_safe=from_safe,
1980
- chain=chain,
1981
- )
1982
-
1946
+ # TODO deprecate
1983
1947
  def fund_service_single_chain( # pylint: disable=too-many-arguments,too-many-locals,too-many-statements
1984
1948
  self,
1985
1949
  service_config_id: str,
@@ -2012,7 +1976,10 @@ class ServiceManager:
2012
1976
  else:
2013
1977
  on_chain_operations_buffer = (
2014
1978
  chain_data.user_params.cost_of_bond
2015
- * (1 + len(service.agent_addresses))
1979
+ * (
1980
+ MIN_SECURITY_DEPOSIT
1981
+ + MIN_AGENT_BOND * len(service.agent_addresses)
1982
+ )
2016
1983
  )
2017
1984
 
2018
1985
  asset_funding_values = (
@@ -2057,10 +2024,13 @@ class ServiceManager:
2057
2024
  to_transfer = max(
2058
2025
  min(available_balance, target_balance - agent_balance), 0
2059
2026
  )
2027
+ if to_transfer <= 0:
2028
+ continue
2029
+
2060
2030
  self.logger.info(
2061
2031
  f"[FUNDING_JOB] Transferring {to_transfer} units (asset {asset_address}) to agent {agent_address}"
2062
2032
  )
2063
- wallet.transfer_asset(
2033
+ wallet.transfer(
2064
2034
  asset=asset_address,
2065
2035
  to=agent_address,
2066
2036
  amount=int(to_transfer),
@@ -2129,7 +2099,7 @@ class ServiceManager:
2129
2099
  # when not enough funds are present, and the FE doesn't let the user to start the agent.
2130
2100
  # Ideally this error should be allowed, and then the FE should ask the user for more funds.
2131
2101
  with suppress(RuntimeError):
2132
- wallet.transfer_asset(
2102
+ wallet.transfer(
2133
2103
  asset=asset_address,
2134
2104
  to=t.cast(str, chain_data.multisig),
2135
2105
  amount=int(to_transfer),
@@ -2137,6 +2107,7 @@ class ServiceManager:
2137
2107
  rpc=rpc or ledger_config.rpc,
2138
2108
  )
2139
2109
 
2110
+ # TODO Deprecate
2140
2111
  # TODO This method is possibly not used anymore
2141
2112
  def fund_service_erc20( # pylint: disable=too-many-arguments,too-many-locals
2142
2113
  self,
@@ -2174,9 +2145,12 @@ class ServiceManager:
2174
2145
  agent_topup
2175
2146
  or chain_data.user_params.fund_requirements[ZERO_ADDRESS].agent
2176
2147
  )
2148
+ if to_transfer <= 0:
2149
+ continue
2150
+
2177
2151
  self.logger.info(f"Transferring {to_transfer} units to {agent_address}")
2178
- wallet.transfer_erc20(
2179
- token=token,
2152
+ wallet.transfer(
2153
+ asset=token,
2180
2154
  to=agent_address,
2181
2155
  amount=int(to_transfer),
2182
2156
  chain=ledger_config.chain,
@@ -2201,150 +2175,36 @@ class ServiceManager:
2201
2175
  safe_topup
2202
2176
  or chain_data.user_params.fund_requirements[ZERO_ADDRESS].safe
2203
2177
  )
2178
+ if to_transfer <= 0:
2179
+ return
2180
+
2204
2181
  self.logger.info(
2205
2182
  f"Transferring {to_transfer} units to {chain_data.multisig}"
2206
2183
  )
2207
- wallet.transfer_erc20(
2208
- token=token,
2184
+ wallet.transfer(
2185
+ asset=token,
2209
2186
  to=t.cast(str, chain_data.multisig),
2210
2187
  amount=int(to_transfer),
2211
2188
  chain=ledger_config.chain,
2212
2189
  rpc=rpc or ledger_config.rpc,
2213
2190
  )
2214
2191
 
2215
- def drain_service_safe( # pylint: disable=too-many-locals
2216
- self,
2217
- service_config_id: str,
2218
- withdrawal_address: str,
2219
- chain: Chain,
2220
- ) -> None:
2221
- """Drain the funds out of the service safe."""
2222
- self.logger.info(
2223
- f"Draining the safe of service: {service_config_id} on chain {chain.value}"
2224
- )
2225
- service = self.load(service_config_id=service_config_id)
2226
- chain_config = service.chain_configs[chain.value]
2227
- ledger_config = chain_config.ledger_config
2228
- chain_data = chain_config.chain_data
2229
- wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
2230
- ledger_api = wallet.ledger_api(chain=ledger_config.chain, rpc=ledger_config.rpc)
2231
- ethereum_crypto = KeysManager().get_crypto_instance(service.agent_addresses[0])
2232
- withdrawal_address = Web3.to_checksum_address(withdrawal_address)
2233
-
2234
- # drain ERC20 tokens from service safe
2235
- for token_name, token_address in (
2236
- ("OLAS", OLAS[chain]),
2237
- (
2238
- f"W{get_currency_denom(chain)}",
2239
- WRAPPED_NATIVE_ASSET[chain],
2240
- ),
2241
- ("USDC", USDC[chain]),
2242
- ):
2243
- token_instance = registry_contracts.erc20.get_instance(
2244
- ledger_api=ledger_api,
2245
- contract_address=token_address,
2246
- )
2247
- balance = token_instance.functions.balanceOf(chain_data.multisig).call()
2248
- if balance == 0:
2249
- self.logger.info(
2250
- f"No {token_name} to drain from service safe: {chain_data.multisig}"
2251
- )
2252
- continue
2253
-
2254
- self.logger.info(
2255
- f"Draining {balance} {token_name} out of service safe: {chain_data.multisig}"
2256
- )
2257
- transfer_erc20_from_safe(
2258
- ledger_api=ledger_api,
2259
- crypto=ethereum_crypto,
2260
- safe=chain_data.multisig,
2261
- token=token_address,
2262
- to=withdrawal_address,
2263
- amount=balance,
2264
- )
2265
-
2266
- # drain native asset from service safe
2267
- balance = ledger_api.get_balance(chain_data.multisig)
2268
- if balance == 0:
2269
- self.logger.info(
2270
- f"No {get_currency_denom(chain)} to drain from service safe: {chain_data.multisig}"
2271
- )
2272
- else:
2273
- self.logger.info(
2274
- f"Draining {balance} {get_currency_denom(chain)} out of service safe: {chain_data.multisig}"
2275
- )
2276
- transfer_from_safe(
2277
- ledger_api=ledger_api,
2278
- crypto=ethereum_crypto,
2279
- safe=chain_data.multisig,
2280
- to=withdrawal_address,
2281
- amount=balance,
2282
- )
2283
-
2284
- self.logger.info(f"{service.name} safe drained ({service_config_id=})")
2285
-
2286
- async def funding_job(
2287
- self,
2288
- service_config_id: str,
2289
- loop: t.Optional[asyncio.AbstractEventLoop] = None,
2290
- from_safe: bool = True,
2192
+ def drain(
2193
+ self, service_config_id: str, chain_str: str, withdrawal_address: str
2291
2194
  ) -> None:
2292
- """Start a background funding job."""
2293
- loop = loop or asyncio.get_event_loop()
2195
+ """Drain service safe and agent EOAs."""
2294
2196
  service = self.load(service_config_id=service_config_id)
2295
- chain_config = service.chain_configs[service.home_chain]
2296
- task = asyncio.current_task()
2297
- task_id = id(task) if task else "Unknown task_id"
2298
- with ThreadPoolExecutor() as executor:
2299
- last_claim = 0
2300
- while True:
2301
- try:
2302
- await loop.run_in_executor(
2303
- executor,
2304
- self.fund_service,
2305
- service_config_id, # Service id
2306
- {
2307
- asset_address: {
2308
- "agent": {
2309
- "topup": fund_requirements.agent,
2310
- "threshold": int(
2311
- fund_requirements.agent
2312
- * DEFAULT_TOPUP_THRESHOLD
2313
- ),
2314
- },
2315
- "safe": {
2316
- "topup": fund_requirements.safe,
2317
- "threshold": int(
2318
- fund_requirements.safe * DEFAULT_TOPUP_THRESHOLD
2319
- ),
2320
- },
2321
- }
2322
- for asset_address, fund_requirements in chain_config.chain_data.user_params.fund_requirements.items()
2323
- },
2324
- from_safe,
2325
- task_id,
2326
- )
2327
- except Exception: # pylint: disable=broad-except
2328
- logging.info(
2329
- f"Error occured while funding the service\n{traceback.format_exc()}"
2330
- )
2331
-
2332
- # try claiming rewards every hour
2333
- if last_claim + 3600 < time():
2334
- try:
2335
- await loop.run_in_executor(
2336
- executor,
2337
- self.claim_on_chain_from_safe,
2338
- service_config_id,
2339
- service.home_chain,
2340
- )
2341
- except Exception: # pylint: disable=broad-except
2342
- logging.info(
2343
- f"Error occured while claiming rewards\n{traceback.format_exc()}"
2344
- )
2345
- last_claim = time()
2346
-
2347
- await asyncio.sleep(60)
2197
+ chain = Chain(chain_str)
2198
+ self.funding_manager.drain_service_safe(
2199
+ service=service,
2200
+ withdrawal_address=withdrawal_address,
2201
+ chain=chain,
2202
+ )
2203
+ self.funding_manager.drain_agents_eoas(
2204
+ service=service,
2205
+ withdrawal_address=withdrawal_address,
2206
+ chain=chain,
2207
+ )
2348
2208
 
2349
2209
  def deploy_service_locally(
2350
2210
  self,
@@ -2418,6 +2278,14 @@ class ServiceManager:
2418
2278
  )
2419
2279
  return service
2420
2280
 
2281
+ def funding_requirements( # pylint: disable=too-many-locals,too-many-statements,too-many-nested-blocks
2282
+ self, service_config_id: str
2283
+ ) -> t.Dict:
2284
+ """Get the funding requirements for a service."""
2285
+ service = self.load(service_config_id=service_config_id)
2286
+ return self.funding_manager.funding_requirements(service)
2287
+
2288
+ # TODO deprecate
2421
2289
  def refill_requirements( # pylint: disable=too-many-locals,too-many-statements,too-many-nested-blocks
2422
2290
  self, service_config_id: str
2423
2291
  ) -> t.Dict:
@@ -2621,6 +2489,7 @@ class ServiceManager:
2621
2489
  "allow_start_agent": allow_start_agent,
2622
2490
  }
2623
2491
 
2492
+ # TODO deprecate
2624
2493
  def _compute_bonded_assets( # pylint: disable=too-many-locals
2625
2494
  self, service_config_id: str, chain: str
2626
2495
  ) -> t.Dict:
@@ -2736,6 +2605,7 @@ class ServiceManager:
2736
2605
 
2737
2606
  return dict(bonded_assets)
2738
2607
 
2608
+ # TODO deprecate
2739
2609
  def _compute_protocol_asset_requirements( # pylint: disable=too-many-locals
2740
2610
  self, service_config_id: str, chain: str
2741
2611
  ) -> t.Dict:
@@ -2768,6 +2638,7 @@ class ServiceManager:
2768
2638
  ),
2769
2639
  )
2770
2640
 
2641
+ # TODO address this comment in FundingManager
2771
2642
  # This computation assumes the service will be/has been minted with these
2772
2643
  # parameters. Otherwise, these values should be retrieved on-chain as follows:
2773
2644
  # - agent_bonds: by combining the output of ServiceRegistry .getAgentParams .getService
@@ -2783,6 +2654,7 @@ class ServiceManager:
2783
2654
 
2784
2655
  return dict(service_asset_requirements)
2785
2656
 
2657
+ # TODO deprecate
2786
2658
  @staticmethod
2787
2659
  def _compute_refill_requirement(
2788
2660
  asset_funding_values: t.Dict,
@@ -2863,12 +2735,13 @@ class ServiceManager:
2863
2735
  "recommended_refill": recommended_refill,
2864
2736
  }
2865
2737
 
2738
+ # TODO deprecate
2866
2739
  @staticmethod
2867
2740
  def get_master_eoa_native_funding_values(
2868
2741
  master_safe_exists: bool, chain: Chain, balance: int
2869
2742
  ) -> t.Dict:
2870
2743
  """Get Master EOA native funding values."""
2871
2744
 
2872
- topup = DEFAULT_MASTER_EOA_FUNDS[chain][ZERO_ADDRESS]
2745
+ topup = DEFAULT_EOA_TOPUPS[chain][ZERO_ADDRESS]
2873
2746
  threshold = topup / 2 if master_safe_exists else topup
2874
2747
  return {"topup": topup, "threshold": threshold, "balance": balance}