# Aca-py code breakdown ## DID resolution API ### Step 1: API call Endpoint: GET `/resolver/resolve/{did}` From `aries_cloudagent/resolver/routes.py`: ```python= async def register(app: web.Application): """Register routes.""" app.add_routes( [ web.get( "/resolver/resolve/{did}", resolve_did, allow_head=False, ), ] ) ``` ### Step 2: `resolve_did` function - It takes a `request` object of type `web.Request` as an argument. This object represents an HTTP request. - It extracts the `AdminRequestContext` from the request object. This context might contain information about the current request, such as headers, user information, etc. - It retrieves the did (Decentralized Identifier) from the request's match info. This did is part of the URL path. - It opens a session with the current profile context. Within this session, it injects a `DIDResolver` instance. This resolver is likely responsible for handling operations related to DIDs. - It attempts to resolve the did using the `resolve_with_metadata` method of the `DIDResolver`. This method returns a `ResolutionResult` object that contains the resolved DID document along with some metadata. - If the did cannot be found, it raises a `web.HTTPNotFound` exception. If the DID method is not supported, it raises a `web.HTTPNotImplemented` exception. If there's a generic resolver error, it raises a `web.HTTPInternalServerError` exception. In each case, it includes the reason for the error. - If the did is successfully resolved, it serializes the `ResolutionResult` object into JSON format and returns it as a response to the HTTP request. From `aries_cloudagent/resolver/routes.py`: ```python= async def resolve_did(request: web.Request): """Retrieve a did document.""" context: AdminRequestContext = request["context"] did = request.match_info["did"] try: async with context.profile.session() as session: resolver = session.inject(DIDResolver) result: ResolutionResult = await resolver.resolve_with_metadata( context.profile, did ) except DIDNotFound as err: raise web.HTTPNotFound(reason=err.roll_up) from err except DIDMethodNotSupported as err: raise web.HTTPNotImplemented(reason=err.roll_up) from err except ResolverError as err: raise web.HTTPInternalServerError(reason=err.roll_up) from err return web.json_response(result.serialize()) ``` ### Step 3: `resolve_did_with_metadata` - It starts by recording the current time as `resolution_start_time`. - It then calls the `_resolve` method, which retrieves the resolver and document associated with the provided DID. - It calculates the duration it took to resolve the DID by subtracting `resolution_start_time` from the current time. - It creates a `ResolutionMetadata` object, which includes the type of the resolver, the name of the resolver class, the time the DID was retrieved, and the duration it took to resolve the DID. - Finally, it returns a `ResolutionResult` object, which includes the resolved document and the `ResolutionMetadata`. From `aries_cloudagent/resolver/did_resolver.py` ```python= async def resolve_with_metadata( self, profile: Profile, did: Union[str, DID], *, timeout: Optional[int] = None ) -> ResolutionResult: """Resolve a DID and return the ResolutionResult.""" resolution_start_time = datetime.now(tz=timezone.utc) resolver, doc = await self._resolve(profile, did, timeout=timeout) time_now = datetime.now(tz=timezone.utc) duration = int((time_now - resolution_start_time).total_seconds() * 1000) retrieved_time = time_now.strftime("%Y-%m-%dT%H:%M:%SZ") resolver_metadata = ResolutionMetadata( resolver.type, type(resolver).__qualname__, retrieved_time, duration ) return ResolutionResult(doc, resolver_metadata) ``` ### Step 4: `_resolve` method - It first checks if the did argument is an instance of the `DID` class. If it is, it converts it to a string. If it's not, it validates the did string. - It then iterates over the resolvers that match the did and profile using the `_match_did_to_resolver` method. - For each resolver, it attempts to resolve the did within a certain timeout period (either the provided timeout or the `DEFAULT_TIMEOUT` if no timeout is specified). If the did is successfully resolved, it returns the resolver and the resolved document. - If the did cannot be resolved by any of the resolvers (i.e., a `DIDNotFound` exception is raised), it logs a debug message and continues to the next resolver. - If none of the resolvers can resolve the did, it raises a `DIDNotFound` exception. From `aries_cloudagent/resolver/did_resolver.py` ```python= async def _resolve( self, profile: Profile, did: Union[str, DID], service_accept: Optional[Sequence[Text]] = None, *, timeout: Optional[int] = None, ) -> Tuple[BaseDIDResolver, dict]: """Retrieve doc and return with resolver. This private method enables the public resolve and resolve_with_metadata methods to share the same logic. """ if isinstance(did, DID): did = str(did) else: DID.validate(did) for resolver in await self._match_did_to_resolver(profile, did): try: LOGGER.debug("Resolving DID %s with %s", did, resolver) document = await asyncio.wait_for( resolver.resolve( profile, did, service_accept, ), timeout if timeout is not None else self.DEFAULT_TIMEOUT, ) LOGGER.debug("Resolved DID %s with %s: %s", did, resolver, document) return resolver, document except DIDNotFound: LOGGER.debug("DID %s not found by resolver %s", did, resolver) raise DIDNotFound(f"DID {did} could not be resolved") ``` ### Step 5: `_match_did_to_resolver` function The `_match_did_to_resolver` function is part of the `DIDResolver` class in the `aries_cloudagent/resolver/did_resolver.py` file. This function is used to match a given DID (Decentralized Identifier) to the appropriate resolver based on the method of the DID. The function takes two parameters: `profile` and `did`. The `profile` parameter is an instance of the `Profile` class, which represents the active profile for the agent. The `did` parameter is the Decentralized Identifier that needs to be resolved. The function returns a list of resolvers that support the given DID. The resolvers are sorted based on their priority and registration order. Native resolvers (those that are part of the Aries Cloud Agent Python) are given higher priority. This function is used internally by the `DIDResolver` class to determine which resolver to use when resolving a DID. It's important to note that this function is a private method, indicated by the underscore prefix, and is not intended to be used directly outside of the `DIDResolver` class. ```python= async def _match_did_to_resolver( self, profile: Profile, did: str ) -> Sequence[BaseDIDResolver]: """Generate supported DID Resolvers. Native resolvers are yielded first, in registered order followed by non-native resolvers in registered order. """ valid_resolvers = [ resolver for resolver in self.resolvers if await resolver.supports(profile, did) ] LOGGER.debug("Valid resolvers for DID %s: %s", did, valid_resolvers) native_resolvers = filter(lambda resolver: resolver.native, valid_resolvers) non_native_resolvers = filter( lambda resolver: not resolver.native, valid_resolvers ) resolvers = list(chain(native_resolvers, non_native_resolvers)) if not resolvers: raise DIDMethodNotSupported(f'No resolver supporting DID "{did}" loaded') return resolvers ``` ### Step 6: Resolve the did using the resolver - This function loops through all the did resolvers that are supported, and tries to resolve the did using `resolver.resolve` function, which is implemented in `aries_cloudagent/resolver/base.py`. It either gets resolved or results in timeout. In the timeout case, it retries with the next resolver, till there are resolvers left. If the did cannot be resolved, it returns a `DIDNotFound` error. - This function also validates the did, and if the did is illegal it returns error. From `aries_cloudagent/resolver/did_resolver.py` ```python= async def _resolve( self, profile: Profile, did: Union[str, DID], service_accept: Optional[Sequence[Text]] = None, *, timeout: Optional[int] = None, ) -> Tuple[BaseDIDResolver, dict]: """Retrieve doc and return with resolver. This private method enables the public resolve and resolve_with_metadata methods to share the same logic. """ if isinstance(did, DID): did = str(did) else: DID.validate(did) for resolver in await self._match_did_to_resolver(profile, did): try: LOGGER.debug("Resolving DID %s with %s", did, resolver) document = await asyncio.wait_for( resolver.resolve( profile, did, service_accept, ), timeout if timeout is not None else self.DEFAULT_TIMEOUT, ) LOGGER.debug("Resolved DID %s with %s: %s", did, resolver, document) return resolver, document except DIDNotFound: LOGGER.debug("DID %s not found by resolver %s", did, resolver) raise DIDNotFound(f"DID {did} could not be resolved") ``` ### Step 7: Resolving the `did` using `BaseDIDResolver` The `BaseDIDResolver` class is the base class inherited by all the did resolvers in aca-py. It contains the outline of all the methods required for resolving dids. The resolve method of the BaseDIDResolver class is used to resolve a DID (Decentralized Identifier) using the specific resolver. - It first checks if the did argument is an instance of the `DID` class. If it is, it converts it to a string. If it's not, it validates the did string. - It then checks if the current resolver supports the method of the provided did by calling the `supports` method. If the method is not supported, it raises a `DIDMethodNotSupported` exception. - It constructs a `cache_key` using the class name of the resolver and the did. - It tries to get the resolved DID document from the cache. If the result is found in the cache, it returns the result immediately. - If the result is not found in the cache, it calls the `_resolve` method to resolve the did. The `_resolve` method is supposed to be implemented by subclasses of `BaseDIDResolver`. - It stores the result from `_resolve` in the cache with a default TTL (Time To Live), and then returns the result. - If the cache is not available, it directly calls the `_resolve` method and returns the result. From `aries_cloudagent/resolver/base.py` ```python= async def resolve( self, profile: Profile, did: Union[str, DID], service_accept: Optional[Sequence[Text]] = None, ) -> dict: """Resolve a DID using this resolver. Handles caching of results. """ if isinstance(did, DID): did = str(did) else: DID.validate(did) if not await self.supports(profile, did): raise DIDMethodNotSupported( f"{self.__class__.__name__} does not support DID method for: {did}" ) cache_key = f"resolver::{type(self).__name__}::{did}" cache = profile.inject_or(BaseCache) if cache: async with cache.acquire(cache_key) as entry: if entry.result: return entry.result else: result = await self._resolve(profile, did, service_accept) await entry.set_result(result, ttl=self.DEFAULT_TTL) return result return await self._resolve(profile, did, service_accept) ``` The `_resolve` method at the end is implemented differently by different dids (like did:indy, did:peer, did:web, etc) ### Step 8.1: How `Indy` resolves dids - It starts by getting an instance of the `BaseMultitenantManager` from the profile. If the `BaseMultitenantManager` is available, it means the system is configured for multi-tenancy. In this case, it creates an instance of `IndyLedgerRequestsExecutor` with the profile. If not, it simply gets the `IndyLedgerRequestsExecutor` from the profile. ```python multitenant_mgr = profile.inject_or(BaseMultitenantManager) if multitenant_mgr: ledger_exec_inst = IndyLedgerRequestsExecutor(profile) else: ledger_exec_inst = profile.inject(IndyLedgerRequestsExecutor) ``` - It then tries to get the ledger for the given DID. If no ledger instance is configured, it raises a `NoIndyLedger` exception. ```python ledger = ( await ledger_exec_inst.get_ledger_for_identifier( did, txn_record_type=GET_KEY_FOR_DID, ) )[1] if not ledger: raise NoIndyLedger("No Indy ledger instance is configured.") ``` - It then tries to get the recipient key and all endpoints for the DID from the ledger. If it encounters a `LedgerError`, it raises a `DIDNotFound` exception. ```python try: async with ledger: recipient_key = await ledger.get_key_for_did(did) endpoints: Optional[dict] = await ledger.get_all_endpoints_for_did(did) except LedgerError as err: raise DIDNotFound(f"DID {did} could not be resolved") from err ``` - It then creates a `DIDDocumentBuilder` with the DID. ```python builder = DIDDocumentBuilder(DID(did)) ``` - It adds a verification method to the builder using the recipient key, and references this verification method in the authentication and assertion methods of the builder. ```python vmethod = builder.verification_method.add( Ed25519VerificationKey2018, ident="key-1", public_key_base58=recipient_key ) builder.authentication.reference(vmethod.id) builder.assertion_method.reference(vmethod.id) ``` - It then adds services to the builder using the `add_services` method. ```python self.add_services(builder, endpoints, vmethod, service_accept) ``` - Finally, it builds the DID document and returns it serialized. ```python result = builder.build() return result.serialize() ``` It is interesting to see that the did is not stored as a `diddoc` in Indy ledger, instead it is built at the runtime. From `aries_cloudagent/resolver/default/indy.py` ```python= async def _resolve( self, profile: Profile, did: str, service_accept: Optional[Sequence[Text]] = None, ) -> dict: """Resolve an indy DID.""" multitenant_mgr = profile.inject_or(BaseMultitenantManager) if multitenant_mgr: ledger_exec_inst = IndyLedgerRequestsExecutor(profile) else: ledger_exec_inst = profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( did, txn_record_type=GET_KEY_FOR_DID, ) )[1] if not ledger: raise NoIndyLedger("No Indy ledger instance is configured.") try: async with ledger: recipient_key = await ledger.get_key_for_did(did) endpoints: Optional[dict] = await ledger.get_all_endpoints_for_did(did) except LedgerError as err: raise DIDNotFound(f"DID {did} could not be resolved") from err builder = DIDDocumentBuilder(DID(did)) vmethod = builder.verification_method.add( Ed25519VerificationKey2018, ident="key-1", public_key_base58=recipient_key ) builder.authentication.reference(vmethod.id) builder.assertion_method.reference(vmethod.id) self.add_services(builder, endpoints, vmethod, service_accept) result = builder.build() return result.serialize() ``` ### Step 8.2 how `did:web` gets resolved The `_resolve` method of the WebDIDResolver class is responsible for resolving did:web DIDs (Decentralized Identifiers). Here's a step-by-step explanation of what the method does: - It takes three parameters: `profile`, `did`, and `service_accept`. The `did` parameter is the DID to be resolved. - It transforms the `did` into a URL using the `__transform_to_url` method. It works according to https://w3c-ccg.github.io/did-method-web/#read-resolve - It creates an HTTP session and sends a GET request to the URL. - If the response status is 200 (OK), it tries to parse the response text into a `DIDDocument` using the `from_json` method from the `pydid` library. If the parsing is successful, it serializes the `DIDDocument` into a dictionary and returns it. - If the response status is 404 (Not Found), it raises a `DIDNotFound` exception. - If the response status is anything else, it raises a `ResolverError` exception. From `aries_cloudagent/resolver/default/web.py` ```python= async def _resolve( self, profile: Profile, did: str, service_accept: Optional[Sequence[Text]] = None, ) -> dict: """Resolve did:web DIDs.""" url = self.__transform_to_url(did) async with aiohttp.ClientSession() as session: async with session.get(url) as response: if response.status == 200: try: # Validate DIDDoc with pyDID did_doc = DIDDocument.from_json(await response.text()) return did_doc.serialize() except Exception as err: raise ResolverError( "Response was incorrectly formatted" ) from err if response.status == 404: raise DIDNotFound(f"No document found for {did}") raise ResolverError( "Could not find doc for {}: {}".format(did, await response.text()) ) ``` ## Out of Band invitation (`POST /out-of-band/create-invitation`) The POST /out-of-band/create-invitation API endpoint is implemented in the `invitation_create` function in the `aries_cloudagent/protocols/out_of_band/v1_0/routes.py` file. Here's a brief overview of how it works: - The function first retrieves the request context and the request body. If the request body exists, it is parsed as JSON. ```python context: AdminRequestContext = request["context"] body = await request.json() if request.body_exists else {} ``` - It then retrieves various parameters from the request body, such as `attachments`, `handshake_protocols`, `use_public_did`, `use_did`, `use_did_method`, `metadata`, `my_label`, `alias`, `mediation_id`, `protocol_version`, `goal_code`, `goal`, and others. ```python attachments = body.get("attachments") handshake_protocols = body.get("handshake_protocols", []) service_accept = body.get("accept") use_public_did = body.get("use_public_did", False) use_did = body.get("use_did") use_did_method = body.get("use_did_method") metadata = body.get("metadata") my_label = body.get("my_label") alias = body.get("alias") mediation_id = body.get("mediation_id") protocol_version = body.get("protocol_version") goal_code = body.get("goal_code") goal = body.get("goal") multi_use = json.loads(request.query.get("multi_use", "false")) auto_accept = json.loads(request.query.get("auto_accept", "null")) create_unique_did = json.loads(request.query.get("create_unique_did", "false")) ``` - It then creates an instance of `OutOfBandManager` and calls its `create_invitation` method with the retrieved parameters to create a new invitation. ```python oob_mgr = OutOfBandManager(profile) try: invi_rec = await oob_mgr.create_invitation( my_label=my_label, auto_accept=auto_accept, public=use_public_did, use_did=use_did, use_did_method=use_did_method, hs_protos=[ h for h in [HSProto.get(hsp) for hsp in handshake_protocols] if h ], multi_use=multi_use, create_unique_did=create_unique_did, attachments=attachments, metadata=metadata, alias=alias, ``` - If the `create_invitation` method call is successful, the function returns a JSON response with the serialized invitation record. If an error occurs during the process, it raises an HTTP 400 Bad Request error with the reason for the error. ```python return web.json_response(invi_rec.serialize()) ``` - This endpoint is used to create a new connection invitation. The parameters it accepts allow for customization of the invitation, such as whether to use a public DID, the handshake protocols to use, whether the invitation is for multi-use, and more. ## Out of Band invitation (`POST /out-of-band/receive-invitation`) The `POST /out-of-band/receive-invitation` API endpoint is implemented in the `invitation_receive` function in the file `aries_cloudagent/protocols/out_of_band/v1_0/routes.py`. Here's how it works: - The function takes an `aiohttp` request object as an argument. - It retrieves the `AdminRequestContext` from the request context. - It checks if the configuration allows receipt of invitations. If not, it raises an `HTTPForbidden` error. - It creates an instance of `OutOfBandManager` with the profile from the context. - It retrieves the body of the request and several query parameters. - It attempts to deserialize the body into an `InvitationMessage` and pass it to the `receive_invitation` method of the `OutOfBandManager` along with the other parameters. - If there are any errors during this process, it raises an HTTPBadRequest error. - If successful, it returns a JSON response with the serialized result of the `receive_invitation` method. Here's the relevant code: ```python= async def invitation_receive(request: web.BaseRequest): context: AdminRequestContext = request["context"] if context.settings.get("admin.no_receive_invites"): raise web.HTTPForbidden( reason="Configuration does not allow receipt of invitations" ) profile = context.profile oob_mgr = OutOfBandManager(profile) body = await request.json() auto_accept = json.loads(request.query.get("auto_accept", "null")) alias = request.query.get("alias") use_existing_conn = json.loads(request.query.get("use_existing_connection", "true")) mediation_id = request.query.get("mediation_id") try: invitation = InvitationMessage.deserialize(body) result = await oob_mgr.receive_invitation( invitation, auto_accept=auto_accept, alias=alias, use_existing_connection=use_existing_conn, mediation_id=mediation_id, ) except (DIDXManagerError, StorageError, BaseModelError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err return web.json_response(result.serialize()) ``` This function is part of the Out-of-Band protocol, which is used to establish connections between agents. The `POST /out-of-band/receive-invitation` endpoint is used to receive an invitation from another agent. ## DID-Exchange Invitation (`POST /didexchange/create-request`) ### Step 1: function `didx_create_request_implicit` The `didx_create_request_implicit` function is an asynchronous function that handles the creation and sending of a request to an implicit invitation. Here's a step-by-step explanation of how it works: - The function takes an `aiohttp` request object as an argument. - It extracts various parameters from the request query, including `their_public_did`, `my_label`, `my_endpoint`, `mediation_id`, `alias`, `use_public_did`, `use_did`, `use_did_method`, `goal_code`, `goal`, `auto_accept`, and `protocol`. - It retrieves the profile from the request context and creates an instance of `DIDXManager` with the profile. - It then calls the `create_request_implicit` method of the `DIDXManager` instance, passing all the extracted parameters. This method is responsible for creating the DID exchange request. - If the `create_request_implicit` method raises a `StorageNotFoundError`, the function returns a `HTTPNotFound` response with the error message. - If the `create_request_implicit` method raises a `StorageError`, `WalletError`, `DIDXManagerError`, or `BaseModelError`, the function returns a `HTTPBadRequest` response with the error message. - If the `create_request_implicit` method executes successfully, the function returns a `json_response` with the serialized DID exchange request. From `aries_cloudagent/protocols/didexchange/v1_0/routes.py` ```python= async def didx_create_request_implicit(request: web.BaseRequest): """Request handler for creating and sending a request to an implicit invitation. Args: request: aiohttp request object Returns: The resulting connection record details """ context: AdminRequestContext = request["context"] their_public_did = request.query["their_public_did"] my_label = request.query.get("my_label") or None my_endpoint = request.query.get("my_endpoint") or None mediation_id = request.query.get("mediation_id") or None alias = request.query.get("alias") or None use_public_did = json.loads(request.query.get("use_public_did", "null")) use_did = request.query.get("use_did") or None use_did_method = request.query.get("use_did_method") or None goal_code = request.query.get("goal_code") or None goal = request.query.get("goal") or None auto_accept = json.loads(request.query.get("auto_accept", "null")) protocol = request.query.get("protocol") or None profile = context.profile didx_mgr = DIDXManager(profile) try: didx_request = await didx_mgr.create_request_implicit( their_public_did=their_public_did, my_label=my_label, my_endpoint=my_endpoint, mediation_id=mediation_id, use_public_did=use_public_did, use_did=use_did, use_did_method=use_did_method, alias=alias, goal_code=goal_code, goal=goal, auto_accept=auto_accept, protocol=protocol, ) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except (StorageError, WalletError, DIDXManagerError, BaseModelError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err return web.json_response(didx_request.serialize()) ``` The `DIDXManager` class is used for managing connections under RFC 23 (DID exchange). It's part of the `aries_cloudagent/protocols/didexchange/v1_0/manager.py` file. This class is responsible for handling the creation, retrieval, and management of DID exchange connections. It inherits from the `BaseConnectionManager` class. ### Step 2: function `create_request_implicit` This function creates a DID exchange request for implicit invitation. - It first retrives the public did(if public did is enabled in request), otherwise it takes the did provided by user. - Then it retrieves the connection record by using `my_did` and `their_did`. If connection already exists it throws error. - It sets the parameters and creates a connection record by instantiating a `ConnRecord` instance. - Then it creates the invitation request by calling `create_request` for the other party and then returns the connection record. From `aries_cloudagent/protocols/didexchange/v1_0/manager.py` ```python= async def create_request_implicit( self, their_public_did: str, my_label: Optional[str] = None, my_endpoint: Optional[str] = None, mediation_id: Optional[str] = None, use_public_did: bool = False, alias: Optional[str] = None, goal_code: Optional[str] = None, goal: Optional[str] = None, auto_accept: bool = False, protocol: Optional[str] = None, use_did: Optional[str] = None, use_did_method: Optional[str] = None, ) -> ConnRecord: """Create and send a request against a public DID only (no explicit invitation). Args: their_public_did: public DID to which to request a connection my_label: my label for request my_endpoint: my endpoint mediation_id: record id for mediation with routing_keys, service endpoint use_public_did: use my public DID for this connection goal_code: Optional self-attested code for sharing intent of connection goal: Optional self-attested string for sharing intent of connection auto_accept: auto-accept a corresponding connection request Returns: The new `ConnRecord` instance """ if use_did and use_did_method: raise DIDXManagerError("Cannot specify both use_did and use_did_method") if use_public_did and use_did: raise DIDXManagerError("Cannot specify both use_public_did and use_did") if use_public_did and use_did_method: raise DIDXManagerError( "Cannot specify both use_public_did and use_did_method" ) my_info = None async with self.profile.session() as session: wallet = session.inject(BaseWallet) if use_public_did: my_info = await wallet.get_public_did() if not my_info: raise WalletError("No public DID configured") if ( my_info.did == their_public_did or f"did:sov:{my_info.did}" == their_public_did ): raise DIDXManagerError( "Cannot connect to yourself through public DID" ) elif use_did: my_info = await wallet.get_local_did(use_did) if my_info: try: await ConnRecord.retrieve_by_did( session, their_did=their_public_did, my_did=my_info.did, ) raise DIDXManagerError( "Connection already exists for their_did " f"{their_public_did} and my_did {my_info.did}" ) except StorageNotFoundError: pass auto_accept = bool( auto_accept or ( auto_accept is None and self.profile.settings.get("debug.auto_accept_requests") ) ) protocol = protocol or DIDEX_1_0 conn_rec = ConnRecord( my_did=( my_info.did if my_info else None ), # create-request will fill in on local DID creation their_did=their_public_did, their_label=None, their_role=ConnRecord.Role.RESPONDER.rfc23, invitation_key=None, invitation_msg_id=None, alias=alias, their_public_did=their_public_did, connection_protocol=protocol, accept=ConnRecord.ACCEPT_AUTO if auto_accept else ConnRecord.ACCEPT_MANUAL, ) request = await self.create_request( # saves and updates conn_rec conn_rec=conn_rec, my_label=my_label, my_endpoint=my_endpoint, mediation_id=mediation_id, goal_code=goal_code, goal=goal, use_did_method=use_did_method, ) conn_rec.request_id = request._id conn_rec.state = ConnRecord.State.REQUEST.rfc160 async with self.profile.session() as session: await conn_rec.save(session, reason="Created connection request") responder = self.profile.inject_or(BaseResponder) if responder: await responder.send(request, connection_id=conn_rec.connection_id) return conn_rec ``` ### Step 3: function `create_request` - This function validates the input, creates parameters for registering a request. - If `their_public_did` is present in connection request, it tries to resolve the did by calling `resolve_didcomm_services` function, which internally calls the previously discussed `resolver.resolve` function. Then it checks the services. From `aries_cloudagent/protocols/didexchange/v1_0/manager.py` ```python= async def create_request( self, conn_rec: ConnRecord, my_label: Optional[str] = None, my_endpoint: Optional[str] = None, mediation_id: Optional[str] = None, goal_code: Optional[str] = None, goal: Optional[str] = None, use_did_method: Optional[str] = None, ) -> DIDXRequest: """Create a new connection request for a previously-received invitation. Args: conn_rec: The `ConnRecord` representing the invitation to accept my_label: My label for request my_endpoint: My endpoint mediation_id: The record id for mediation that contains routing_keys and service endpoint goal_code: Optional self-attested code for sharing intent of connection goal: Optional self-attested string for sharing intent of connection Returns: A new `DIDXRequest` message to send to the other agent """ if use_did_method and use_did_method not in self.SUPPORTED_USE_DID_METHODS: raise DIDXManagerError( f"Unsupported use_did_method: {use_did_method}. Supported methods: " f"{self.SUPPORTED_USE_DID_METHODS}" ) # Mediation Support mediation_records = await self._route_manager.mediation_records_for_connection( self.profile, conn_rec, mediation_id, or_default=True, ) if my_endpoint: my_endpoints = [my_endpoint] else: my_endpoints = [] default_endpoint = self.profile.settings.get("default_endpoint") if default_endpoint: my_endpoints.append(default_endpoint) my_endpoints.extend(self.profile.settings.get("additional_endpoints", [])) if not my_label: my_label = self.profile.settings.get("default_label") assert my_label did_url = None if conn_rec.their_public_did is not None: services = await self.resolve_didcomm_services(conn_rec.their_public_did) if services: did_url = services[0].id pthid = conn_rec.invitation_msg_id or did_url if conn_rec.connection_protocol == DIDEX_1_0: did, attach = await self._legacy_did_with_attached_doc( conn_rec, my_endpoints, mediation_records ) else: if conn_rec.accept == ConnRecord.ACCEPT_AUTO or use_did_method is None: # If we're auto accepting or engaging in 1.1 without setting a # use_did_method, default to did:peer:4 use_did_method = "did:peer:4" try: did, attach = await self._qualified_did_with_fallback( conn_rec, my_endpoints, mediation_records, use_did_method, ) except LegacyHandlingFallback: did, attach = await self._legacy_did_with_attached_doc( conn_rec, my_endpoints, mediation_records ) request = DIDXRequest( label=my_label, did=did, did_doc_attach=attach, goal=goal, goal_code=goal_code, ) if conn_rec.connection_protocol == DIDEX_1_0: request.assign_version("1.0") request.assign_thread_id(thid=request._id, pthid=pthid) # Update connection state conn_rec.request_id = request._id conn_rec.state = ConnRecord.State.REQUEST.rfc160 async with self.profile.session() as session: await conn_rec.save(session, reason="Created connection request") # Idempotent; if routing has already been set up, no action taken await self._route_manager.route_connection_as_invitee( self.profile, conn_rec, mediation_records ) return request ``` ## DID Exchange receive invitation (`POST /didexchange/receive-request`) The `POST /didexchange/receive-request` API endpoint is used to accept a stored connection request. ### Step 1: function `didx_receive_request_implicit` - It extracts the requred data from request body, and then instantiates a `DIDXManager` instance - It retrieves the `didx_request` from the `didx_mgr` by passing on the body. - It recieves the request by calling `didx_mgr.receive_request` function. Then it returns the result. From `aries_cloudagent/protocols/didexchange/v1_0/routes.py` ```python= async def didx_receive_request_implicit(request: web.BaseRequest): """Request handler for receiving a request against public DID's implicit invitation. Args: request: aiohttp request object Returns: The resulting connection record details """ context: AdminRequestContext = request["context"] body = await request.json() alias = request.query.get("alias") auto_accept = json.loads(request.query.get("auto_accept", "null")) profile = context.profile didx_mgr = DIDXManager(profile) try: didx_request = DIDXRequest.deserialize(body) conn_rec = await didx_mgr.receive_request( request=didx_request, recipient_did=didx_request._thread.pthid.split(":")[-1], alias=alias, auto_accept_implicit=auto_accept, ) result = conn_rec.serialize() except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except (StorageError, WalletError, DIDXManagerError, BaseModelError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err return web.json_response(result) ``` ### Step 2: Function `receive_request` - If the function recieves a recipient `verkey`, it receives a DID Exchange request against a pairwise (not public) DID (`_receive_request_pairwise_did`). Otherwise it recieves against the public did (`_receive_request_public_did`). ### Step 3.1: Function `_receive_request_pairwise_did` - Here it first tries to get the connection record, otherwise create one if it does not exists. - Then it extracts details from body like `alias`, `their_did`, `request_id`, etc. - If did doc is given, it matches the request did with the did doc. - If did doc is not present, it resolves the did (using `resolve_didcomm_services` present in `aries_cloudagent/connections/base_manager.py`, it internally uses the resolve method of normal dids). ### Step 3.2: Function `_receive_request_public_did` - It first checks if public invites are enabled - Then it gets the public did and connection record. - Then it extracts the did doc matches it with the did like previous. If it is not present, it resolves the did. ## Issue Credential (`POST /issue-credential-2.0/send-offer`) The `POST /issue-credential-2.0/send-offer` endpoint is implemented in the `aries_cloudagent/protocols/issue_credential/v2_0/routes.py` file. This endpoint is used to send a credential offer to a holder. ### Step 1: function `credential_exchange_send_free_offer` - The endpoint handler function receives a web request object as an argument. This request object contains the necessary data to create and send a credential offer. - The handler function extracts the necessary data from the request object. This data includes the connection ID, filter specification, auto-issue flag, auto-remove flag, replacement ID, credential preview, and comment. - The handler function then calls the `_create_free_offer` function, passing in the extracted data. This function creates a credential offer and a related exchange record. - The handler function then saves the credential exchange record and sends the credential offer message to the holder. - Finally, the handler function returns a JSON response containing the result of the operation. from `aries_cloudagent/protocols/issue_credential/v2_0/routes.py` ```python= async def credential_exchange_send_free_offer(request: web.BaseRequest): """Request handler for sending free credential offer. An issuer initiates a such a credential offer, free from any holder-initiated corresponding credential proposal with preview. Args: request: aiohttp request object Returns: The credential exchange record """ r_time = get_timer() context: AdminRequestContext = request["context"] profile = context.profile outbound_handler = request["outbound_message_router"] body = await request.json() connection_id = body.get("connection_id") filt_spec = body.get("filter") if not filt_spec: raise web.HTTPBadRequest(reason="Missing filter") auto_issue = body.get( "auto_issue", context.settings.get("debug.auto_respond_credential_request") ) auto_remove = body.get( "auto_remove", not profile.settings.get("preserve_exchange_records") ) replacement_id = body.get("replacement_id") comment = body.get("comment") preview_spec = body.get("credential_preview") trace_msg = body.get("trace") cred_ex_record = None conn_record = None try: async with profile.session() as session: conn_record = await ConnRecord.retrieve_by_id(session, connection_id) if not conn_record.is_ready: raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready") cred_ex_record, cred_offer_message = await _create_free_offer( profile=profile, filt_spec=filt_spec, connection_id=connection_id, auto_issue=auto_issue, auto_remove=auto_remove, preview_spec=preview_spec, comment=comment, trace_msg=trace_msg, replacement_id=replacement_id, ) result = cred_ex_record.serialize() except ( BaseModelError, AnonCredsIssuerError, IndyIssuerError, LedgerError, StorageNotFoundError, V20CredFormatError, V20CredManagerError, ) as err: LOGGER.exception("Error preparing free credential offer") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state(session, reason=err.roll_up) # other party cannot yet receive a problem report about our failed protocol start raise web.HTTPBadRequest(reason=err.roll_up) await outbound_handler(cred_offer_message, connection_id=connection_id) trace_event( context.settings, cred_offer_message, outcome="credential_exchange_send_free_offer.END", perf_counter=r_time, ) return web.json_response(result) ``` ### Step 2: function `_create_free_offer` - The `_create_free_offer` function first creates a `V20CredProposal` object using the provided filter specification and credential preview. It then creates a `V20CredExRecord` object using the connection ID, auto-issue flag, auto-remove flag, and the serialized credential proposal. - The `_create_free_offer` function then creates a `V20CredManager` object and calls its `create_offer` method, passing in the `V20CredExRecord` object, comment, and replacement ID. This method returns a credential exchange record and a credential offer message. From `aries_cloudagent/protocols/issue_credential/v2_0/routes.py` ```python= async def _create_free_offer( profile: Profile, filt_spec: Mapping = None, connection_id: str = None, auto_issue: bool = False, auto_remove: bool = False, replacement_id: str = None, preview_spec: dict = None, comment: str = None, trace_msg: bool = None, ): """Create a credential offer and related exchange record.""" cred_preview = V20CredPreview.deserialize(preview_spec) if preview_spec else None cred_proposal = V20CredProposal( comment=comment, credential_preview=cred_preview, **_formats_filters(filt_spec), ) cred_proposal.assign_trace_decorator( profile.settings, trace_msg, ) cred_ex_record = V20CredExRecord( connection_id=connection_id, initiator=V20CredExRecord.INITIATOR_SELF, role=V20CredExRecord.ROLE_ISSUER, cred_proposal=cred_proposal.serialize(), auto_issue=auto_issue, auto_remove=auto_remove, trace=trace_msg, ) cred_manager = V20CredManager(profile) (cred_ex_record, cred_offer_message) = await cred_manager.create_offer( cred_ex_record, comment=comment, replacement_id=replacement_id, ) return (cred_ex_record, cred_offer_message) ``` ## Send Credential Request (`POST /issue-credential-2.0/records/{cred_ex_id}/send-request`) ### Step 1: function `credential_exchange_send_free_request` From `aries_cloudagent/protocols/issue_credential/v2_0/routes.py` ```python= async def credential_exchange_send_free_request(request: web.BaseRequest): """Request handler for sending free credential request. Args: request: aiohttp request object Returns: The credential exchange record """ r_time = get_timer() context: AdminRequestContext = request["context"] profile = context.profile outbound_handler = request["outbound_message_router"] body = await request.json() connection_id = body.get("connection_id") comment = body.get("comment") filt_spec = body.get("filter") if not filt_spec: raise web.HTTPBadRequest(reason="Missing filter") auto_remove = body.get( "auto_remove", not profile.settings.get("preserve_exchange_records") ) trace_msg = body.get("trace") holder_did = body.get("holder_did") conn_record = None cred_ex_record = None try: try: async with profile.session() as session: conn_record = await ConnRecord.retrieve_by_id(session, connection_id) if not conn_record.is_ready: raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready") except StorageNotFoundError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err cred_manager = V20CredManager(profile) cred_proposal = V20CredProposal( comment=comment, **_formats_filters(filt_spec), ) cred_ex_record = V20CredExRecord( connection_id=connection_id, auto_remove=auto_remove, cred_proposal=cred_proposal.serialize(), initiator=V20CredExRecord.INITIATOR_SELF, role=V20CredExRecord.ROLE_HOLDER, trace=trace_msg, ) cred_ex_record, cred_request_message = await cred_manager.create_request( cred_ex_record=cred_ex_record, holder_did=holder_did, comment=comment, ) result = cred_ex_record.serialize() except ( BaseModelError, AnonCredsHolderError, IndyHolderError, LedgerError, StorageError, V20CredManagerError, ) as err: LOGGER.exception("Error preparing free credential request") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state(session, reason=err.roll_up) # other party cannot yet receive a problem report about our failed protocol start raise web.HTTPBadRequest(reason=err.roll_up) await outbound_handler(cred_request_message, connection_id=connection_id) trace_event( context.settings, cred_request_message, outcome="credential_exchange_send_free_request.END", perf_counter=r_time, ) return web.json_response(result) ``` ### Step 2: function `cred_manager.create_request` From `aries_cloudagent/protocols/issue_credential/v2_0/manager.py` ```python= async def create_request( self, cred_ex_record: V20CredExRecord, holder_did: str, comment: str = None ) -> Tuple[V20CredExRecord, V20CredRequest]: """Create a credential request. Args: cred_ex_record: credential exchange record for which to create request holder_did: holder DID comment: optional human-readable comment to set in request message Returns: A tuple (credential exchange record, credential request message) """ if cred_ex_record.cred_request: raise V20CredManagerError( "create_request() called multiple times for " f"v2.0 credential exchange {cred_ex_record.cred_ex_id}" ) # react to credential offer, use offer formats if cred_ex_record.state: if cred_ex_record.state != V20CredExRecord.STATE_OFFER_RECEIVED: raise V20CredManagerError( f"Credential exchange {cred_ex_record.cred_ex_id} " f"in {cred_ex_record.state} state " f"(must be {V20CredExRecord.STATE_OFFER_RECEIVED})" ) cred_offer = cred_ex_record.cred_offer input_formats = cred_offer.formats # start with request (not allowed for indy -> checked in indy format handler) # use proposal formats else: cred_proposal = cred_ex_record.cred_proposal input_formats = cred_proposal.formats request_formats = [] # Format specific create_request handler for format in input_formats: cred_format = V20CredFormat.Format.get(format.format) if cred_format: request_formats.append( await cred_format.handler(self.profile).create_request( cred_ex_record, {"holder_did": holder_did} ) ) if len(request_formats) == 0: raise V20CredManagerError( "Unable to create credential request. No supported formats" ) cred_request_message = V20CredRequest( comment=comment, formats=[format for (format, _) in request_formats], requests_attach=[attach for (_, attach) in request_formats], ) # Assign thid (and optionally pthid) to message cred_request_message.assign_thread_from(cred_ex_record.cred_offer) cred_request_message.assign_trace_decorator( self._profile.settings, cred_ex_record.trace ) cred_ex_record.thread_id = cred_request_message._thread_id cred_ex_record.state = V20CredExRecord.STATE_REQUEST_SENT cred_ex_record.cred_request = cred_request_message async with self._profile.session() as session: await cred_ex_record.save(session, reason="create v2.0 credential request") return (cred_ex_record, cred_request_message) ``` If `cred_format` is given it creates the credential according to that format. Formats include Indy, LD proof etc. ### Step 3.1 Indy credential #### Function `create_request` From `aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py` ```python= async def create_request( self, cred_ex_record: V20CredExRecord, request_data: Mapping = None ) -> CredFormatAttachment: """Create indy credential request.""" # Temporary shim while the new anoncreds library integration is in progress if self.anoncreds_handler: return await self.anoncreds_handler.create_request( cred_ex_record, request_data, ) if cred_ex_record.state != V20CredExRecord.STATE_OFFER_RECEIVED: raise V20CredFormatError( "Indy issue credential format cannot start from credential request" ) await self._check_uniqueness(cred_ex_record.cred_ex_id) holder_did = request_data.get("holder_did") if request_data else None cred_offer = cred_ex_record.cred_offer.attachment(IndyCredFormatHandler.format) if "nonce" not in cred_offer: raise V20CredFormatError("Missing nonce in credential offer") nonce = cred_offer["nonce"] cred_def_id = cred_offer["cred_def_id"] async def _create(): multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) if multitenant_mgr: ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) else: ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( cred_def_id, txn_record_type=GET_CRED_DEF, ) )[1] async with ledger: cred_def = await ledger.get_credential_definition(cred_def_id) holder = self.profile.inject(IndyHolder) request_json, metadata_json = await holder.create_credential_request( cred_offer, cred_def, holder_did ) return { "request": json.loads(request_json), "metadata": json.loads(metadata_json), } cache_key = f"credential_request::{cred_def_id}::{holder_did}::{nonce}" cred_req_result = None cache = self.profile.inject_or(BaseCache) if cache: async with cache.acquire(cache_key) as entry: if entry.result: cred_req_result = entry.result else: cred_req_result = await _create() await entry.set_result(cred_req_result, 3600) if not cred_req_result: cred_req_result = await _create() detail_record = V20CredExRecordIndy( cred_ex_id=cred_ex_record.cred_ex_id, cred_request_metadata=cred_req_result["metadata"], ) async with self.profile.session() as session: await detail_record.save(session, reason="create v2.0 credential request") return self.get_format_data(CRED_20_REQUEST, cred_req_result["request"]) ``` #### Function `holder.create_credential_request` This is an abstract method and is defined for issuer holder verifier seperately. From `aries_cloudagent/indy/credx/holder.py` ```python= async def create_credential_request( self, credential_offer: dict, credential_definition: dict, holder_did: str ) -> Tuple[str, str]: """Create a credential request for the given credential offer. Args: credential_offer: The credential offer to create request for credential_definition: The credential definition to create an offer for holder_did: the DID of the agent making the request Returns: A tuple of the credential request and credential request metadata """ try: secret = await self.get_link_secret() ( cred_req, cred_req_metadata, ) = await asyncio.get_event_loop().run_in_executor( None, CredentialRequest.create, holder_did, credential_definition, secret, IndyCredxHolder.LINK_SECRET_ID, credential_offer, ) except CredxError as err: raise IndyHolderError("Error creating credential request") from err cred_req_json, cred_req_metadata_json = ( cred_req.to_json(), cred_req_metadata.to_json(), ) LOGGER.debug( "Created credential request. " "credential_request_json=%s credential_request_metadata_json=%s", cred_req_json, cred_req_metadata_json, ) return cred_req_json, cred_req_metadata_json ``` Note: This call stack does not use did functionalities. ## Credential offer (POST `/issue-credential-2.0/records/{cred_ex_id}/send-offer`) ### `credential_exchange_send_bound_offer` At `aries_cloudagent/protocols/issue_credential /v2_0/routes.py` ### `create_offer` At `aries_cloudagent/protocols/issue_credential/v2_0/manager.py` ### `Create offer` as per cred handler (indy/JSON-LD) #### For LD-Proof: - Function `create_offer` at `aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py` - `assert_can_issue_with_id_and_proof_type` at `aries_cloudagent/vc/vc_ld/manager.py` - `_did_info_for_did` at `aries_cloudagent/vc/vc_ld/manager.py` - For askar wallet, `get_local_did` at `aries_cloudagent/wallet/askar.py` ## Issue Credential (`POST /issue-credential-2.0/records/{cred_ex_id}/issue`) ### Step 1: Function `credential_exchange_issue` ### Step 2: Function `cred_manager.issue_credential` ### Step 3: Function `cred_format.handler(self.profile).issue_credential` This function is format specific. For indy: ### Step 4: `issue_credential` at `aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py` ## Credential issue handler `(Receive Credential callback) /issue_credential_v2_0/` ### Step 1: `handle` function At `aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_issue_handler.py` ### Step 2: Function `find_oob_record_for_inbound_message` This function tries to get the OOB(connection) record for the credential issue recieved. If OOB record is not found it gets the connection record from request context. Function at `aries_cloudagent/core/oob_processor.py` ### Step 3: Function `store_credential` Stores credential as per format. ### Step 4: Function `send_cred_ack` ## POST `/issue-credential-2.0/records/{cred_ex_id}/store` ### Step 1: Function `credential_exchange_store` At `aries_cloudagent/protocols/issue_credential/v2_0/routes.py` ### Step 2: Get connection details following step 2 of prev section ### Step 3: Function `store_credential` At `aries_cloudagent/protocols/issue_credential/v2_0/manager.py` Store the credential as per format. ## POST `/issue-credential-2.0/records/{cred_ex_id}/send-offer` Similiar to endpoint POST `/issue-credential-2.0/send-offer` ## POST `/issue-credential-2.0/send-proposal` ### Step 1: Function `credential_exchange_send_proposal` At `aries_cloudagent/protocols/issue_credential/v2_0/routes.py` It calls `ConnRecord.retrieve_by_id` to get the connection record by ID. ### Step 2: Function `create_proposal` At `aries_cloudagent/protocols/issue_credential/v2_0/manager.py` Creates a proposal as per the format. ## POST `/issue-credential-2.0/records/{cred_ex_id}/problem-report` ### Step 1: Function `credential_exchange_problem_report` At`aries_cloudagent/protocols/issue_credential/v2_0/routes.py` It extracts the `cred_ex_record` and updates it. ### Step 2: Function `problem_report_for_record` At `aries_cloudagent/protocols/issue_credential/v2_0/__init__.py` Creates a `V20CredProblemReport` object and returns it. ## POST `/present-proof-2.0/send-request` ### Step 1: `present_proof_send_free_request` At `aries_cloudagent/protocols/present_proof/v2_0/routes.py` ### Step 2: Retireve Connecton record from `ConnRecord.retrieve_by_id` At `aries_cloudagent/messaging/models/base_record.py` ### Step 3: Create request object using `V20PresRequest` constructor At `aries_cloudagent/protocols/present_proof/v2_0/messages/pres_request.py` ### Step 4: Create a presentation exchange record by function `create_exchange_for_request` At `aries_cloudagent/protocols/present_proof/v2_0/manager.py` #### Step 4.1: Create a exchange record using constructor `V20PresExRecord` At `aries_cloudagent/protocols/present_proof/v2_0/models/pres_exchange.py` ### Step 5: Serialize and send the message to the connection. At `aries_cloudagent/protocols/present_proof/v2_0/routes.py` ## (Receive Request callback) `/present_proof_v2_0/` ### Step 1: Function `handle` At `aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_request_handler.py` #### Check if connection is ready ```python # If connection is present it must be ready for use if context.connection_record and not context.connection_ready: raise HandlerException("Connection used for presentation request not ready") # Find associated oob record oob_processor = context.inject(OobMessageProcessor) oob_record = await oob_processor.find_oob_record_for_inbound_message(context) # Either connection or oob context must be present if not context.connection_record and not oob_record: raise HandlerException( "No connection or associated connectionless exchange found for" " presentation request" ) ``` ### Step 2: Create another proposal for the holder using `V20PresExRecord` At `aries_cloudagent/protocols/present_proof/v2_0/models/pres_exchange.py` ## Send Presentation `POST /present-proof-2.0/records/{pres_ex_id}/send-presentation` ### `present_proof_send_presentation` At `aries_cloudagent/protocols/present_proof/v2_0/routes.py` ### `create_pres` At `aries_cloudagent/protocols/present_proof/v2_0/manager.py` ### `create_pres` as per handler (indy/LD) #### For Indy - `create_pres` at `aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py` - `indy_proof_req_preview2indy_requested_creds` at `aries_cloudagent/indy/models/xform.py` #### For LD - `create_pres` at `aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py` ## (Receive Proof callback) `/present_proof_v2_0/` ### Get connection details: `find_oob_record_for_inbound_message` At `aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_handler.py` ### `receive_pres` At `aries_cloudagent/protocols/present_proof/v2_0/manager.py` ## POST `/present-proof-2.0/records/{pres_ex_id}/verify-presentation` ### `present_proof_verify_presentation` At `aries_cloudagent/protocols/present_proof/v2_0/routes.py` ### `verify_pres` At `aries_cloudagent/protocols/present_proof/v2_0/manager.py` ### `verify_pres` as per handler For indy - `verify_pres` at `aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py` For JSON-LD - `verify_pres` at `aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py`