Repository: hyperledger/aries-cloudagent-python

Issue:
DID Exchange - Issue with implicit invitations
I'm experimenting with the DID Exchange protocol in order to create a connection based on a public DIDs implicit invitation.
A public DID allows to retrieve the necessary keys and service information from a given VDR, i.e. all information that is needed to pack and send the respective request message.

ACA-Py currently provides the following APIs:

![image](https://user-images.githubusercontent.com/1970182/116874306-6561cc00-ac19-11eb-9902-0fc38637b7a5.png)

`create-request` allows to create a connection request and returns it, but there seems to be no exposed functionality to actually send the request. 

I tried a simple hack adding
```
 # Send request
        responder = self._session.inject(BaseResponder, required=False)
        if responder:
            await responder.send(request, connection_id=conn_rec.connection_id)
```
for testing, but `fetch_connection_targets` fails because there's no invitation attached to the connection.

In addition, I don't understand the use case for `receive-request`, since I'd say requests should use the DIDcomm channel. Everything else should use the Out of Band API.




Discussion:
Ok, what does work is using `/out-of-band/receive-invitation` with a payload like this:
```
{
  "@id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "handshake_protocols": [
    "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0"
  ],
  "services": [
    "did:web:478eb39f6464.ngrok.io"
  ]
}
```

So this works similar to the connections API.
@domwoe while that works, sounds like the `create-request` approach should work.  Do you agree?  Or are we good with what you found did work?

I'm kind of with your initial comment -- that if you know a public DID with a DIDComm service block, you should be able to use it without having to use the OOB protocol.  However, not sure what complexities that might create within ACA-Py.
@swcurran Yes, I agree. I don't know the use cases for the `create-request` and `receive-request` routes, but a `create and send request` api would make sense. We could either extend the `create-request` route or add an additional route. 

From an implementation point of view I think there should be an additional condition in `BaseConnectionManager.fetch_connection_targets` that catches the case where no invitation is present and gets the connection target information from `connection.their_did` and the resolver.
@shaangill025 -- please add to your list.  Thanks!
Possible implementation would be [here](https://github.com/boschresearch/aries-cloudagent-python/tree/fix/1137-send-didx-request) but not sure it's the way to go.

This leaves the current API untouched but adds `POST
​/didexchange​/send-request` with same parameters as `create-request`. It sends the request and returns the connection record.

Resolving patch:
diff --git a/aries_cloudagent/connections/base_manager.py b/aries_cloudagent/connections/base_manager.py
--- a/aries_cloudagent/connections/base_manager.py
+++ b/aries_cloudagent/connections/base_manager.py
@@ -289,45 +289,56 @@ async def fetch_connection_targets(
             in (ConnRecord.State.INVITATION, ConnRecord.State.REQUEST)
             and ConnRecord.Role.get(connection.their_role) is ConnRecord.Role.RESPONDER
         ):
-            invitation = await connection.retrieve_invitation(self._session)
-            if isinstance(invitation, ConnectionInvitation):  # conn protocol invitation
-                if invitation.did:
-                    did = invitation.did
+            if connection.invitation_msg_id or connection.invitation_key:
+                invitation = await connection.retrieve_invitation(self._session)
+                if isinstance(
+                    invitation, ConnectionInvitation
+                ):  # conn protocol invitation
+                    if invitation.did:
+                        did = invitation.did
+                        (
+                            endpoint,
+                            recipient_keys,
+                            routing_keys,
+                        ) = await self.resolve_invitation(did)
+
+                    else:
+                        endpoint = invitation.endpoint
+                        recipient_keys = invitation.recipient_keys
+                        routing_keys = invitation.routing_keys
+                else:  # out-of-band invitation
+                    if invitation.service_dids:
+                        did = invitation.service_dids[0]
+                        (
+                            endpoint,
+                            recipient_keys,
+                            routing_keys,
+                        ) = await self.resolve_invitation(did)
+
+                    else:
+                        endpoint = invitation.service_blocks[0].service_endpoint
+                        recipient_keys = [
+                            DIDKey.from_did(k).public_key_b58
+                            for k in invitation.service_blocks[0].recipient_keys
+                        ]
+                        routing_keys = [
+                            DIDKey.from_did(k).public_key_b58
+                            for k in invitation.service_blocks[0].routing_keys
+                        ]
+            else:
+                if connection.their_did:
+                    invitation = None
+                    did = connection.their_did
                     (
                         endpoint,
                         recipient_keys,
                         routing_keys,
                     ) = await self.resolve_invitation(did)
-
-                else:
-                    endpoint = invitation.endpoint
-                    recipient_keys = invitation.recipient_keys
-                    routing_keys = invitation.routing_keys
-            else:  # out-of-band invitation
-                if invitation.service_dids:
-                    did = invitation.service_dids[0]
-                    (
-                        endpoint,
-                        recipient_keys,
-                        routing_keys,
-                    ) = await self.resolve_invitation(did)
-
-                else:
-                    endpoint = invitation.service_blocks[0].service_endpoint
-                    recipient_keys = [
-                        DIDKey.from_did(k).public_key_b58
-                        for k in invitation.service_blocks[0].recipient_keys
-                    ]
-                    routing_keys = [
-                        DIDKey.from_did(k).public_key_b58
-                        for k in invitation.service_blocks[0].routing_keys
-                    ]
-
             results = [
                 ConnectionTarget(
                     did=connection.their_did,
                     endpoint=endpoint,
-                    label=invitation.label,
+                    label=invitation.label if invitation else None,
                     recipient_keys=recipient_keys,
                     routing_keys=routing_keys,
                     sender_key=my_info.verkey,
diff --git a/aries_cloudagent/protocols/didexchange/v1_0/manager.py b/aries_cloudagent/protocols/didexchange/v1_0/manager.py
--- a/aries_cloudagent/protocols/didexchange/v1_0/manager.py
+++ b/aries_cloudagent/protocols/didexchange/v1_0/manager.py
@@ -162,6 +162,7 @@ async def create_request_implicit(
         my_label: str = None,
         my_endpoint: str = None,
         mediation_id: str = None,
+        send_request: bool = False,
     ) -> DIDXRequest:
         """
         Create a request against a public DID only (no explicit invitation).
@@ -184,17 +185,25 @@ async def create_request_implicit(
             their_role=ConnRecord.Role.RESPONDER.rfc23,
             invitation_key=None,
             invitation_msg_id=None,
-            state=ConnRecord.State.REQUEST.rfc23,
             accept=None,
             alias=my_label,
             their_public_did=their_public_did,
         )
-        return await self.create_request(  # saves and updates conn_rec
+        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,
         )
+        conn_rec.request_id = request._id
+        conn_rec.state = ConnRecord.State.REQUEST.rfc23
+        await conn_rec.save(self._session, reason="Created connection request")
+        if send_request:
+            responder = self._session.inject(BaseResponder, required=False)
+            if responder:
+                await responder.send(request, connection_id=conn_rec.connection_id)
+
+        return request
 
     async def create_request(
         self,
diff --git a/aries_cloudagent/protocols/didexchange/v1_0/routes.py b/aries_cloudagent/protocols/didexchange/v1_0/routes.py
--- a/aries_cloudagent/protocols/didexchange/v1_0/routes.py
+++ b/aries_cloudagent/protocols/didexchange/v1_0/routes.py
@@ -53,6 +53,9 @@ class DIDXCreateRequestImplicitQueryStringSchema(OpenAPISchema):
         description="Identifier for active mediation record to be used",
         **UUID4,
     )
+    send_request = fields.Bool(
+        description="Whether to send request for implicit invitations", default=False
+    )
 
 
 class DIDXReceiveRequestImplicitQueryStringSchema(OpenAPISchema):
@@ -179,6 +182,7 @@ async def didx_create_request_implicit(request: web.BaseRequest):
     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
+    send_request = request.query.get("send_request")
 
     didx_mgr = DIDXManager(session)
     try:
@@ -187,6 +191,7 @@ async def didx_create_request_implicit(request: web.BaseRequest):
             my_label=my_label,
             my_endpoint=my_endpoint,
             mediation_id=mediation_id,
+            send_request=send_request,
         )
     except StorageNotFoundError as err:
         raise web.HTTPNotFound(reason=err.roll_up) from err


Test patch:
diff --git a/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py
--- a/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py
+++ b/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py
@@ -2595,6 +2595,45 @@ async def test_fetch_connection_targets_conn_completed_their_did(self):
         assert target.routing_keys == []
         assert target.sender_key == local_did.verkey
 
+    async def test_fetch_connection_targets_conn_no_invi_with_their_did(self):
+        local_did = await self.session.wallet.create_local_did(
+            method=DIDMethod.SOV,
+            key_type=KeyType.ED25519,
+            seed=self.test_seed,
+            did=self.test_did,
+            metadata=None,
+        )
+
+        self.manager.resolve_invitation = async_mock.CoroutineMock()
+        self.manager.resolve_invitation.return_value = (
+            self.test_endpoint,
+            [self.test_verkey],
+            [],
+        )
+
+        did_doc = self.make_did_doc(did=self.test_did, verkey=self.test_verkey)
+        await self.manager.store_did_document(did_doc)
+
+        mock_conn = async_mock.MagicMock(
+            my_did=self.test_did,
+            their_did=self.test_did,
+            their_label="label",
+            their_role=ConnRecord.Role.RESPONDER.rfc23,
+            state=ConnRecord.State.REQUEST.rfc23,
+            invitation_key=None,
+            invitation_msg_id=None,
+        )
+
+        targets = await self.manager.fetch_connection_targets(mock_conn)
+        assert len(targets) == 1
+        target = targets[0]
+        assert target.did == mock_conn.their_did
+        assert target.endpoint == self.test_endpoint
+        assert target.label is None
+        assert target.recipient_keys == [self.test_verkey]
+        assert target.routing_keys == []
+        assert target.sender_key == local_did.verkey
+
     async def test_diddoc_connection_targets_diddoc_underspecified(self):
         with self.assertRaises(BaseConnectionManagerError):
             self.manager.diddoc_connection_targets(None, self.test_verkey)
diff --git a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py
--- a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py
+++ b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py
@@ -19,7 +19,6 @@
 from .....multitenant.manager import MultitenantManager
 from .....storage.error import StorageNotFoundError
 from .....transport.inbound.receipt import MessageReceipt
-from .....multitenant.manager import MultitenantManager
 from .....wallet.did_info import DIDInfo
 from .....wallet.in_memory import InMemoryWallet
 from .....wallet.did_method import DIDMethod
@@ -236,6 +235,35 @@ async def test_create_request_implicit(self):
 
             assert didx_req._id
 
+    async def test_create_send_request_implicit(self):
+        mediation_record = MediationRecord(
+            role=MediationRecord.ROLE_CLIENT,
+            state=MediationRecord.STATE_GRANTED,
+            connection_id=self.test_mediator_conn_id,
+            routing_keys=self.test_mediator_routing_keys,
+            endpoint=self.test_mediator_endpoint,
+        )
+        await mediation_record.save(self.session)
+
+        with async_mock.patch.object(
+            self.manager, "create_did_document", async_mock.CoroutineMock()
+        ) as mock_create_did_doc, async_mock.patch.object(
+            self.multitenant_mgr, "get_default_mediator"
+        ) as mock_get_default_mediator:
+            mock_get_default_mediator.return_value = mediation_record
+            mock_create_did_doc.return_value = async_mock.MagicMock(
+                serialize=async_mock.MagicMock(return_value={})
+            )
+            didx_req = await self.manager.create_request_implicit(
+                their_public_did=TestConfig.test_target_did,
+                my_label=None,
+                my_endpoint=None,
+                mediation_id=mediation_record._id,
+                send_request=True,
+            )
+
+            assert didx_req._id
+
     async def test_create_request(self):
         mock_conn_rec = async_mock.MagicMock(
             connection_id="dummy",