@@ -83,6 +83,7 @@ class DiscoveredService:
8383 # New fields for full service import
8484 staking_contract_address : Optional [str ] = None
8585 service_owner_address : Optional [str ] = None
86+ service_owner_safe : Optional [str ] = None # To store the Safe Owner address if it exists
8687
8788 @property
8889 def agent_key (self ) -> Optional [DiscoveredKey ]:
@@ -517,7 +518,12 @@ def _extract_external_keys_folder(self, operate_folder: Path) -> List[Discovered
517518 return keys
518519
519520 def _extract_owner_address (self , service : DiscoveredService , operate_folder : Path ) -> None :
520- """Extract owner address from wallets/ethereum.json."""
521+ """Extract owner address from wallets/ethereum.json.
522+
523+ Handles two cases:
524+ 1. EOA is the owner (legacy).
525+ 2. Safe is the owner, and EOA is a signer (new staking programs).
526+ """
521527 wallets_folder = operate_folder / "wallets"
522528 if not wallets_folder .exists ():
523529 return
@@ -526,9 +532,35 @@ def _extract_owner_address(self, service: DiscoveredService, operate_folder: Pat
526532 if eth_json .exists ():
527533 try :
528534 data = json .loads (eth_json .read_text ())
529- if "address" in data :
530- service .service_owner_address = data ["address" ]
531- logger .debug (f"Extracted owner address: { service .service_owner_address } " )
535+
536+ # Check for "safes" entry which indicates the owner is a Safe
537+ # Structure: "safes": { "gnosis": "0x..." }
538+ if "safes" in data and FLAGS_OWNER_SAFE in data ["safes" ]: # Need to detect chain dynamically or iterate
539+ pass
540+
541+ # Logic update:
542+ # 1. Capture EOA address always (it's the signer)
543+ eoa_address = data .get ("address" )
544+
545+ # 2. Check for Safe Owner for the current service chain
546+ safe_owner_address = None
547+ if "safes" in data and isinstance (data ["safes" ], dict ):
548+ # We try to match with service.chain_name if available, usually "gnosis"
549+ chain = service .chain_name or "gnosis"
550+ safe_owner_address = data ["safes" ].get (chain )
551+
552+ if safe_owner_address :
553+ # CASE: Owner is Safe
554+ service .service_owner_address = safe_owner_address # The Safe is the official owner
555+ service .service_owner_safe = safe_owner_address
556+ # We also need to ensure the EOA is imported. 'check_wallet' usually handles the EOA.
557+ # But we need to link them.
558+ logger .debug (f"Extracted Safe owner address: { safe_owner_address } (Signer: { eoa_address } )" )
559+ elif eoa_address :
560+ # CASE: Owner is EOA
561+ service .service_owner_address = eoa_address
562+ logger .debug (f"Extracted EOA owner address: { eoa_address } " )
563+
532564 except (json .JSONDecodeError , IOError ) as e :
533565 logger .warning (f"Failed to parse { eth_json } : { e } " )
534566
@@ -725,15 +757,54 @@ def _import_discovered_keys(
725757
726758 def _import_discovered_safes (self , service : DiscoveredService , result : ImportResult ) -> None :
727759 """Import Safe from the service if present."""
760+
761+ # 1. Import Agent Multisig (the one the agent controls)
728762 if service .safe_address :
729- safe_result = self ._import_safe (service )
763+ safe_result = self ._import_safe (
764+ address = service .safe_address ,
765+ signers = self ._get_agent_signers (service ),
766+ tag_suffix = "safe" , # e.g. trader_zeta_safe
767+ service_name = service .service_name
768+ )
730769 if safe_result [0 ]:
731770 result .imported_safes .append (service .safe_address )
732771 elif safe_result [1 ] == "duplicate" :
733772 result .skipped .append (f"Safe { service .safe_address } (already exists)" )
734773 else :
735774 result .errors .append (f"Safe { service .safe_address } : { safe_result [1 ]} " )
736775
776+ # 2. Import Owner Safe (if it exists and is different)
777+ if service .service_owner_safe and service .service_owner_safe != service .safe_address :
778+ # Signer for Owner Safe is the EOA owner key
779+ owner_signers = self ._get_owner_signers (service )
780+
781+ safe_result = self ._import_safe (
782+ address = service .service_owner_safe ,
783+ signers = owner_signers ,
784+ tag_suffix = "owner_safe" , # e.g. trader_zeta_owner_safe
785+ service_name = service .service_name
786+ )
787+ if safe_result [0 ]:
788+ result .imported_safes .append (service .service_owner_safe )
789+ logger .info (f"Imported Owner Safe { service .service_owner_safe } " )
790+
791+ def _get_agent_signers (self , service : DiscoveredService ) -> List [str ]:
792+ """Get list of signers for the agent safe."""
793+ signers = []
794+ for key in service .keys :
795+ if key .role == "agent" :
796+ signers .append (key .address )
797+ return signers
798+
799+ def _get_owner_signers (self , service : DiscoveredService ) -> List [str ]:
800+ """Get list of signers for the owner safe."""
801+ signers = []
802+ for key in service .keys :
803+ # We look for keys marked as owner/operator
804+ if key .role in ["owner" , "operator" ]:
805+ signers .append (key .address )
806+ return signers
807+
737808 def _import_discovered_service_config (
738809 self , service : DiscoveredService , result : ImportResult
739810 ) -> None :
@@ -833,27 +904,26 @@ def _generate_tag(self, key: DiscoveredKey, service_name: Optional[str]) -> str:
833904 i += 1
834905 return f"{ base_tag } _{ i } "
835906
836- def _import_safe (self , service : DiscoveredService ) -> Tuple [bool , str ]:
837- """Import a Safe from a discovered service."""
838- if not service .safe_address :
907+ def _import_safe (
908+ self ,
909+ address : str ,
910+ signers : List [str ],
911+ tag_suffix : str ,
912+ service_name : Optional [str ]
913+ ) -> Tuple [bool , str ]:
914+ """Import a generic Safe."""
915+ if not address :
839916 return False , "no safe address"
840917
841918 # Check for duplicate
842- existing = self .key_storage .find_stored_account (service . safe_address )
919+ existing = self .key_storage .find_stored_account (address )
843920 if existing :
844921 return False , "duplicate"
845922
846- # Get signers from agent keys
847- signers = []
848- for key in service .keys :
849- if key .role == "agent" :
850- signers .append (key .address )
851-
852923 # Generate tag
853-
854- prefix = service .service_name or "imported"
924+ prefix = service_name or "imported"
855925 prefix = re .sub (r"[^a-z0-9]+" , "_" , prefix .lower ()).strip ("_" )
856- base_tag = f"{ prefix } _safe "
926+ base_tag = f"{ prefix } _ { tag_suffix } "
857927
858928 existing_tags = {
859929 acc .tag for acc in self .key_storage .accounts .values () if hasattr (acc , "tag" )
@@ -866,15 +936,15 @@ def _import_safe(self, service: DiscoveredService) -> Tuple[bool, str]:
866936
867937 safe_account = StoredSafeAccount (
868938 tag = tag ,
869- address = service . safe_address ,
870- chains = [service . chain_name ],
939+ address = address ,
940+ chains = ["gnosis" ], # TODO: detecting chain dynamically would be better
871941 threshold = 1 , # Default, accurate value requires on-chain query
872942 signers = signers ,
873943 )
874944
875- self .key_storage .accounts [service . safe_address ] = safe_account
945+ self .key_storage .accounts [address ] = safe_account
876946 self .key_storage .save ()
877- logger .info (f"Imported Safe { service . safe_address } as '{ tag } '" )
947+ logger .info (f"Imported Safe { address } as '{ tag } '" )
878948 return True , "ok"
879949
880950 def _import_service_config (self , service : DiscoveredService ) -> Tuple [bool , str ]:
@@ -975,3 +1045,4 @@ def _verify_key_signature(self, key: DiscoveredKey) -> None:
9751045 key .signature_failed = True
9761046 logger .warning (f"Error verifying signature for key { key .address } : { e } " )
9771047
1048+ FLAGS_OWNER_SAFE = "deprecated"
0 commit comments