olas-operate-middleware 0.13.5__py3-none-any.whl → 0.13.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: olas-operate-middleware
3
- Version: 0.13.5
3
+ Version: 0.13.7
4
4
  Summary:
5
5
  License-File: LICENSE
6
6
  Author: David Vilela
@@ -21,7 +21,7 @@ Requires-Dist: open-aea-cli-ipfs (>=2.0.6,<3.0.0)
21
21
  Requires-Dist: open-aea-ledger-cosmos (>=2.0.6,<3.0.0)
22
22
  Requires-Dist: open-aea-ledger-ethereum (>=2.0.6,<3.0.0)
23
23
  Requires-Dist: open-aea-ledger-ethereum-flashbots (>=2.0.6,<3.0.0)
24
- Requires-Dist: open-autonomy (>=0.21.4,<0.22.0)
24
+ Requires-Dist: open-autonomy (>=0.21.5,<0.22.0)
25
25
  Requires-Dist: psutil (>=5.9.8,<6.0.0)
26
26
  Requires-Dist: pyinstaller (>=6.8.0,<7.0.0)
27
27
  Requires-Dist: requests-mock (>=1.12.1,<2.0.0)
@@ -78,24 +78,24 @@ operate/resource.py,sha256=LlHJWw66RLIA9TV3HIQ6p7zZSIqoUvn3ntaVs4eRs-g,5803
78
78
  operate/services/__init__.py,sha256=isrThS-Ccu5Sc15JZgkN4uTAVaSg-NwUUSDeTyJEqLk,855
79
79
  operate/services/agent_runner.py,sha256=JGjyrzA5hX4Nuh79h81-dl2hdt74ZkC63t7UsGXY6Rw,7500
80
80
  operate/services/deployment_runner.py,sha256=7A94QpZu100BwIk1Q9Cr0SVK5Sj7nTWx2GRCwr0gvaM,30772
81
- operate/services/funding_manager.py,sha256=S9jYnRQe2m6QDVrkvGS11KFYkbTPrZc0zNygahukHVs,38621
81
+ operate/services/funding_manager.py,sha256=4jcu3KHQOKaaX5AYJsKb-F6GnsSy5fiqC_mWpe7wKVk,41431
82
82
  operate/services/health_checker.py,sha256=dARikrgzU1jEuK4NUqlZ7N0DQq4Ah1ZiRKHmrlh8v-A,11472
83
- operate/services/manage.py,sha256=el0PPSu2g994qQXmaVdjd47Ut9KDhNPJB2B6_i0yoGI,113640
84
- operate/services/protocol.py,sha256=ZRnY-irel11jgVL4G1jVE_sZUi1hQUXEClYsmmvamJg,71134
83
+ operate/services/manage.py,sha256=9dTjXhBq6tgcFWfhU61JQL67L2h3_b_7p8OICimWWB4,113292
84
+ operate/services/protocol.py,sha256=4wAMHoELkqJJlETsMJa-XT8xVA0jfueiaKMn7X4SUKo,71898
85
85
  operate/services/service.py,sha256=WK1RSe83OceETmzpzR22vpWdx1qMkF2J6RSf4P9GTyk,45363
86
86
  operate/services/utils/__init__.py,sha256=TvioaZ1mfTRUSCtrQoLNAp4WMVXyqEJqFJM4PxSQCRU,24
87
87
  operate/services/utils/mech.py,sha256=98gNw8pMNvv_O34V1blr7JUwenqxFeeyFuXLuSYv10w,3864
88
88
  operate/services/utils/tendermint.py,sha256=M4zjF97SOJomhmj97bWKIphnia30lbDie65fs_vy_q8,25686
89
89
  operate/settings.py,sha256=0J2E69-Oplo-Ijy-7rzYHc2Q9Xvct-EUMiEdmKKaYOQ,2353
90
- operate/utils/__init__.py,sha256=EXZ5SQFszLr4qr5oq9bCJ7L4zdjqP6tSCaoOudHyLBQ,5110
91
- operate/utils/gnosis.py,sha256=TYAciQRFxhTdDZIa4Z5XT1SOscrUpK8rnNeVkOV4MiY,19042
92
- operate/utils/single_instance.py,sha256=pmtumg0fFDWWcGzXFXQdLXSW54Zq9qBKgJTEPF6pVW8,9092
90
+ operate/utils/__init__.py,sha256=LD5xScW5ZW9pJeBg1h_9ydj0epPcRXbP0v7OVV85Cl8,8560
91
+ operate/utils/gnosis.py,sha256=mw-K-_xmRnY7QYl6Bzrh-jP7_JwHyiXhvwX6pCMSgt8,18056
92
+ operate/utils/single_instance.py,sha256=1qVibsLoK2m45ip38kRC6IMghaza_kD14D0wAbevcYk,9090
93
93
  operate/utils/ssl.py,sha256=O5DrDoZD4T4qQuHP8GLwWUVxQ-1qXeefGp6uDJiF2lM,4308
94
94
  operate/wallet/__init__.py,sha256=NGiozD3XhvkBi7_FaOWQ8x1thZPK4uGpokJaeDY_o2w,813
95
- operate/wallet/master.py,sha256=zzEODjWOoqKB0XJQU3sAznOdsmvWEzQkaYLzGe2Lx5o,33515
95
+ operate/wallet/master.py,sha256=5UvfEDFjgHNOHC25ZhZeewte1kXr69YPs5ue1NYSe5Q,33751
96
96
  operate/wallet/wallet_recovery_manager.py,sha256=Dn0QmOYmmNj4p1X1TVQOrua3fysR3XJ99m6Z3H7tJQI,19792
97
- olas_operate_middleware-0.13.5.dist-info/METADATA,sha256=yUHWZKpOlfCabvulzX5z9b7F_GWbeyWZALVpzp_qTtE,1492
98
- olas_operate_middleware-0.13.5.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
99
- olas_operate_middleware-0.13.5.dist-info/entry_points.txt,sha256=dM1g2I7ODApKQFcgl5J4NGA7pfBTo6qsUTXM-j2OLlw,44
100
- olas_operate_middleware-0.13.5.dist-info/licenses/LICENSE,sha256=mdBDB-mWKV5Cz4ejBzBiKqan6Z8zVLAh9xwM64O2FW4,11339
101
- olas_operate_middleware-0.13.5.dist-info/RECORD,,
97
+ olas_operate_middleware-0.13.7.dist-info/METADATA,sha256=GJWEmdM2H2PxAekMBtr3__CW9lmHmrjpVAidOdcIXy4,1492
98
+ olas_operate_middleware-0.13.7.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
99
+ olas_operate_middleware-0.13.7.dist-info/entry_points.txt,sha256=dM1g2I7ODApKQFcgl5J4NGA7pfBTo6qsUTXM-j2OLlw,44
100
+ olas_operate_middleware-0.13.7.dist-info/licenses/LICENSE,sha256=mdBDB-mWKV5Cz4ejBzBiKqan6Z8zVLAh9xwM64O2FW4,11339
101
+ olas_operate_middleware-0.13.7.dist-info/RECORD,,
@@ -60,6 +60,7 @@ from operate.ledger.profiles import (
60
60
  from operate.operate_types import Chain, ChainAmounts, LedgerType, OnChainState
61
61
  from operate.services.protocol import EthSafeTxBuilder, StakingManager, StakingState
62
62
  from operate.services.service import NON_EXISTENT_TOKEN, Service
63
+ from operate.utils import concurrent_execute
63
64
  from operate.utils.gnosis import drain_eoa, get_asset_balance, get_owners
64
65
  from operate.utils.gnosis import transfer as transfer_from_safe
65
66
  from operate.utils.gnosis import transfer_erc20_from_safe
@@ -344,13 +345,27 @@ class FundingManager:
344
345
 
345
346
  # os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc # TODO do we need this?
346
347
 
347
- # Determine bonded native amount
348
- service_registry_address = CHAIN_PROFILES[chain]["service_registry"]
348
+ # Fetch on-chain data
349
349
  service_registry = registry_contracts.service_registry.get_instance(
350
350
  ledger_api=ledger_api,
351
- contract_address=service_registry_address,
351
+ contract_address=CHAIN_PROFILES[chain]["service_registry"],
352
+ )
353
+ (
354
+ service_info,
355
+ operator_balance,
356
+ current_staking_program,
357
+ ) = concurrent_execute(
358
+ (service_registry.functions.getService(service_id).call, ()),
359
+ (
360
+ service_registry.functions.getOperatorBalance(
361
+ master_safe, service_id
362
+ ).call,
363
+ (),
364
+ ),
365
+ (staking_manager.get_current_staking_program, (service_id,)),
352
366
  )
353
- service_info = service_registry.functions.getService(service_id).call()
367
+
368
+ # Determine bonded native amount
354
369
  security_deposit = service_info[0]
355
370
  service_state = service_info[6]
356
371
  agent_ids = service_info[7]
@@ -362,15 +377,9 @@ class FundingManager:
362
377
  ):
363
378
  bonded_assets[ZERO_ADDRESS] += security_deposit
364
379
 
365
- operator_balance = service_registry.functions.getOperatorBalance(
366
- master_safe, service_id
367
- ).call()
368
380
  bonded_assets[ZERO_ADDRESS] += operator_balance
369
381
 
370
382
  # Determine bonded token amount for staking programs
371
- current_staking_program = staking_manager.get_current_staking_program(
372
- service_id=service_id,
373
- )
374
383
  target_staking_program = user_params.staking_program_id
375
384
  staking_contract = staking_manager.get_staking_contract(
376
385
  staking_program_id=current_staking_program or target_staking_program,
@@ -381,9 +390,7 @@ class FundingManager:
381
390
 
382
391
  staking_manager = StakingManager(Chain(chain))
383
392
  staking_params = staking_manager.get_staking_params(
384
- staking_contract=staking_manager.get_staking_contract(
385
- staking_program_id=user_params.staking_program_id,
386
- ),
393
+ staking_contract=staking_contract,
387
394
  )
388
395
 
389
396
  service_registry_token_utility_address = staking_params[
@@ -396,24 +403,62 @@ class FundingManager:
396
403
  )
397
404
  )
398
405
 
406
+ (
407
+ *agent_instances_and_bonds,
408
+ token_bond,
409
+ security_deposits,
410
+ staking_state,
411
+ ) = concurrent_execute(
412
+ *(
413
+ [
414
+ (
415
+ service_registry.functions.getInstancesForAgentId(
416
+ service_id, agent_id
417
+ ).call,
418
+ (),
419
+ )
420
+ for agent_id in agent_ids
421
+ ]
422
+ + [
423
+ (
424
+ service_registry_token_utility.functions.getAgentBond(
425
+ service_id, agent_id
426
+ ).call,
427
+ (),
428
+ )
429
+ for agent_id in agent_ids
430
+ ]
431
+ + [
432
+ (
433
+ service_registry_token_utility.functions.getOperatorBalance(
434
+ master_safe, service_id
435
+ ).call,
436
+ (),
437
+ ),
438
+ (
439
+ service_registry_token_utility.functions.mapServiceIdTokenDeposit(
440
+ service_id
441
+ ).call,
442
+ (),
443
+ ),
444
+ (
445
+ staking_manager.staking_state,
446
+ (service_id, staking_params["staking_contract"]),
447
+ ),
448
+ ]
449
+ ),
450
+ )
451
+
399
452
  agent_bonds = 0
400
- for agent_id in agent_ids:
401
- num_agent_instances = service_registry.functions.getInstancesForAgentId(
402
- service_id, agent_id
403
- ).call()[0]
404
- agent_bond = service_registry_token_utility.functions.getAgentBond(
405
- service_id, agent_id
406
- ).call()
453
+ for agent_instances, agent_bond in zip(
454
+ agent_instances_and_bonds[: len(agent_ids)],
455
+ agent_instances_and_bonds[len(agent_ids) :],
456
+ ):
457
+ num_agent_instances = agent_instances[0]
407
458
  agent_bonds += num_agent_instances * agent_bond
408
459
 
409
460
  if service_state == OnChainState.TERMINATED_BONDED:
410
461
  num_agent_instances = service_info[5]
411
- token_bond = (
412
- service_registry_token_utility.functions.getOperatorBalance(
413
- master_safe,
414
- service_id,
415
- ).call()
416
- )
417
462
  agent_bonds += num_agent_instances * token_bond
418
463
 
419
464
  security_deposit = 0
@@ -422,20 +467,11 @@ class FundingManager:
422
467
  <= service_state
423
468
  < OnChainState.TERMINATED_BONDED
424
469
  ):
425
- security_deposit = (
426
- service_registry_token_utility.functions.mapServiceIdTokenDeposit(
427
- service_id
428
- ).call()[1]
429
- )
470
+ security_deposit = security_deposits[1]
430
471
 
431
472
  bonded_assets[staking_params["staking_token"]] += agent_bonds
432
473
  bonded_assets[staking_params["staking_token"]] += security_deposit
433
474
 
434
- staking_state = staking_manager.staking_state(
435
- service_id=service_id,
436
- staking_contract=staking_params["staking_contract"],
437
- )
438
-
439
475
  if staking_state in (StakingState.STAKED, StakingState.EVICTED):
440
476
  for token, amount in staking_params[
441
477
  "additional_staking_tokens"
@@ -564,40 +600,64 @@ class FundingManager:
564
600
 
565
601
  def _get_master_safe_balances(self, thresholds: ChainAmounts) -> ChainAmounts:
566
602
  output = ChainAmounts()
603
+ batch_calls_args = {}
567
604
  for chain_str, addresses in thresholds.items():
568
605
  chain = Chain(chain_str)
569
606
  master_safe = self._resolve_master_safe(chain)
570
- master_safe_dict = output.setdefault(chain_str, {}).setdefault(
571
- master_safe, {}
572
- )
607
+ output.setdefault(chain_str, {}).setdefault(master_safe, {})
573
608
  for _, assets in addresses.items():
574
609
  for asset, _ in assets.items():
575
- master_safe_dict[asset] = get_asset_balance(
576
- ledger_api=get_default_ledger_api(chain),
577
- asset_address=asset,
578
- address=master_safe,
579
- raise_on_invalid_address=False,
610
+ batch_calls_args[
611
+ (
612
+ get_default_ledger_api(chain),
613
+ asset,
614
+ master_safe,
615
+ False,
616
+ )
617
+ ] = (
618
+ chain_str,
619
+ master_safe,
620
+ asset,
580
621
  )
581
622
 
623
+ batch_calls_results = concurrent_execute(
624
+ *[(get_asset_balance, args) for args in batch_calls_args.keys()]
625
+ )
626
+ for args, balance in zip(batch_calls_args.keys(), batch_calls_results):
627
+ chain_str, master_safe, asset = batch_calls_args[args]
628
+ output[chain_str][master_safe][asset] = balance
629
+
582
630
  return output
583
631
 
584
632
  def _get_master_eoa_balances(self, thresholds: ChainAmounts) -> ChainAmounts:
585
633
  output = ChainAmounts()
634
+ batch_calls_args = {}
586
635
  for chain_str, addresses in thresholds.items():
587
636
  chain = Chain(chain_str)
588
637
  master_eoa = self._resolve_master_eoa(chain)
589
- master_eoa_dict = output.setdefault(chain_str, {}).setdefault(
590
- master_eoa, {}
591
- )
638
+ output.setdefault(chain_str, {}).setdefault(master_eoa, {})
592
639
  for _, assets in addresses.items():
593
640
  for asset, _ in assets.items():
594
- master_eoa_dict[asset] = get_asset_balance(
595
- ledger_api=get_default_ledger_api(chain),
596
- asset_address=asset,
597
- address=master_eoa,
598
- raise_on_invalid_address=False,
641
+ batch_calls_args[
642
+ (
643
+ get_default_ledger_api(chain),
644
+ asset,
645
+ master_eoa,
646
+ False,
647
+ )
648
+ ] = (
649
+ chain_str,
650
+ master_eoa,
651
+ asset,
599
652
  )
600
653
 
654
+ batch_calls_results = concurrent_execute(
655
+ *[(get_asset_balance, args) for args in batch_calls_args.keys()]
656
+ )
657
+ for args, balance in zip(batch_calls_args.keys(), batch_calls_results):
658
+ chain_str, master_eoa, asset = batch_calls_args[args]
659
+ output[chain_str][master_eoa][asset] = balance
660
+
601
661
  return output
602
662
 
603
663
  def fund_master_eoa(self) -> None:
@@ -623,12 +683,30 @@ class FundingManager:
623
683
  }
624
684
  )
625
685
  master_eoa_balances = self._get_master_eoa_balances(master_eoa_topups)
686
+ master_safe_balance = self._get_master_safe_balances(master_eoa_topups)
626
687
  master_eoa_shortfalls = self._compute_shortfalls(
627
688
  balances=master_eoa_balances,
628
689
  thresholds=master_eoa_topups * DEFAULT_EOA_THRESHOLD,
629
690
  topups=master_eoa_topups,
630
691
  )
631
- self.fund_chain_amounts(master_eoa_shortfalls)
692
+ possible_to_fund_shortfalls = ChainAmounts(
693
+ {
694
+ chain_str: {
695
+ address: {
696
+ asset: min(
697
+ amount,
698
+ master_safe_balance.get(chain_str, {})
699
+ .get(self._resolve_master_safe(Chain(chain_str)), {})
700
+ .get(asset, 0),
701
+ )
702
+ for asset, amount in assets.items()
703
+ }
704
+ for address, assets in addresses.items()
705
+ }
706
+ for chain_str, addresses in master_eoa_shortfalls.items()
707
+ }
708
+ )
709
+ self.fund_chain_amounts(possible_to_fund_shortfalls)
632
710
 
633
711
  def funding_requirements(self, service: Service) -> t.Dict:
634
712
  """Funding requirements"""
@@ -639,9 +717,13 @@ class FundingManager:
639
717
  total_requirements: ChainAmounts
640
718
  chains = [Chain(chain_str) for chain_str in service.chain_configs.keys()]
641
719
 
642
- # Protocol shortfall
643
- protocol_thresholds = self._compute_protocol_asset_requirements(service)
644
- protocol_balances = self._compute_protocol_bonded_assets(service)
720
+ (
721
+ protocol_thresholds,
722
+ protocol_balances,
723
+ ) = concurrent_execute(
724
+ (self._compute_protocol_asset_requirements, (service,)),
725
+ (self._compute_protocol_bonded_assets, (service,)),
726
+ )
645
727
  protocol_topups = protocol_thresholds
646
728
  protocol_shortfalls = self._compute_shortfalls(
647
729
  balances=protocol_balances,
@@ -22,7 +22,6 @@
22
22
  import json
23
23
  import logging
24
24
  import os
25
- import time
26
25
  import traceback
27
26
  import typing as t
28
27
  from collections import Counter, defaultdict
@@ -967,9 +966,6 @@ class ServiceManager:
967
966
  chain_data.token = event_data["args"]["serviceId"]
968
967
  service.store()
969
968
 
970
- if is_first_mint: # Hotfix to prevent RPC out-of-sync issues
971
- time.sleep(RPC_SYNC_TIMEOUT)
972
-
973
969
  # Activate service
974
970
  if (
975
971
  self._get_on_chain_state(service=service, chain=chain)
@@ -1035,9 +1031,6 @@ class ServiceManager:
1035
1031
  )
1036
1032
  ).settle()
1037
1033
 
1038
- if is_first_mint: # Hotfix to prevent RPC out-of-sync issues
1039
- time.sleep(RPC_SYNC_TIMEOUT)
1040
-
1041
1034
  # Register agent instances
1042
1035
  if (
1043
1036
  self._get_on_chain_state(service=service, chain=chain)
@@ -1105,9 +1098,6 @@ class ServiceManager:
1105
1098
  )
1106
1099
  ).settle()
1107
1100
 
1108
- if is_first_mint: # Hotfix to prevent RPC out-of-sync issues
1109
- time.sleep(RPC_SYNC_TIMEOUT)
1110
-
1111
1101
  # Deploy service
1112
1102
  is_initial_funding = False
1113
1103
  if (
@@ -80,6 +80,7 @@ from operate.ledger.profiles import CONTRACTS, STAKING
80
80
  from operate.operate_types import Chain as OperateChain
81
81
  from operate.operate_types import ContractAddresses
82
82
  from operate.services.service import NON_EXISTENT_TOKEN
83
+ from operate.utils import concurrent_execute
83
84
  from operate.utils.gnosis import (
84
85
  MultiSendOperation,
85
86
  SafeOperation,
@@ -226,37 +227,20 @@ class StakingManager:
226
227
  def _get_staking_params(chain: OperateChain, staking_contract: str) -> t.Dict:
227
228
  """Get staking params"""
228
229
  ledger_api = get_default_ledger_api(chain=chain)
229
- instance = StakingManager.staking_ctr.get_instance(
230
- ledger_api=ledger_api,
231
- contract_address=staking_contract,
230
+
231
+ second_token_func = (
232
+ lambda: None # pylint: disable=unnecessary-lambda-assignment # noqa: E731
232
233
  )
233
- agent_ids = instance.functions.getAgentIds().call()
234
- service_registry = instance.functions.serviceRegistry().call()
235
- staking_token = instance.functions.stakingToken().call()
236
- service_registry_token_utility = (
237
- instance.functions.serviceRegistryTokenUtility().call()
234
+ second_token_amount_func = (
235
+ lambda: None # pylint: disable=unnecessary-lambda-assignment # noqa: E731
238
236
  )
239
- min_staking_deposit = instance.functions.minStakingDeposit().call()
240
- activity_checker = instance.functions.activityChecker().call()
241
-
242
- output = {
243
- "staking_contract": staking_contract,
244
- "agent_ids": agent_ids,
245
- "service_registry": service_registry,
246
- "staking_token": staking_token,
247
- "service_registry_token_utility": service_registry_token_utility,
248
- "min_staking_deposit": min_staking_deposit,
249
- "activity_checker": activity_checker,
250
- "additional_staking_tokens": {},
251
- }
252
237
  try:
253
238
  instance = StakingManager.dual_staking_ctr.get_instance(
254
239
  ledger_api=ledger_api,
255
240
  contract_address=staking_contract,
256
241
  )
257
- output["additional_staking_tokens"][
258
- instance.functions.secondToken().call()
259
- ] = instance.functions.secondTokenAmount().call()
242
+ second_token_func = instance.functions.secondToken().call
243
+ second_token_amount_func = instance.functions.secondTokenAmount().call
260
244
  except Exception: # pylint: disable=broad-except # nosec
261
245
  # Contract is not a dual staking contract
262
246
 
@@ -269,6 +253,45 @@ class StakingManager:
269
253
  # avoid any issues we are simply catching all exceptions.
270
254
  pass
271
255
 
256
+ instance = StakingManager.staking_ctr.get_instance(
257
+ ledger_api=ledger_api,
258
+ contract_address=staking_contract,
259
+ )
260
+ (
261
+ agent_ids,
262
+ service_registry,
263
+ staking_token,
264
+ service_registry_token_utility,
265
+ min_staking_deposit,
266
+ activity_checker,
267
+ second_token,
268
+ second_token_amount,
269
+ ) = concurrent_execute(
270
+ (instance.functions.getAgentIds().call, ()),
271
+ (instance.functions.serviceRegistry().call, ()),
272
+ (instance.functions.stakingToken().call, ()),
273
+ (instance.functions.serviceRegistryTokenUtility().call, ()),
274
+ (instance.functions.minStakingDeposit().call, ()),
275
+ (instance.functions.activityChecker().call, ()),
276
+ (second_token_func, ()),
277
+ (second_token_amount_func, ()),
278
+ ignore_exceptions=True,
279
+ )
280
+
281
+ output = {
282
+ "staking_contract": staking_contract,
283
+ "agent_ids": agent_ids,
284
+ "service_registry": service_registry,
285
+ "staking_token": staking_token,
286
+ "service_registry_token_utility": service_registry_token_utility,
287
+ "min_staking_deposit": min_staking_deposit,
288
+ "activity_checker": activity_checker,
289
+ "additional_staking_tokens": (
290
+ {second_token: second_token_amount}
291
+ if second_token and second_token_amount
292
+ else {}
293
+ ),
294
+ }
272
295
  return output
273
296
 
274
297
  def get_staking_params(self, staking_contract: str) -> t.Dict:
operate/utils/__init__.py CHANGED
@@ -19,14 +19,25 @@
19
19
 
20
20
  """Helper utilities."""
21
21
 
22
+ import asyncio
23
+ import inspect
24
+ import logging
22
25
  import os
23
26
  import platform
24
27
  import shutil
25
28
  import time
26
29
  import typing as t
30
+ from concurrent.futures import ThreadPoolExecutor
31
+ from concurrent.futures import TimeoutError as FuturesTimeoutError
32
+ from contextlib import contextmanager
27
33
  from pathlib import Path
28
34
  from threading import Lock
29
35
 
36
+ from operate.constants import DEFAULT_TIMEOUT
37
+
38
+
39
+ logger = logging.getLogger(__name__)
40
+
30
41
 
31
42
  class SingletonMeta(type):
32
43
  """A metaclass for creating thread-safe singleton classes."""
@@ -153,3 +164,100 @@ def unrecoverable_delete(file_path: Path, passes: int = 3) -> None:
153
164
  print(f"Permission denied to securely delete file '{file_path}'.")
154
165
  except Exception as e: # pylint: disable=broad-except
155
166
  print(f"Error during secure deletion of '{file_path}': {e}")
167
+
168
+
169
+ @contextmanager
170
+ def timing_context(label: str = "Block") -> t.Generator[None, None, None]:
171
+ """Context manager for timing a code block."""
172
+ start = time.perf_counter()
173
+ try:
174
+ yield
175
+ finally:
176
+ end = time.perf_counter()
177
+ logger.debug(f"[{label}] Elapsed time: {end - start:.4f} seconds")
178
+
179
+
180
+ def concurrent_execute(
181
+ *func_calls: t.Tuple[t.Callable, t.Tuple],
182
+ ignore_exceptions: bool = False,
183
+ ) -> t.List[t.Any]:
184
+ """Execute callables concurrently.
185
+
186
+ This is a synchronous convenience wrapper around `parallel_execute_async`.
187
+ If called from within an active asyncio event loop, use
188
+ `await parallel_execute_async(...)` instead.
189
+ """
190
+
191
+ async def _runner() -> t.List[t.Any]:
192
+ return await concurrent_execute_async(
193
+ *func_calls,
194
+ ignore_exceptions=ignore_exceptions,
195
+ )
196
+
197
+ try:
198
+ asyncio.get_running_loop()
199
+ except RuntimeError:
200
+ # No running loop in this thread.
201
+ return asyncio.run(_runner())
202
+
203
+ # Running inside an event loop thread.
204
+ # We cannot call `asyncio.run` here, so offload to a background thread.
205
+ # NOTE: this blocks the current thread until completion.
206
+ with ThreadPoolExecutor(max_workers=1) as executor:
207
+ future = executor.submit(asyncio.run, _runner())
208
+ return future.result()
209
+
210
+
211
+ async def concurrent_execute_async(
212
+ *func_calls: t.Tuple[t.Callable, t.Tuple],
213
+ ignore_exceptions: bool = False,
214
+ ) -> t.List[t.Any]:
215
+ """Execute callables concurrently using asyncio.
216
+
217
+ - Async callables are awaited directly.
218
+ - Sync callables are executed via `asyncio.to_thread`.
219
+
220
+ Results are returned in the same order as `funcs`/`args_list`.
221
+ """
222
+
223
+ async def _invoke(func: t.Callable, args: t.Tuple) -> t.Any:
224
+ with timing_context(f"Executing {func.__name__}"):
225
+ if inspect.iscoroutinefunction(func):
226
+ return await t.cast(t.Awaitable[t.Any], func(*args))
227
+ return await asyncio.to_thread(func, *args)
228
+
229
+ results: t.List[t.Any] = [None] * len(func_calls)
230
+
231
+ async def _invoke_indexed(
232
+ idx: int, func: t.Callable, args: t.Tuple
233
+ ) -> t.Tuple[int, t.Any]:
234
+ try:
235
+ return idx, await _invoke(func, args)
236
+ except Exception as e: # pylint: disable=broad-except
237
+ return idx, e
238
+
239
+ tasks: t.List[asyncio.Task] = [
240
+ asyncio.create_task(_invoke_indexed(idx, func, args))
241
+ for idx, (func, args) in enumerate(func_calls)
242
+ ]
243
+
244
+ try:
245
+ for task in asyncio.as_completed(tasks, timeout=DEFAULT_TIMEOUT):
246
+ idx, outcome = await task
247
+ if isinstance(outcome, BaseException):
248
+ if ignore_exceptions:
249
+ results[idx] = None
250
+ else:
251
+ raise outcome
252
+ else:
253
+ results[idx] = outcome
254
+ except asyncio.TimeoutError as e:
255
+ raise FuturesTimeoutError() from e
256
+ finally:
257
+ # Ensure we don't leak pending tasks.
258
+ for task in tasks:
259
+ if not task.done():
260
+ task.cancel()
261
+ await asyncio.gather(*tasks, return_exceptions=True)
262
+
263
+ return results
operate/utils/gnosis.py CHANGED
@@ -22,7 +22,6 @@
22
22
  import binascii
23
23
  import itertools
24
24
  import secrets
25
- import time
26
25
  import typing as t
27
26
  from enum import Enum
28
27
 
@@ -164,7 +163,6 @@ def _get_nonce() -> int:
164
163
  def create_safe(
165
164
  ledger_api: LedgerApi,
166
165
  crypto: Crypto,
167
- backup_owner: t.Optional[str] = None,
168
166
  salt_nonce: t.Optional[int] = None,
169
167
  ) -> t.Tuple[str, int, str]:
170
168
  """Create gnosis safe."""
@@ -202,29 +200,6 @@ def create_safe(
202
200
  event_name="ProxyCreation",
203
201
  )
204
202
  safe_address = event["args"]["proxy"]
205
-
206
- if backup_owner is not None:
207
- retry_delays = [0, 60, 120, 180, 240]
208
- for attempt in range(1, len(retry_delays) + 1):
209
- try:
210
- add_owner(
211
- ledger_api=ledger_api,
212
- crypto=crypto,
213
- safe=safe_address,
214
- owner=backup_owner,
215
- )
216
- break # success
217
- except Exception as e: # pylint: disable=broad-except
218
- if attempt == len(retry_delays):
219
- raise RuntimeError(
220
- f"Failed to add backup owner {backup_owner} after {len(retry_delays)} attempts: {e}"
221
- ) from e
222
- next_delay = retry_delays[attempt]
223
- logger.error(
224
- f"Retry add owner {attempt}/{len(retry_delays)} in {next_delay} seconds due to error: {e}"
225
- )
226
- time.sleep(next_delay)
227
-
228
203
  return safe_address, salt_nonce, tx_settler.tx_hash
229
204
 
230
205
 
@@ -36,9 +36,9 @@ class AppSingleInstance:
36
36
 
37
37
  host = "127.0.0.1"
38
38
  after_kill_sleep_time = 1
39
- proc_kill_wait_timeout = 10
40
- proc_terminate_wait_timeout = 10
41
- http_request_timeout = 3
39
+ proc_kill_wait_timeout = 3
40
+ proc_terminate_wait_timeout = 3
41
+ http_request_timeout = 1
42
42
 
43
43
  def __init__(self, port_number: int, shutdown_endpoint: str = "/shutdown") -> None:
44
44
  """Initialize the AppSingleInstance manager."""
operate/wallet/master.py CHANGED
@@ -675,20 +675,29 @@ class EthereumMasterWallet(MasterWallet):
675
675
  rpc: t.Optional[str] = None,
676
676
  ) -> t.Optional[str]:
677
677
  """Create safe."""
678
- if chain in self.safes:
679
- raise ValueError(f"Wallet already has a Safe on chain {chain}.")
680
-
681
- safe, self.safe_nonce, tx_hash = create_gnosis_safe(
682
- ledger_api=self.ledger_api(chain=chain, rpc=rpc),
683
- crypto=self.crypto,
684
- backup_owner=backup_owner,
685
- salt_nonce=self.safe_nonce,
686
- )
687
- self.safe_chains.append(chain)
678
+ tx_hash = None
679
+ ledger_api = self.ledger_api(chain=chain, rpc=rpc)
688
680
  if self.safes is None:
689
681
  self.safes = {}
690
- self.safes[chain] = safe
691
- self.store()
682
+
683
+ if chain not in self.safe_chains and chain not in self.safes:
684
+ safe, self.safe_nonce, tx_hash = create_gnosis_safe(
685
+ ledger_api=ledger_api,
686
+ crypto=self.crypto,
687
+ salt_nonce=self.safe_nonce,
688
+ )
689
+ self.safe_chains.append(chain)
690
+ self.safes[chain] = safe
691
+ self.store()
692
+
693
+ if backup_owner is not None:
694
+ add_owner(
695
+ ledger_api=ledger_api,
696
+ crypto=self.crypto,
697
+ safe=self.safes[chain],
698
+ owner=backup_owner,
699
+ )
700
+
692
701
  return tx_hash
693
702
 
694
703
  def update_backup_owner(