Skip to content

Commit 93de02d

Browse files
committed
fix(importer): support Safe owners in operate import
1 parent 416ec36 commit 93de02d

2 files changed

Lines changed: 94 additions & 23 deletions

File tree

src/iwa/plugins/olas/importer.py

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)