olas-operate-middleware 0.8.2__py3-none-any.whl → 0.10.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 (34) hide show
  1. {olas_operate_middleware-0.8.2.dist-info → olas_operate_middleware-0.10.0.dist-info}/METADATA +2 -2
  2. {olas_operate_middleware-0.8.2.dist-info → olas_operate_middleware-0.10.0.dist-info}/RECORD +34 -34
  3. operate/bridge/bridge_manager.py +5 -6
  4. operate/bridge/providers/native_bridge_provider.py +1 -1
  5. operate/bridge/providers/provider.py +4 -5
  6. operate/bridge/providers/relay_provider.py +1 -1
  7. operate/cli.py +128 -48
  8. operate/constants.py +9 -9
  9. operate/keys.py +26 -14
  10. operate/ledger/__init__.py +4 -4
  11. operate/ledger/profiles.py +9 -11
  12. operate/migration.py +326 -0
  13. operate/operate_types.py +9 -27
  14. operate/quickstart/analyse_logs.py +3 -6
  15. operate/quickstart/claim_staking_rewards.py +1 -4
  16. operate/quickstart/reset_configs.py +0 -3
  17. operate/quickstart/reset_password.py +0 -3
  18. operate/quickstart/reset_staking.py +3 -5
  19. operate/quickstart/run_service.py +5 -7
  20. operate/quickstart/stop_service.py +3 -4
  21. operate/quickstart/terminate_on_chain_service.py +1 -4
  22. operate/quickstart/utils.py +4 -7
  23. operate/resource.py +37 -5
  24. operate/services/deployment_runner.py +170 -38
  25. operate/services/health_checker.py +5 -8
  26. operate/services/manage.py +103 -164
  27. operate/services/protocol.py +5 -5
  28. operate/services/service.py +42 -242
  29. operate/utils/__init__.py +44 -0
  30. operate/utils/gnosis.py +25 -17
  31. operate/wallet/master.py +20 -24
  32. {olas_operate_middleware-0.8.2.dist-info → olas_operate_middleware-0.10.0.dist-info}/LICENSE +0 -0
  33. {olas_operate_middleware-0.8.2.dist-info → olas_operate_middleware-0.10.0.dist-info}/WHEEL +0 -0
  34. {olas_operate_middleware-0.8.2.dist-info → olas_operate_middleware-0.10.0.dist-info}/entry_points.txt +0 -0
@@ -45,7 +45,9 @@ from aea.configurations.constants import (
45
45
  from aea.helpers.yaml_utils import yaml_dump, yaml_load, yaml_load_all
46
46
  from aea_cli_ipfs.ipfs_utils import IPFSTool
47
47
  from autonomy.cli.helpers.deployment import run_deployment, stop_deployment
48
+ from autonomy.configurations.constants import DEFAULT_SERVICE_CONFIG_FILE
48
49
  from autonomy.configurations.loader import apply_env_variables, load_service_config
50
+ from autonomy.constants import DEFAULT_KEYS_FILE, DOCKER_COMPOSE_YAML
49
51
  from autonomy.deploy.base import BaseDeploymentGenerator
50
52
  from autonomy.deploy.base import ServiceBuilder as BaseServiceBuilder
51
53
  from autonomy.deploy.constants import (
@@ -61,14 +63,8 @@ from autonomy.deploy.generators.docker_compose.base import DockerComposeGenerato
61
63
  from autonomy.deploy.generators.kubernetes.base import KubernetesGenerator
62
64
  from docker import from_env
63
65
 
64
- from operate.constants import (
65
- DEPLOYMENT,
66
- DEPLOYMENT_JSON,
67
- DOCKER_COMPOSE_YAML,
68
- KEYS_JSON,
69
- ZERO_ADDRESS,
70
- )
71
- from operate.keys import Keys
66
+ from operate.constants import CONFIG_JSON, DEPLOYMENT_DIR, DEPLOYMENT_JSON
67
+ from operate.keys import KeysManager
72
68
  from operate.operate_http.exceptions import NotAllowed
73
69
  from operate.operate_types import (
74
70
  Chain,
@@ -96,11 +92,10 @@ from operate.utils.ssl import create_ssl_certificate
96
92
  SAFE_CONTRACT_ADDRESS = "safe_contract_address"
97
93
  ALL_PARTICIPANTS = "all_participants"
98
94
  CONSENSUS_THRESHOLD = "consensus_threshold"
99
- DELETE_PREFIX = "delete_"
100
- SERVICE_CONFIG_VERSION = 6
95
+ SERVICE_CONFIG_VERSION = 8
101
96
  SERVICE_CONFIG_PREFIX = "sc-"
102
97
 
103
- NON_EXISTENT_MULTISIG = "0xm"
98
+ NON_EXISTENT_MULTISIG = None
104
99
  NON_EXISTENT_TOKEN = -1
105
100
 
106
101
  DEFAULT_TRADER_ENV_VARS = {
@@ -410,7 +405,7 @@ class Deployment(LocalResource):
410
405
  nodes: DeployedNodes
411
406
  path: Path
412
407
 
413
- _file = "deployment.json"
408
+ _file = DEPLOYMENT_JSON
414
409
 
415
410
  @staticmethod
416
411
  def new(path: Path) -> "Deployment":
@@ -435,14 +430,14 @@ class Deployment(LocalResource):
435
430
 
436
431
  def copy_previous_agent_run_logs(self) -> None:
437
432
  """Copy previous agent logs."""
438
- source_path = self.path / DEPLOYMENT / "agent" / "log.txt"
433
+ source_path = self.path / DEPLOYMENT_DIR / "agent" / "log.txt"
439
434
  destination_path = self.path / "prev_log.txt"
440
435
  if source_path.exists():
441
436
  shutil.copy(source_path, destination_path)
442
437
 
443
438
  def _build_kubernetes(self, force: bool = True) -> None:
444
439
  """Build kubernetes deployment."""
445
- k8s_build = self.path / DEPLOYMENT / "abci_build_k8s"
440
+ k8s_build = self.path / DEPLOYMENT_DIR / "abci_build_k8s"
446
441
  if k8s_build.exists() and force:
447
442
  shutil.rmtree(k8s_build)
448
443
  mkdirs(build_dir=k8s_build)
@@ -450,8 +445,8 @@ class Deployment(LocalResource):
450
445
  service = Service.load(path=self.path)
451
446
  builder = ServiceBuilder.from_dir(
452
447
  path=service.package_absolute_path,
453
- keys_file=self.path / KEYS_JSON,
454
- number_of_agents=len(service.keys),
448
+ keys_file=self.path / DEFAULT_KEYS_FILE,
449
+ number_of_agents=len(service.agent_addresses),
455
450
  )
456
451
  builder.deplopyment_type = KubernetesGenerator.deployment_type
457
452
  (
@@ -482,7 +477,7 @@ class Deployment(LocalResource):
482
477
  force=force,
483
478
  )
484
479
 
485
- build = self.path / DEPLOYMENT
480
+ build = self.path / DEPLOYMENT_DIR
486
481
  if build.exists() and not force:
487
482
  return
488
483
  if build.exists() and force:
@@ -490,16 +485,12 @@ class Deployment(LocalResource):
490
485
  shutil.rmtree(build)
491
486
  mkdirs(build_dir=build)
492
487
 
493
- keys_file = self.path / KEYS_JSON
488
+ keys_file = self.path / DEFAULT_KEYS_FILE
494
489
  keys_file.write_text(
495
490
  json.dumps(
496
491
  [
497
- {
498
- "address": key.address,
499
- "private_key": key.private_key,
500
- "ledger": key.ledger.name.lower(),
501
- }
502
- for key in service.keys
492
+ KeysManager().get(address).json
493
+ for address in service.agent_addresses
503
494
  ],
504
495
  indent=4,
505
496
  ),
@@ -509,7 +500,7 @@ class Deployment(LocalResource):
509
500
  builder = ServiceBuilder.from_dir(
510
501
  path=service.package_absolute_path,
511
502
  keys_file=keys_file,
512
- number_of_agents=len(service.keys),
503
+ number_of_agents=len(service.agent_addresses),
513
504
  )
514
505
  builder.deplopyment_type = DockerComposeGenerator.deployment_type
515
506
  builder.try_update_abci_connection_params()
@@ -588,7 +579,7 @@ class Deployment(LocalResource):
588
579
 
589
580
  def _build_host(self, force: bool = True, chain: t.Optional[str] = None) -> None:
590
581
  """Build host depployment."""
591
- build = self.path / DEPLOYMENT
582
+ build = self.path / DEPLOYMENT_DIR
592
583
  if build.exists() and not force:
593
584
  return
594
585
 
@@ -617,16 +608,12 @@ class Deployment(LocalResource):
617
608
  chain_config = service.chain_configs[chain]
618
609
  chain_data = chain_config.chain_data
619
610
 
620
- keys_file = self.path / KEYS_JSON
611
+ keys_file = self.path / DEFAULT_KEYS_FILE
621
612
  keys_file.write_text(
622
613
  json.dumps(
623
614
  [
624
- {
625
- "address": key.address,
626
- "private_key": key.private_key,
627
- "ledger": key.ledger.name.lower(),
628
- }
629
- for key in service.keys
615
+ KeysManager().get(address).json
616
+ for address in service.agent_addresses
630
617
  ],
631
618
  indent=4,
632
619
  ),
@@ -636,7 +623,7 @@ class Deployment(LocalResource):
636
623
  builder = ServiceBuilder.from_dir(
637
624
  path=service.package_absolute_path,
638
625
  keys_file=keys_file,
639
- number_of_agents=len(service.keys),
626
+ number_of_agents=len(service.agent_addresses),
640
627
  )
641
628
  builder.deplopyment_type = HostDeploymentGenerator.deployment_type
642
629
  builder.try_update_abci_connection_params()
@@ -708,7 +695,7 @@ class Deployment(LocalResource):
708
695
  self._build_kubernetes(force=force)
709
696
  else:
710
697
  ssl_key_path, ssl_cert_path = create_ssl_certificate(
711
- ssl_dir=service.path / DEPLOYMENT / "ssl"
698
+ ssl_dir=service.path / DEPLOYMENT_DIR / "ssl"
712
699
  )
713
700
  service.update_env_variables_values(
714
701
  {
@@ -734,7 +721,11 @@ class Deployment(LocalResource):
734
721
 
735
722
  try:
736
723
  if use_docker:
737
- run_deployment(build_dir=self.path / "deployment", detach=True)
724
+ run_deployment(
725
+ build_dir=self.path / "deployment",
726
+ detach=True,
727
+ project_name=self.path.name,
728
+ )
738
729
  else:
739
730
  run_host_deployment(build_dir=self.path / "deployment")
740
731
  except Exception:
@@ -754,7 +745,10 @@ class Deployment(LocalResource):
754
745
  self.store()
755
746
 
756
747
  if use_docker:
757
- stop_deployment(build_dir=self.path / "deployment")
748
+ stop_deployment(
749
+ build_dir=self.path / "deployment",
750
+ project_name=self.path.name,
751
+ )
758
752
  else:
759
753
  stop_host_deployment(build_dir=self.path / "deployment")
760
754
 
@@ -763,7 +757,7 @@ class Deployment(LocalResource):
763
757
 
764
758
  def delete(self) -> None:
765
759
  """Delete the deployment."""
766
- build = self.path / DEPLOYMENT
760
+ build = self.path / DEPLOYMENT_DIR
767
761
  shutil.rmtree(build)
768
762
  self.status = DeploymentStatus.DELETED
769
763
  self.store()
@@ -777,7 +771,7 @@ class Service(LocalResource):
777
771
  service_config_id: str
778
772
  hash: str
779
773
  hash_history: t.Dict[int, str]
780
- keys: Keys
774
+ agent_addresses: t.List[str]
781
775
  home_chain: str
782
776
  chain_configs: ChainConfigs
783
777
  description: str
@@ -791,10 +785,10 @@ class Service(LocalResource):
791
785
  _helper: t.Optional[ServiceHelper] = None
792
786
  _deployment: t.Optional[Deployment] = None
793
787
 
794
- _file = "config.json"
788
+ _file = CONFIG_JSON
795
789
 
796
790
  @staticmethod
797
- def _determine_agent_id(service_name: str) -> int:
791
+ def determine_agent_id(service_name: str) -> int:
798
792
  """Determine the appropriate agent ID based on service name."""
799
793
  service_name_lower = service_name.lower()
800
794
  if "mech" in service_name_lower:
@@ -805,195 +799,6 @@ class Service(LocalResource):
805
799
  return AGENT_TYPE_IDS["modius"]
806
800
  return AGENT_TYPE_IDS["trader"]
807
801
 
808
- @classmethod
809
- def migrate_format(cls, path: Path) -> bool: # pylint: disable=too-many-statements
810
- """Migrate the JSON file format if needed."""
811
-
812
- if not path.is_dir():
813
- return False
814
-
815
- if not path.name.startswith(SERVICE_CONFIG_PREFIX) and not path.name.startswith(
816
- "bafybei"
817
- ):
818
- return False
819
-
820
- if path.name.startswith("bafybei"):
821
- backup_name = f"backup_{int(time.time())}_{path.name}"
822
- backup_path = path.parent / backup_name
823
- shutil.copytree(path, backup_path)
824
- deployment_path = backup_path / "deployment"
825
- if deployment_path.is_dir():
826
- shutil.rmtree(deployment_path)
827
-
828
- with open(path / Service._file, "r", encoding="utf-8") as file:
829
- data = json.load(file)
830
-
831
- version = data.get("version", 0)
832
- if version > SERVICE_CONFIG_VERSION:
833
- raise RuntimeError(
834
- f"Service configuration in {path} has version {version}, which means it was created with a newer version of olas-operate-middleware. Only configuration versions <= {SERVICE_CONFIG_VERSION} are supported by this version of olas-operate-middleware."
835
- )
836
-
837
- # Complete missing env vars for trader
838
- if "trader" in data["name"].lower():
839
- data.setdefault("env_variables", {})
840
-
841
- for key, value in DEFAULT_TRADER_ENV_VARS.items():
842
- if key not in data["env_variables"]:
843
- data["env_variables"][key] = value
844
-
845
- with open(path / Service._file, "w", encoding="utf-8") as file:
846
- json.dump(data, file, indent=2)
847
-
848
- if version == SERVICE_CONFIG_VERSION:
849
- return False
850
-
851
- # Migration steps for older versions
852
- if version == 0:
853
- new_data = {
854
- "version": 2,
855
- "hash": data.get("hash"),
856
- "keys": data.get("keys"),
857
- "home_chain_id": "100", # This is the default value for version 2 - do not change, will be corrected below
858
- "chain_configs": {
859
- "100": { # This is the default value for version 2 - do not change, will be corrected below
860
- "ledger_config": {
861
- "rpc": data.get("ledger_config", {}).get("rpc"),
862
- "type": data.get("ledger_config", {}).get("type"),
863
- "chain": data.get("ledger_config", {}).get("chain"),
864
- },
865
- "chain_data": {
866
- "instances": data.get("chain_data", {}).get(
867
- "instances", []
868
- ),
869
- "token": data.get("chain_data", {}).get("token"),
870
- "multisig": data.get("chain_data", {}).get("multisig"),
871
- "staked": data.get("chain_data", {}).get("staked", False),
872
- "on_chain_state": data.get("chain_data", {}).get(
873
- "on_chain_state", 3
874
- ),
875
- "user_params": {
876
- "staking_program_id": "pearl_alpha",
877
- "nft": data.get("chain_data", {})
878
- .get("user_params", {})
879
- .get("nft"),
880
- "threshold": data.get("chain_data", {})
881
- .get("user_params", {})
882
- .get("threshold"),
883
- "use_staking": data.get("chain_data", {})
884
- .get("user_params", {})
885
- .get("use_staking"),
886
- "cost_of_bond": data.get("chain_data", {})
887
- .get("user_params", {})
888
- .get("cost_of_bond"),
889
- "fund_requirements": data.get("chain_data", {})
890
- .get("user_params", {})
891
- .get("fund_requirements", {}),
892
- "agent_id": data.get("chain_data", {})
893
- .get("user_params", {})
894
- .get("agent_id", "14"),
895
- },
896
- },
897
- }
898
- },
899
- "service_path": data.get("service_path", ""),
900
- "name": data.get("name", ""),
901
- }
902
- data = new_data
903
-
904
- if version < 4:
905
- # Add missing fields introduced in later versions, if necessary.
906
- for _, chain_data in data.get("chain_configs", {}).items():
907
- chain_data.setdefault("chain_data", {}).setdefault(
908
- "user_params", {}
909
- ).setdefault("use_mech_marketplace", False)
910
- service_name = data.get("name", "")
911
- agent_id = cls._determine_agent_id(service_name)
912
- chain_data.setdefault("chain_data", {}).setdefault("user_params", {})[
913
- "agent_id"
914
- ] = agent_id
915
-
916
- data["description"] = data.setdefault("description", data.get("name"))
917
- data["hash_history"] = data.setdefault(
918
- "hash_history", {int(time.time()): data["hash"]}
919
- )
920
-
921
- if "service_config_id" not in data:
922
- service_config_id = Service.get_new_service_config_id(path)
923
- new_path = path.parent / service_config_id
924
- data["service_config_id"] = service_config_id
925
- path = path.rename(new_path)
926
-
927
- old_to_new_ledgers = ["ethereum", "solana"]
928
- for key_data in data["keys"]:
929
- key_data["ledger"] = old_to_new_ledgers[key_data["ledger"]]
930
-
931
- old_to_new_chains = [
932
- "ethereum",
933
- "goerli",
934
- "gnosis",
935
- "solana",
936
- "optimistic",
937
- "base",
938
- "mode",
939
- ]
940
- new_chain_configs = {}
941
- for chain_id, chain_data in data["chain_configs"].items():
942
- chain_data["ledger_config"]["chain"] = old_to_new_chains[
943
- chain_data["ledger_config"]["chain"]
944
- ]
945
- del chain_data["ledger_config"]["type"]
946
- new_chain_configs[Chain.from_id(int(chain_id)).value] = chain_data # type: ignore
947
-
948
- data["chain_configs"] = new_chain_configs
949
- data["home_chain"] = data.setdefault("home_chain", Chain.from_id(int(data.get("home_chain_id", "100"))).value) # type: ignore
950
- del data["home_chain_id"]
951
-
952
- if "env_variables" not in data:
953
- if data["name"] == "valory/trader_pearl":
954
- data["env_variables"] = DEFAULT_TRADER_ENV_VARS
955
- else:
956
- data["env_variables"] = {}
957
-
958
- if version < 5:
959
- new_chain_configs = {}
960
- for chain, chain_data in data["chain_configs"].items():
961
- fund_requirements = chain_data["chain_data"]["user_params"][
962
- "fund_requirements"
963
- ]
964
- if ZERO_ADDRESS not in fund_requirements:
965
- chain_data["chain_data"]["user_params"]["fund_requirements"] = {
966
- ZERO_ADDRESS: fund_requirements
967
- }
968
-
969
- new_chain_configs[chain] = chain_data # type: ignore
970
- data["chain_configs"] = new_chain_configs
971
-
972
- data["version"] = SERVICE_CONFIG_VERSION
973
-
974
- # Redownload service path
975
- if "service_path" in data:
976
- package_absolute_path = path / Path(data["service_path"]).name
977
- data.pop("service_path")
978
- else:
979
- package_absolute_path = path / data["package_path"]
980
-
981
- if package_absolute_path.exists() and package_absolute_path.is_dir():
982
- shutil.rmtree(package_absolute_path)
983
-
984
- package_absolute_path = Path(
985
- IPFSTool().download(
986
- hash_id=data["hash"],
987
- target_dir=path,
988
- )
989
- )
990
- data["package_path"] = str(package_absolute_path.name)
991
-
992
- with open(path / Service._file, "w", encoding="utf-8") as file:
993
- json.dump(data, file, indent=2)
994
-
995
- return True
996
-
997
802
  @classmethod
998
803
  def load(cls, path: Path) -> "Service":
999
804
  """Load a service"""
@@ -1028,7 +833,7 @@ class Service(LocalResource):
1028
833
  package_absolute_path = self.path / self.package_path
1029
834
  if (
1030
835
  not package_absolute_path.exists()
1031
- or not (package_absolute_path / "service.yaml").exists()
836
+ or not (package_absolute_path / DEFAULT_SERVICE_CONFIG_FILE).exists()
1032
837
  ):
1033
838
  with tempfile.TemporaryDirectory(dir=self.path) as temp_dir:
1034
839
  package_temp_path = Path(
@@ -1048,7 +853,7 @@ class Service(LocalResource):
1048
853
 
1049
854
  @staticmethod
1050
855
  def new( # pylint: disable=too-many-locals
1051
- keys: Keys,
856
+ agent_addresses: t.List[str],
1052
857
  service_template: ServiceTemplate,
1053
858
  storage: Path,
1054
859
  ) -> "Service":
@@ -1090,7 +895,7 @@ class Service(LocalResource):
1090
895
  name=service_template["name"],
1091
896
  description=service_template["description"],
1092
897
  hash=service_template["hash"],
1093
- keys=keys,
898
+ agent_addresses=agent_addresses,
1094
899
  home_chain=service_template["home_chain"],
1095
900
  hash_history={current_timestamp: service_template["hash"]},
1096
901
  chain_configs=chain_configs,
@@ -1103,7 +908,7 @@ class Service(LocalResource):
1103
908
 
1104
909
  def service_public_id(self, include_version: bool = True) -> str:
1105
910
  """Get the public id (based on the service hash)."""
1106
- with (self.package_absolute_path / "service.yaml").open(
911
+ with (self.package_absolute_path / DEFAULT_SERVICE_CONFIG_FILE).open(
1107
912
  "r", encoding="utf-8"
1108
913
  ) as fp:
1109
914
  service_yaml, *_ = yaml_load_all(fp)
@@ -1135,7 +940,9 @@ class Service(LocalResource):
1135
940
  )
1136
941
  )
1137
942
 
1138
- with (package_path / "service.yaml").open("r", encoding="utf-8") as fp:
943
+ with (package_path / DEFAULT_SERVICE_CONFIG_FILE).open(
944
+ "r", encoding="utf-8"
945
+ ) as fp:
1139
946
  service_yaml, *_ = yaml_load_all(fp)
1140
947
 
1141
948
  public_id = f"{service_yaml['author']}/{service_yaml['name']}"
@@ -1321,10 +1128,3 @@ class Service(LocalResource):
1321
1128
 
1322
1129
  if updated:
1323
1130
  self.store()
1324
-
1325
- def delete(self) -> None:
1326
- """Delete a service."""
1327
- parent_directory = self.path.parent
1328
- new_path = parent_directory / f"{DELETE_PREFIX}{self.path.name}"
1329
- shutil.move(self.path, new_path)
1330
- shutil.rmtree(new_path)
operate/utils/__init__.py CHANGED
@@ -19,10 +19,54 @@
19
19
 
20
20
  """Helper utilities."""
21
21
 
22
+ import functools
22
23
  import shutil
23
24
  import time
24
25
  import typing as t
25
26
  from pathlib import Path
27
+ from threading import Lock
28
+
29
+
30
+ class SingletonMeta(type):
31
+ """A metaclass for creating thread-safe singleton classes."""
32
+
33
+ _instances: t.Dict[t.Type, t.Any] = {}
34
+ _lock: Lock = Lock()
35
+ _class_locks: t.Dict[t.Type, Lock] = {}
36
+
37
+ def __new__(
38
+ cls, name: str, bases: t.Tuple[type, ...], dct: t.Dict[str, t.Any]
39
+ ) -> t.Type:
40
+ """Create a new class with thread-safe methods."""
41
+ # Wrap all callable methods (except special methods) with thread safety
42
+ for key, value in list(dct.items()):
43
+ if callable(value) and not key.startswith("__"):
44
+ dct[key] = cls._make_thread_safe(value)
45
+
46
+ new_class = super().__new__(cls, name, bases, dct)
47
+ cls._class_locks[new_class] = Lock()
48
+ return new_class
49
+
50
+ @staticmethod
51
+ def _make_thread_safe(func: t.Callable) -> t.Callable:
52
+ """Wrap a function to make it thread-safe."""
53
+
54
+ @functools.wraps(func)
55
+ def wrapper(self: t.Any, *args: t.Any, **kwargs: t.Any) -> t.Any:
56
+ class_lock = SingletonMeta._class_locks.get(type(self))
57
+ if class_lock:
58
+ with class_lock:
59
+ return func(self, *args, **kwargs)
60
+ return func(self, *args, **kwargs)
61
+
62
+ return wrapper
63
+
64
+ def __call__(cls, *args: t.Any, **kwargs: t.Any) -> t.Any:
65
+ """Override the __call__ method to control instance creation."""
66
+ with cls._lock:
67
+ if cls not in cls._instances:
68
+ cls._instances[cls] = super().__call__(*args, **kwargs)
69
+ return cls._instances[cls]
26
70
 
27
71
 
28
72
  def create_backup(path: Path) -> Path:
operate/utils/gnosis.py CHANGED
@@ -42,10 +42,8 @@ from operate.constants import (
42
42
  from operate.operate_types import Chain
43
43
 
44
44
 
45
- logger = setup_logger(name="operate.manager")
46
- NULL_ADDRESS: str = "0x" + "0" * 40
45
+ logger = setup_logger(name="operate.utils.gnosis")
47
46
  MAX_UINT256 = 2**256 - 1
48
- ZERO_ETH = 0
49
47
  SENTINEL_OWNERS = "0x0000000000000000000000000000000000000001"
50
48
 
51
49
 
@@ -73,8 +71,8 @@ def hash_payload_to_hex( # pylint: disable=too-many-arguments,too-many-locals
73
71
  operation: int = SafeOperation.CALL.value,
74
72
  base_gas: int = 0,
75
73
  safe_gas_price: int = 0,
76
- gas_token: str = NULL_ADDRESS,
77
- refund_receiver: str = NULL_ADDRESS,
74
+ gas_token: str = ZERO_ADDRESS,
75
+ refund_receiver: str = ZERO_ADDRESS,
78
76
  use_flashbots: bool = False,
79
77
  gas_limit: int = 0,
80
78
  raise_on_failed_simulation: bool = False,
@@ -492,7 +490,7 @@ def drain_eoa(
492
490
  crypto: Crypto,
493
491
  withdrawal_address: str,
494
492
  chain_id: int,
495
- ) -> str:
493
+ ) -> t.Optional[str]:
496
494
  """Drain all the native tokens from the crypto wallet."""
497
495
  tx_helper = TxSettler(
498
496
  ledger_api=ledger_api,
@@ -518,21 +516,20 @@ def drain_eoa(
518
516
  )
519
517
  tx = ledger_api.update_with_gas_estimate(
520
518
  transaction=tx,
521
- raise_on_try=True,
519
+ raise_on_try=False,
522
520
  )
523
521
 
524
522
  chain_fee = tx["gas"] * tx["maxFeePerGas"]
525
523
  if Chain.from_id(chain_id) in (
526
524
  Chain.ARBITRUM_ONE,
527
525
  Chain.BASE,
528
- Chain.OPTIMISTIC,
526
+ Chain.OPTIMISM,
529
527
  Chain.MODE,
530
528
  ):
531
529
  chain_fee += ledger_api.get_l1_data_fee(tx)
532
530
 
533
531
  tx["value"] = ledger_api.get_balance(crypto.address) - chain_fee
534
532
  if tx["value"] <= 0:
535
- logger.warning(f"No balance to drain from wallet: {crypto.address}")
536
533
  raise ChainInteractionError(
537
534
  f"No balance to drain from wallet: {crypto.address}"
538
535
  )
@@ -544,14 +541,25 @@ def drain_eoa(
544
541
  return tx
545
542
 
546
543
  setattr(tx_helper, "build", _build_tx) # noqa: B010
547
- tx_receipt = tx_helper.transact(
548
- method=lambda: {},
549
- contract="",
550
- kwargs={},
551
- dry_run=False,
552
- )
553
- tx_hash = tx_receipt.get("transactionHash", "").hex()
554
- return tx_hash
544
+ try:
545
+ tx_receipt = tx_helper.transact(
546
+ method=lambda: {},
547
+ contract="",
548
+ kwargs={},
549
+ dry_run=False,
550
+ )
551
+ except ChainInteractionError as e:
552
+ if "No balance to drain from wallet" in str(e):
553
+ logger.warning(f"Failed to drain wallet {crypto.address} with error: {e}.")
554
+ return None
555
+
556
+ raise e
557
+
558
+ tx_hash = tx_receipt.get("transactionHash", None)
559
+ if tx_hash is not None:
560
+ return tx_hash.hex()
561
+
562
+ return None
555
563
 
556
564
 
557
565
  def get_asset_balance(