|
1 | 1 | import logging |
| 2 | +import re |
2 | 3 | from typing import Optional |
3 | 4 |
|
4 | 5 | from acapy_agent.core.event_bus import EventBus, Event |
|
8 | 9 | from .models import CredDefStorageRecord |
9 | 10 |
|
10 | 11 | from acapy_agent.messaging.credential_definitions.util import ( |
11 | | - EVENT_LISTENER_PATTERN as CREDDEF_EVENT_LISTENER_PATTERN, |
| 12 | + EVENT_LISTENER_PATTERN as INDY_CREDDEF_EVENT_PATTERN, |
| 13 | +) |
| 14 | +from acapy_agent.anoncreds.events import ( |
| 15 | + CRED_DEF_FINISHED_EVENT as ANONCREDS_CREDDEF_FINISHED_EVENT, |
12 | 16 | ) |
13 | 17 |
|
14 | 18 | LOGGER = logging.getLogger(__name__) |
@@ -61,20 +65,85 @@ async def list_items( |
61 | 65 | self.logger.info(f"< list_items({tag_filter}, {post_filter}): {len(records)}") |
62 | 66 | return records |
63 | 67 |
|
| 68 | + def _is_anoncreds_wallet(self, profile: Profile) -> bool: |
| 69 | + """Check if the wallet is an anoncreds wallet.""" |
| 70 | + wallet_type = profile.settings.get("wallet.type", "askar") |
| 71 | + return wallet_type in ("askar-anoncreds", "kanon-anoncreds") |
| 72 | + |
| 73 | + async def _fetch_tag( |
| 74 | + self, |
| 75 | + profile: Profile, |
| 76 | + cred_def_id: str, |
| 77 | + ) -> Optional[str]: |
| 78 | + """Fetch tag from registry for AnonCreds credential definitions. |
| 79 | +
|
| 80 | + AnonCreds events never include tag in the event payload, so we must fetch it from the registry. |
| 81 | + This function is only called for AnonCreds credential definitions. |
| 82 | +
|
| 83 | + Returns tag from registry, or None if fetch fails. |
| 84 | + """ |
| 85 | + from acapy_agent.anoncreds.registry import AnonCredsRegistry |
| 86 | + from acapy_agent.anoncreds.base import AnonCredsResolutionError |
| 87 | + |
| 88 | + try: |
| 89 | + anoncreds_registry = profile.inject(AnonCredsRegistry) |
| 90 | + cred_def_result = await anoncreds_registry.get_credential_definition( |
| 91 | + profile, cred_def_id |
| 92 | + ) |
| 93 | + |
| 94 | + cred_def = cred_def_result.credential_definition |
| 95 | + tag = cred_def.tag or "default" |
| 96 | + |
| 97 | + self.logger.debug(f"Fetched tag from registry: {tag}") |
| 98 | + return tag |
| 99 | + except AnonCredsResolutionError: |
| 100 | + # Registry doesn't support this identifier |
| 101 | + self.logger.error( |
| 102 | + f"Registry could not resolve credential definition: {cred_def_id}" |
| 103 | + ) |
| 104 | + return None |
| 105 | + except Exception as err: |
| 106 | + # Other errors from registry (e.g., network issues, injection errors) |
| 107 | + self.logger.error( |
| 108 | + f"Error fetching credential definition from registry: {err}" |
| 109 | + ) |
| 110 | + return None |
| 111 | + |
| 112 | + async def _create_storage_record( |
| 113 | + self, profile: Profile, data: dict |
| 114 | + ) -> CredDefStorageRecord: |
| 115 | + """Create and save a credential definition storage record.""" |
| 116 | + rec: CredDefStorageRecord = CredDefStorageRecord.deserialize(data) |
| 117 | + self.logger.debug(f"cred_def_storage_rec = {rec}") |
| 118 | + async with profile.session() as session: |
| 119 | + await rec.save(session, reason="New cred def storage record") |
| 120 | + return rec |
| 121 | + |
64 | 122 | async def add_item(self, profile: Profile, data: dict): |
65 | 123 | self.logger.info(f"> add_item({data})") |
66 | | - # check if |
67 | | - cred_def_id = data["cred_def_id"] |
| 124 | + cred_def_id = data.get("cred_def_id") |
| 125 | + if not cred_def_id: |
| 126 | + raise ValueError("cred_def_id is required in data") |
| 127 | + |
| 128 | + # Early return if record already exists |
68 | 129 | rec = await self.read_item(profile, cred_def_id) |
69 | | - if not rec: |
70 | | - try: |
71 | | - rec: CredDefStorageRecord = CredDefStorageRecord.deserialize(data) |
72 | | - self.logger.debug(f"cred_def_storage_rec = {rec}") |
73 | | - async with profile.session() as session: |
74 | | - await rec.save(session, reason="New cred def storage record") |
75 | | - except Exception as err: |
76 | | - self.logger.error("Error adding cred def storage record.", err) |
77 | | - raise err |
| 130 | + if rec: |
| 131 | + self.logger.info(f"< add_item({cred_def_id}): {rec}") |
| 132 | + return rec |
| 133 | + |
| 134 | + # Fetch tag from registry for AnonCreds credential definitions |
| 135 | + # AnonCreds events never include tag, so we must fetch it |
| 136 | + if self._is_anoncreds_wallet(profile): |
| 137 | + tag = await self._fetch_tag(profile, cred_def_id) |
| 138 | + if tag: |
| 139 | + data["tag"] = tag |
| 140 | + |
| 141 | + # Create and save the storage record |
| 142 | + try: |
| 143 | + rec = await self._create_storage_record(profile, data) |
| 144 | + except Exception as err: |
| 145 | + self.logger.error("Error adding cred def storage record.", err) |
| 146 | + raise err |
78 | 147 |
|
79 | 148 | self.logger.info(f"< add_item({cred_def_id}): {rec}") |
80 | 149 | return rec |
@@ -105,15 +174,66 @@ async def remove_item(self, profile: Profile, cred_def_id: str): |
105 | 174 |
|
106 | 175 |
|
107 | 176 | def subscribe(bus: EventBus): |
108 | | - bus.subscribe(CREDDEF_EVENT_LISTENER_PATTERN, creddef_event_handler) |
| 177 | + # Subscribe to both Indy and AnonCreds credential definition events |
| 178 | + bus.subscribe(INDY_CREDDEF_EVENT_PATTERN, creddef_event_handler) |
| 179 | + # Explicitly compile as literal pattern to ensure it's a Pattern object, not a string |
| 180 | + bus.subscribe( |
| 181 | + re.compile(re.escape(ANONCREDS_CREDDEF_FINISHED_EVENT)), creddef_event_handler |
| 182 | + ) |
| 183 | + |
| 184 | + |
| 185 | +def _normalize_creddef_event_payload(event: Event) -> dict: |
| 186 | + """Normalize credential definition event payload from either Indy (dict with context) or AnonCreds (NamedTuple) format. |
| 187 | +
|
| 188 | + AnonCreds events use CredDefFinishedPayload NamedTuple (not a dict), so we check for |
| 189 | + NamedTuple attributes and convert to a dict format for unified processing. |
| 190 | + """ |
| 191 | + payload = event.payload |
| 192 | + |
| 193 | + # Check if it's an AnonCreds event (NamedTuple) |
| 194 | + # AnonCreds events are CredDefFinishedPayload NamedTuples, not dicts |
| 195 | + if hasattr(payload, "schema_id") and hasattr(payload, "cred_def_id"): |
| 196 | + # AnonCreds event: CredDefFinishedPayload NamedTuple |
| 197 | + rev_reg_size = payload.max_cred_num if payload.support_revocation else None |
| 198 | + return { |
| 199 | + "cred_def_id": payload.cred_def_id, |
| 200 | + "schema_id": payload.schema_id, |
| 201 | + "tag": None, # NEVER in AnonCreds event, must be fetched from registry |
| 202 | + "support_revocation": payload.support_revocation, |
| 203 | + "rev_reg_size": rev_reg_size, # Always set (from max_cred_num) |
| 204 | + "issuer_id": payload.issuer_id, |
| 205 | + "options": payload.options, # Preserve for any other fields |
| 206 | + } |
| 207 | + elif isinstance(payload, dict) and "context" in payload: |
| 208 | + # Indy event: dict with "context" key |
| 209 | + context = payload["context"] |
| 210 | + return { |
| 211 | + "cred_def_id": context.get("cred_def_id"), |
| 212 | + "schema_id": context.get("schema_id"), |
| 213 | + "tag": context.get("tag"), |
| 214 | + "support_revocation": context.get("support_revocation", False), |
| 215 | + "rev_reg_size": context.get("rev_reg_size"), |
| 216 | + "issuer_did": context.get("issuer_did"), |
| 217 | + "options": context.get("options", {}), |
| 218 | + } |
| 219 | + else: |
| 220 | + # Fallback: assume it's already in the right format |
| 221 | + return payload if isinstance(payload, dict) else {} |
109 | 222 |
|
110 | 223 |
|
111 | 224 | async def creddef_event_handler(profile: Profile, event: Event): |
112 | 225 | LOGGER.info("> creddef_event_handler") |
113 | 226 | LOGGER.debug(f"profile = {profile}") |
114 | 227 | LOGGER.debug(f"event = {event}") |
| 228 | + LOGGER.debug(f"event.payload = {event.payload}") |
| 229 | + |
115 | 230 | srv = profile.inject(CredDefStorageService) |
116 | | - storage_record = await srv.add_item(profile, event.payload["context"]) |
| 231 | + |
| 232 | + # Normalize event payload to common format |
| 233 | + normalized_data = _normalize_creddef_event_payload(event) |
| 234 | + LOGGER.debug(f"normalized_data = {normalized_data}") |
| 235 | + |
| 236 | + storage_record = await srv.add_item(profile, normalized_data) |
117 | 237 | LOGGER.debug(f"creddef_storage_record = {storage_record}") |
118 | 238 |
|
119 | 239 | LOGGER.info("< creddef_event_handler") |
0 commit comments