From 997aff5f1680d3cd6dd6897c532f793181ff467c Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 16 Jun 2023 17:00:20 +0200 Subject: [PATCH 01/56] feat(prism-agent): introduce webhook publisher config --- .../src/main/resources/application.conf | 207 +++++++++--------- .../atala/agent/server/config/AppConfig.scala | 9 +- 2 files changed, 114 insertions(+), 102 deletions(-) diff --git a/prism-agent/service/server/src/main/resources/application.conf b/prism-agent/service/server/src/main/resources/application.conf index b2c81eeafc..4dd3d67312 100644 --- a/prism-agent/service/server/src/main/resources/application.conf +++ b/prism-agent/service/server/src/main/resources/application.conf @@ -1,120 +1,125 @@ iris { - service { - host = "localhost" - host = ${?IRIS_HOST} - port = 8081 - port = ${?IRIS_PORT} - } + service { + host = "localhost" + host = ${?IRIS_HOST} + port = 8081 + port = ${?IRIS_PORT} + } } prismNode { - service = { - host = "localhost" - host = ${?PRISM_NODE_HOST} - port = 50053 - port = ${?PRISM_NODE_PORT} - } + service = { + host = "localhost" + host = ${?PRISM_NODE_HOST} + port = 50053 + port = ${?PRISM_NODE_PORT} + } } castor { - database { - host = "localhost" - host = ${?CASTOR_DB_HOST} - port = 5432 - port = ${?CASTOR_DB_PORT} - databaseName = "castor" - databaseName = ${?CASTOR_DB_NAME} - username = "postgres" - username = ${?CASTOR_DB_USER} - password = "postgres" - password = ${?CASTOR_DB_PASSWORD} - awaitConnectionThreads = 8 - } + database { + host = "localhost" + host = ${?CASTOR_DB_HOST} + port = 5432 + port = ${?CASTOR_DB_PORT} + databaseName = "castor" + databaseName = ${?CASTOR_DB_NAME} + username = "postgres" + username = ${?CASTOR_DB_USER} + password = "postgres" + password = ${?CASTOR_DB_PASSWORD} + awaitConnectionThreads = 8 + } } pollux { - database { - host = "localhost" - host = ${?POLLUX_DB_HOST} - port = 5432 - port = ${?POLLUX_DB_PORT} - databaseName = "pollux" - databaseName = ${?POLLUX_DB_NAME} - username = "postgres" - username = ${?POLLUX_DB_USER} - password = "postgres" - password = ${?POLLUX_DB_PASSWORD} - awaitConnectionThreads = 8 - } - issueBgJobRecordsLimit = 25 - issueBgJobRecurrenceDelay = 2 seconds - issueBgJobProcessingParallelism = 5 - presentationBgJobRecordsLimit = 25 - presentationBgJobRecurrenceDelay = 2 seconds - presentationBgJobProcessingParallelism = 5 + database { + host = "localhost" + host = ${?POLLUX_DB_HOST} + port = 5432 + port = ${?POLLUX_DB_PORT} + databaseName = "pollux" + databaseName = ${?POLLUX_DB_NAME} + username = "postgres" + username = ${?POLLUX_DB_USER} + password = "postgres" + password = ${?POLLUX_DB_PASSWORD} + awaitConnectionThreads = 8 + } + issueBgJobRecordsLimit = 25 + issueBgJobRecurrenceDelay = 2 seconds + issueBgJobProcessingParallelism = 5 + presentationBgJobRecordsLimit = 25 + presentationBgJobRecurrenceDelay = 2 seconds + presentationBgJobProcessingParallelism = 5 } connect { - database { - host = "localhost" - host = ${?CONNECT_DB_HOST} - port = 5432 - port = ${?CONNECT_DB_PORT} - databaseName = "connect" - databaseName = ${?CONNECT_DB_NAME} - username = "postgres" - username = ${?CONNECT_DB_USER} - password = "postgres" - password = ${?CONNECT_DB_PASSWORD} - awaitConnectionThreads = 8 - } - connectBgJobRecordsLimit = 25 - connectBgJobRecurrenceDelay = 2 seconds - connectBgJobProcessingParallelism = 5 + database { + host = "localhost" + host = ${?CONNECT_DB_HOST} + port = 5432 + port = ${?CONNECT_DB_PORT} + databaseName = "connect" + databaseName = ${?CONNECT_DB_NAME} + username = "postgres" + username = ${?CONNECT_DB_USER} + password = "postgres" + password = ${?CONNECT_DB_PASSWORD} + awaitConnectionThreads = 8 + } + connectBgJobRecordsLimit = 25 + connectBgJobRecurrenceDelay = 2 seconds + connectBgJobProcessingParallelism = 5 } agent { - httpEndpoint { - http { - port = 8085 - port =${?AGENT_HTTP_PORT} - } - } - didCommServiceEndpointUrl = "http://localhost:8090" - didCommServiceEndpointUrl = ${?DIDCOMM_SERVICE_URL} - database { - host = "localhost" - host = ${?AGENT_DB_HOST} - port = 5432 - port = ${?AGENT_DB_PORT} - databaseName = "agent" - databaseName = ${?AGENT_DB_NAME} - username = "postgres" - username = ${?AGENT_DB_USER} - password = "postgres" - password = ${?AGENT_DB_PASSWORD} - awaitConnectionThreads = 8 + httpEndpoint { + http { + port = 8085 + port = ${?AGENT_HTTP_PORT} } - verification { - options { - credential { - verifySignature = true - verifyDates = false - leeway = 0 seconds - verifySignature = ${?CREDENTIAL_VERIFY_SIGNATURE} - verifyDates = ${?CREDENTIAL_VERIFY_DATES} - leeway = ${?CREDENTIAL_LEEWAY} - } - presentation { - verifySignature = true - verifyDates = false - verifyHoldersBinding = false - leeway = 0 seconds - verifySignature = ${?PRESENTATION_VERIFY_SIGNATURE} - verifyDates = ${?PRESENTATION_VERIFY_DATES} - verifyHoldersBinding = ${?PRESENTATION_VERIFY_HOLDER_BINDING} - leeway = ${?PRESENTATION_LEEWAY} - } - } + } + didCommServiceEndpointUrl = "http://localhost:8090" + didCommServiceEndpointUrl = ${?DIDCOMM_SERVICE_URL} + database { + host = "localhost" + host = ${?AGENT_DB_HOST} + port = 5432 + port = ${?AGENT_DB_PORT} + databaseName = "agent" + databaseName = ${?AGENT_DB_NAME} + username = "postgres" + username = ${?AGENT_DB_USER} + password = "postgres" + password = ${?AGENT_DB_PASSWORD} + awaitConnectionThreads = 8 + } + verification { + options { + credential { + verifySignature = true + verifyDates = false + leeway = 0 seconds + verifySignature = ${?CREDENTIAL_VERIFY_SIGNATURE} + verifyDates = ${?CREDENTIAL_VERIFY_DATES} + leeway = ${?CREDENTIAL_LEEWAY} + } + presentation { + verifySignature = true + verifyDates = false + verifyHoldersBinding = false + leeway = 0 seconds + verifySignature = ${?PRESENTATION_VERIFY_SIGNATURE} + verifyDates = ${?PRESENTATION_VERIFY_DATES} + verifyHoldersBinding = ${?PRESENTATION_VERIFY_HOLDER_BINDING} + leeway = ${?PRESENTATION_LEEWAY} + } } + } + webhookPublisher { + url = ${?WEBHOOK_URL} + apiKey = ${?WEBHOOK_API_KEY} + parallelism = ${?WEBHOOK_PARALLELISM} + } } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala index 5db0d45a29..a9091bb550 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala @@ -87,11 +87,18 @@ final case class VerificationConfig(options: Options) { } } +final case class WebhookPublisherConfig( + url: Option[String], + apiKey: Option[String], + parallelism: Option[Int] +) + final case class AgentConfig( httpEndpoint: HttpEndpointConfig, didCommServiceEndpointUrl: String, database: DatabaseConfig, - verification: VerificationConfig + verification: VerificationConfig, + webhookPublisher: WebhookPublisherConfig ) final case class HttpEndpointConfig(http: HttpConfig) From c66eaf2702b799a214ae910c91b14e4c2ae38d15 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 16 Jun 2023 17:02:55 +0200 Subject: [PATCH 02/56] feat(prism-agent): add necessary event notification interfaces --- .../scala/io/iohk/atala/agent/notification/Event.scala | 3 +++ .../iohk/atala/agent/notification/EventConsumer.scala | 9 +++++++++ .../agent/notification/EventNotificationService.scala | 10 ++++++++++ 3 files changed, 22 insertions(+) create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/Event.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventConsumer.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationService.scala diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/Event.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/Event.scala new file mode 100644 index 0000000000..9489f9837c --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/Event.scala @@ -0,0 +1,3 @@ +package io.iohk.atala.agent.notification + +case class Event(content: String) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventConsumer.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventConsumer.scala new file mode 100644 index 0000000000..1dfda54250 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventConsumer.scala @@ -0,0 +1,9 @@ +package io.iohk.atala.agent.notification + +import zio.IO + +trait EventConsumer: + def poll(count: Int): IO[EventConsumer.Error, Seq[Event]] + +object EventConsumer: + sealed trait Error diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationService.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationService.scala new file mode 100644 index 0000000000..e2f4252a56 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationService.scala @@ -0,0 +1,10 @@ +package io.iohk.atala.agent.notification + +import zio.IO + +trait EventNotificationService: + def notify(event: Event): IO[EventNotificationService.Error, Unit] + def subscribe(topic: String): IO[EventNotificationService.Error, EventConsumer] + +object EventNotificationService: + sealed trait Error From d2f86b233dc29efd1c36a2f36b05b3f8664e7f0c Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 16 Jun 2023 17:04:00 +0200 Subject: [PATCH 03/56] feat(prism-agent): add event notification service 'in-memory' implementation --- .../EventNotificationServiceInMemoryImpl.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationServiceInMemoryImpl.scala diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationServiceInMemoryImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationServiceInMemoryImpl.scala new file mode 100644 index 0000000000..d4add04e8c --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationServiceInMemoryImpl.scala @@ -0,0 +1,18 @@ +package io.iohk.atala.agent.notification +import zio.{IO, Queue, ULayer, URLayer, ZIO, ZLayer} + +class EventNotificationServiceInMemoryImpl(queue: Queue[Event]) extends EventNotificationService { + + override def notify(event: Event): IO[EventNotificationService.Error, Unit] = + queue.offer(event).unit + + override def subscribe(topic: String): IO[EventNotificationService.Error, EventConsumer] = + ZIO.succeed(new EventConsumer { + override def poll(count: Int): IO[EventConsumer.Error, Seq[Event]] = queue.takeBetween(1, count) + }) +} + +object EventNotificationServiceInMemoryImpl { + val layer: URLayer[Queue[Event], EventNotificationServiceInMemoryImpl] = + ZLayer.fromFunction(new EventNotificationServiceInMemoryImpl(_)) +} From 2fa6d454b7ba03b4c7bf6f837ead7a9818ff8823 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 16 Jun 2023 17:04:54 +0200 Subject: [PATCH 04/56] feat(prism-agent): implement dummy webhook publisher consumer --- .../agent/notification/WebhookPublisher.scala | 42 +++++++++++++++++++ .../notification/WebhookPublisherError.scala | 8 ++++ 2 files changed, 50 insertions(+) create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisherError.scala diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala new file mode 100644 index 0000000000..670db87ae5 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -0,0 +1,42 @@ +package io.iohk.atala.agent.notification +import io.iohk.atala.agent.notification.WebhookPublisherError.{InvalidWebhookURL, UnexpectedError} +import io.iohk.atala.agent.server.config.{AppConfig, WebhookPublisherConfig} +import zio.{IO, UIO, URLayer, ZIO, ZLayer} + +import java.net.{URI, URL} + +class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificationService) { + + private val config = appConfig.agent.webhookPublisher + + private val parallelism = config.parallelism match { + case Some(p) if p < 1 => 1 + case Some(p) if p > 10 => 10 + case Some(p) => p + case None => 1 + } + + val run: IO[WebhookPublisherError, Unit] = config.url match { + case Some(url) => + for { + url <- ZIO.attempt(URL(url)).mapError(th => InvalidWebhookURL(s"$url [${th.getMessage}]")) + consumer <- notificationService.subscribe("ALL").mapError(e => UnexpectedError(e.toString)) + pollAndNotify = for { + _ <- ZIO.log(s"Polling $parallelism event(s)") + events <- consumer.poll(parallelism).mapError(e => UnexpectedError(e.toString)) + _ <- ZIO.log(s"Got ${events.size} event(s)") + _ <- ZIO.foreachPar(events)(e => notifyWebhook(e, url)) + } yield () + poll <- pollAndNotify.forever + } yield poll + case None => ZIO.unit + } + + private[this] def notifyWebhook(event: Event, url: URL): UIO[Unit] = + ZIO.log(s"Sending event: $event to webhook URL: $url with API key ${config.apiKey}") +} + +object WebhookPublisher { + val layer: URLayer[AppConfig & EventNotificationService, WebhookPublisher] = + ZLayer.fromFunction(WebhookPublisher(_, _)) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisherError.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisherError.scala new file mode 100644 index 0000000000..4d629c0684 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisherError.scala @@ -0,0 +1,8 @@ +package io.iohk.atala.agent.notification + +sealed trait WebhookPublisherError + +object WebhookPublisherError { + case class InvalidWebhookURL(msg: String) extends WebhookPublisherError + case class UnexpectedError(msg: String) extends WebhookPublisherError +} From 3d4f35164fa9e98d2aa72be9a6a96ced0443bff7 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 16 Jun 2023 17:06:52 +0200 Subject: [PATCH 05/56] feat(prism-agent): add notification service calls for issue credential flow (DIDComm sender side only) --- .../io/iohk/atala/agent/server/Modules.scala | 24 ++++--------------- .../agent/server/jobs/BackgroundJobs.scala | 19 ++++++++++----- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala index c644f02a83..5a82c8cad5 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala @@ -6,6 +6,7 @@ import com.typesafe.config.ConfigFactory import doobie.util.transactor.Transactor import io.circe.{DecodingFailure, ParsingFailure} import io.grpc.ManagedChannelBuilder +import io.iohk.atala.agent.notification.EventNotificationService import io.iohk.atala.agent.server.config.{AgentConfig, AppConfig} import io.iohk.atala.agent.server.http.{ZHttp4sBlazeServer, ZHttpEndpoints} import io.iohk.atala.agent.server.jobs.* @@ -15,12 +16,7 @@ import io.iohk.atala.agent.walletapi.model.error.DIDSecretStorageError import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, ManagedDIDServiceImpl} import io.iohk.atala.agent.walletapi.sql.{JdbcDIDNonSecretStorage, JdbcDIDSecretStorage} import io.iohk.atala.agent.walletapi.util.SeedResolver -import io.iohk.atala.castor.controller.{ - DIDController, - DIDRegistrarController, - DIDRegistrarServerEndpoints, - DIDServerEndpoints -} +import io.iohk.atala.castor.controller.{DIDController, DIDRegistrarController, DIDRegistrarServerEndpoints, DIDServerEndpoints} import io.iohk.atala.castor.core.service.{DIDService, DIDServiceImpl} import io.iohk.atala.castor.core.util.DIDOperationValidator import io.iohk.atala.connect.controller.{ConnectionController, ConnectionControllerImpl, ConnectionServerEndpoints} @@ -45,19 +41,9 @@ import io.iohk.atala.pollux.core.repository.{CredentialRepository, PresentationR import io.iohk.atala.pollux.core.service.* import io.iohk.atala.pollux.credentialschema.controller.* import io.iohk.atala.pollux.credentialschema.{SchemaRegistryServerEndpoints, VerificationPolicyServerEndpoints} -import io.iohk.atala.pollux.sql.repository.{ - JdbcCredentialRepository, - JdbcCredentialSchemaRepository, - JdbcPresentationRepository, - JdbcVerificationPolicyRepository, - DbConfig as PolluxDbConfig -} +import io.iohk.atala.pollux.sql.repository.{JdbcCredentialRepository, JdbcCredentialSchemaRepository, JdbcPresentationRepository, JdbcVerificationPolicyRepository, DbConfig as PolluxDbConfig} import io.iohk.atala.pollux.vc.jwt.{PrismDidResolver, DidResolver as JwtDidResolver} -import io.iohk.atala.presentproof.controller.{ - PresentProofController, - PresentProofEndpoints, - PresentProofServerEndpoints -} +import io.iohk.atala.presentproof.controller.{PresentProofController, PresentProofEndpoints, PresentProofServerEndpoints} import io.iohk.atala.prism.protos.node_api.NodeServiceGrpc import io.iohk.atala.resolvers.{DIDResolver, UniversalDidResolver} import io.iohk.atala.system.controller.{SystemController, SystemServerEndpoints} @@ -142,7 +128,7 @@ object Modules { val issueCredentialDidCommExchangesJob: RIO[ AppConfig & DidOps & DIDResolver & JwtDidResolver & HttpClient & CredentialService & DIDService & - ManagedDIDService & PresentationService, + ManagedDIDService & PresentationService & EventNotificationService, Unit ] = for { diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala index 6873dd2cba..f8d7fd673b 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala @@ -5,6 +5,7 @@ import com.ionspin.kotlin.bignum.integer.{BigInteger, Sign} import io.circe.Json import io.circe.parser.* import io.circe.syntax.* +import io.iohk.atala.agent.notification.{Event, EventNotificationService} import io.iohk.atala.agent.server.config.AppConfig import io.iohk.atala.agent.server.jobs.BackgroundJobError.{ ErrorResponseReceivedFromPeerAgent, @@ -134,7 +135,7 @@ object BackgroundJobs { .provideSomeLayer(didCommAgent) credentialService <- ZIO.service[CredentialService] _ <- { - if (resp.status >= 200 && resp.status < 300) credentialService.markOfferSent(id) + if (resp.status >= 200 && resp.status < 300) notifyIfSuccessful(credentialService.markOfferSent(id)) else ZIO.fail(ErrorResponseReceivedFromPeerAgent(resp)) } } yield () @@ -204,7 +205,7 @@ object BackgroundJobs { .provideSomeLayer(didCommAgent) credentialService <- ZIO.service[CredentialService] _ <- { - if (resp.status >= 200 && resp.status < 300) credentialService.markRequestSent(id) + if (resp.status >= 200 && resp.status < 300) notifyIfSuccessful(credentialService.markRequestSent(id)) else ZIO.fail(ErrorResponseReceivedFromPeerAgent(resp)) } } yield () @@ -234,7 +235,7 @@ object BackgroundJobs { ) => for { credentialService <- ZIO.service[CredentialService] - _ <- credentialService.acceptCredentialRequest(id) + _ <- notifyIfSuccessful(credentialService.acceptCredentialRequest(id)) } yield () // Credential is pending, can be generated by Issuer and optionally published on-chain @@ -279,7 +280,7 @@ object BackgroundJobs { thid = issue.thid, credentials = Map("prims/jwt" -> signedJwtCredential.value.getBytes) ) - _ <- credentialService.markCredentialGenerated(id, issueCredential) + _ <- notifyIfSuccessful(credentialService.markCredentialGenerated(id, issueCredential)) } yield () @@ -313,7 +314,7 @@ object BackgroundJobs { .provideSomeLayer(didCommAgent) credentialService <- ZIO.service[CredentialService] _ <- { - if (resp.status >= 200 && resp.status < 300) credentialService.markCredentialSent(id) + if (resp.status >= 200 && resp.status < 300) notifyIfSuccessful(credentialService.markCredentialSent(id)) else ZIO.fail(ErrorResponseReceivedFromPeerAgent(resp)) } } yield () @@ -346,7 +347,7 @@ object BackgroundJobs { resp <- MessagingService.send(issue.makeMessage).provideSomeLayer(didCommAgent) credentialService <- ZIO.service[CredentialService] _ <- { - if (resp.status >= 200 && resp.status < 300) credentialService.markCredentialSent(id) + if (resp.status >= 200 && resp.status < 300) notifyIfSuccessful(credentialService.markCredentialSent(id)) else ZIO.fail(ErrorResponseReceivedFromPeerAgent(resp)) } } yield () @@ -710,6 +711,12 @@ object BackgroundJobs { .catchAllDefect(d => ZIO.logErrorCause(s"Present Proof - Defect processing record: ${record.id}", Cause.fail(d))) } + private[this] def notifyIfSuccessful(effect: IO[_, IssueCredentialRecord]) = for { + notificationService <- ZIO.service[EventNotificationService] + record <- effect + _ <- notificationService.notify(Event(s"Record updated => $record")) + } yield () + private[this] def buildDIDCommAgent( myDid: DidId ): ZIO[ManagedDIDService, KeyNotFoundError, ZLayer[Any, Nothing, DidAgent]] = { From c39cf8d36aa11c470c402db172dc06d80359312b Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 16 Jun 2023 17:07:48 +0200 Subject: [PATCH 06/56] feat(prism-agent): wire notification service and webhook publisher in app entry point --- .../scala/io/iohk/atala/agent/server/Main.scala | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index d6a4d1df71..c93d3a9736 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -5,7 +5,9 @@ import io.circe.* import io.circe.generic.auto.* import io.circe.parser.* import io.circe.syntax.* +import io.iohk.atala.agent.notification.{Event, EventNotificationService, EventNotificationServiceInMemoryImpl, WebhookPublisher} import io.iohk.atala.agent.server.buildinfo.BuildInfo +import io.iohk.atala.agent.server.config.AppConfig import io.iohk.atala.agent.server.http.ZioHttpClient import io.iohk.atala.agent.server.sql.Migrations as AgentMigrations import io.iohk.atala.agent.walletapi.service.ManagedDIDService @@ -16,12 +18,7 @@ import io.iohk.atala.connect.sql.repository.Migrations as ConnectMigrations import io.iohk.atala.issue.controller.IssueControllerImpl import io.iohk.atala.mercury.* import io.iohk.atala.pollux.core.service.URIDereferencerError.{ConnectionError, ResourceNotFound, UnexpectedError} -import io.iohk.atala.pollux.core.service.{ - CredentialSchemaServiceImpl, - URIDereferencer, - URIDereferencerError, - HttpURIDereferencerImpl -} +import io.iohk.atala.pollux.core.service.{CredentialSchemaServiceImpl, HttpURIDereferencerImpl, URIDereferencer, URIDereferencerError} import io.iohk.atala.pollux.sql.repository.{JdbcCredentialSchemaRepository, Migrations as PolluxMigrations} import io.iohk.atala.presentproof.controller.PresentProofControllerImpl import io.iohk.atala.resolvers.{DIDResolver, UniversalDidResolver} @@ -82,6 +79,7 @@ object MainApp extends ZIOAppDefault { server <- serverProgram(didCommServicePort) _ <- Modules.syncDIDPublicationStateFromDltJob.fork _ <- Modules.zioApp.fork + _ <- ZIO.scoped(WebhookPublisher.layer.build.map(_.get[WebhookPublisher])).flatMap(_.run.debug.fork) _ <- server.join *> ZIO.log(s"Server End") _ <- ZIO.never } yield () @@ -154,7 +152,8 @@ object MainApp extends ZIOAppDefault { prometheus.publisherLayer, ZLayer.succeed(MetricsConfig(5.seconds)), DefaultJvmMetrics.live.unit, - SystemControllerImpl.layer + SystemControllerImpl.layer, + ZLayer.fromZIO(Queue.bounded[Event](500)) >>> EventNotificationServiceInMemoryImpl.layer ) } yield app From c7d3b65f276ee7b7d939d04a73ab658c7d120297 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 16 Jun 2023 17:08:33 +0200 Subject: [PATCH 07/56] feat(prism-agent): add fake webhook URL config param --- infrastructure/shared/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/infrastructure/shared/docker-compose.yml b/infrastructure/shared/docker-compose.yml index c882fb21dd..58278c7a39 100644 --- a/infrastructure/shared/docker-compose.yml +++ b/infrastructure/shared/docker-compose.yml @@ -78,6 +78,7 @@ services: DIDCOMM_SERVICE_URL: http://host.docker.internal:${PORT}/didcomm PRISM_NODE_HOST: prism-node PRISM_NODE_PORT: 50053 + WEBHOOK_URL: https://localhost:8080/my-webhook depends_on: db: condition: service_healthy From 2ad8a7aeac03710140c98617aa99f82cc3cc75a5 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Tue, 20 Jun 2023 11:41:39 +0200 Subject: [PATCH 08/56] feat(prism-agent): notify when credential is generated --- .../scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala index f8d7fd673b..c58b42336b 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala @@ -172,7 +172,7 @@ object BackgroundJobs { jwtIssuer <- createJwtIssuer(longFormPrismDID, VerificationRelationship.Authentication) presentationPayload <- credentialService.createPresentationPayload(id, jwtIssuer) signedPayload = JwtPresentation.encodeJwt(presentationPayload.toJwtPresentationPayload, jwtIssuer) - _ <- credentialService.generateCredentialRequest(id, signedPayload) + _ <- notifyIfSuccessful(credentialService.generateCredentialRequest(id, signedPayload)) } yield () // Request should be sent from Holder to Issuer From 21c7ba1d9f0e72f927cf56adbbf61fddb1afbcf8 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Tue, 20 Jun 2023 11:45:37 +0200 Subject: [PATCH 09/56] feat(prism-agent): notify issue credential steps occuring on the DIDComm receiver side --- .../io/iohk/atala/agent/server/Modules.scala | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala index 5a82c8cad5..a0270c638a 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala @@ -6,7 +6,7 @@ import com.typesafe.config.ConfigFactory import doobie.util.transactor.Transactor import io.circe.{DecodingFailure, ParsingFailure} import io.grpc.ManagedChannelBuilder -import io.iohk.atala.agent.notification.EventNotificationService +import io.iohk.atala.agent.notification.{Event, EventNotificationService} import io.iohk.atala.agent.server.config.{AgentConfig, AppConfig} import io.iohk.atala.agent.server.http.{ZHttp4sBlazeServer, ZHttpEndpoints} import io.iohk.atala.agent.server.jobs.* @@ -16,7 +16,12 @@ import io.iohk.atala.agent.walletapi.model.error.DIDSecretStorageError import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, ManagedDIDServiceImpl} import io.iohk.atala.agent.walletapi.sql.{JdbcDIDNonSecretStorage, JdbcDIDSecretStorage} import io.iohk.atala.agent.walletapi.util.SeedResolver -import io.iohk.atala.castor.controller.{DIDController, DIDRegistrarController, DIDRegistrarServerEndpoints, DIDServerEndpoints} +import io.iohk.atala.castor.controller.{ + DIDController, + DIDRegistrarController, + DIDRegistrarServerEndpoints, + DIDServerEndpoints +} import io.iohk.atala.castor.core.service.{DIDService, DIDServiceImpl} import io.iohk.atala.castor.core.util.DIDOperationValidator import io.iohk.atala.connect.controller.{ConnectionController, ConnectionControllerImpl, ConnectionServerEndpoints} @@ -35,15 +40,26 @@ import io.iohk.atala.mercury.protocol.connection.{ConnectionRequest, ConnectionR import io.iohk.atala.mercury.protocol.issuecredential.* import io.iohk.atala.mercury.protocol.presentproof.* import io.iohk.atala.mercury.protocol.trustping.TrustPing +import io.iohk.atala.pollux.core.model.IssueCredentialRecord import io.iohk.atala.pollux.core.model.error.CredentialServiceError.RepositoryError import io.iohk.atala.pollux.core.model.error.{CredentialServiceError, PresentationError} import io.iohk.atala.pollux.core.repository.{CredentialRepository, PresentationRepository} import io.iohk.atala.pollux.core.service.* import io.iohk.atala.pollux.credentialschema.controller.* import io.iohk.atala.pollux.credentialschema.{SchemaRegistryServerEndpoints, VerificationPolicyServerEndpoints} -import io.iohk.atala.pollux.sql.repository.{JdbcCredentialRepository, JdbcCredentialSchemaRepository, JdbcPresentationRepository, JdbcVerificationPolicyRepository, DbConfig as PolluxDbConfig} +import io.iohk.atala.pollux.sql.repository.{ + JdbcCredentialRepository, + JdbcCredentialSchemaRepository, + JdbcPresentationRepository, + JdbcVerificationPolicyRepository, + DbConfig as PolluxDbConfig +} import io.iohk.atala.pollux.vc.jwt.{PrismDidResolver, DidResolver as JwtDidResolver} -import io.iohk.atala.presentproof.controller.{PresentProofController, PresentProofEndpoints, PresentProofServerEndpoints} +import io.iohk.atala.presentproof.controller.{ + PresentProofController, + PresentProofEndpoints, + PresentProofServerEndpoints +} import io.iohk.atala.prism.protos.node_api.NodeServiceGrpc import io.iohk.atala.resolvers.{DIDResolver, UniversalDidResolver} import io.iohk.atala.system.controller.{SystemController, SystemServerEndpoints} @@ -96,7 +112,7 @@ object Modules { def didCommServiceEndpoint: HttpApp[ DidOps & DidAgent & CredentialService & PresentationService & ConnectionService & ManagedDIDService & HttpClient & - DidAgent & DIDResolver, + DidAgent & DIDResolver & EventNotificationService, Throwable ] = Http.collectZIO[Request] { case Method.GET -> !! / "did" => @@ -192,7 +208,7 @@ object Modules { def webServerProgram(jsonString: String): ZIO[ DidOps & CredentialService & PresentationService & ConnectionService & ManagedDIDService & HttpClient & DidAgent & - DIDResolver, + DIDResolver & EventNotificationService, MercuryThrowable | DIDSecretStorageError, Unit ] = { @@ -253,8 +269,7 @@ object Modules { _ <- ZIO.logInfo("As an Holder in issue-credential:") _ <- ZIO.logInfo("Got OfferCredential: " + msg) offerFromIssuer = OfferCredential.readFromMessage(msg) - _ <- credentialService - .receiveCredentialOffer(offerFromIssuer) + _ <- notifyIfSuccessful(credentialService.receiveCredentialOffer(offerFromIssuer)) .catchSome { case CredentialServiceError.RepositoryError(cause) => ZIO.logError(cause.getMessage()) *> ZIO.fail(cause) @@ -270,8 +285,7 @@ object Modules { requestCredential = RequestCredential.readFromMessage(msg) _ <- ZIO.logInfo("Got RequestCredential: " + requestCredential) credentialService <- ZIO.service[CredentialService] - todoTestOption <- credentialService - .receiveCredentialRequest(requestCredential) + _ <- notifyIfSuccessful(credentialService.receiveCredentialRequest(requestCredential)) .catchSome { case CredentialServiceError.RepositoryError(cause) => ZIO.logError(cause.getMessage()) *> ZIO.fail(cause) @@ -288,8 +302,7 @@ object Modules { issueCredential = IssueCredential.readFromMessage(msg) _ <- ZIO.logInfo("Got IssueCredential: " + issueCredential) credentialService <- ZIO.service[CredentialService] - _ <- credentialService - .receiveCredentialIssue(issueCredential) + _ <- notifyIfSuccessful(credentialService.receiveCredentialIssue(issueCredential)) .catchSome { case CredentialServiceError.RepositoryError(cause) => ZIO.logError(cause.getMessage()) *> ZIO.fail(cause) @@ -395,6 +408,12 @@ object Modules { } } + private[this] def notifyIfSuccessful(effect: IO[_, IssueCredentialRecord]) = for { + notificationService <- ZIO.service[EventNotificationService] + record <- effect + _ <- notificationService.notify(Event(s"Record updated => $record")) + } yield () + } object SystemModule { val configLayer: Layer[ReadError[String], AppConfig] = ZLayer.fromZIO { From 4153b87f7c792d7e132980d6c41191b4e09d935a Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Tue, 20 Jun 2023 17:27:08 +0200 Subject: [PATCH 10/56] feat(prism-agent): extract event notification classes to a new shared project --- build.sbt | 22 +++++++++++++++++-- .../iohk/atala/event/notification/Event.scala | 3 +++ .../event}/notification/EventConsumer.scala | 2 +- .../EventNotificationService.scala | 2 +- ...EventNotificationServiceInMemoryImpl.scala | 3 ++- .../iohk/atala/agent/notification/Event.scala | 3 --- 6 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala rename {prism-agent/service/server/src/main/scala/io/iohk/atala/agent => event-notification/src/main/scala/io/iohk/atala/event}/notification/EventConsumer.scala (77%) rename {prism-agent/service/server/src/main/scala/io/iohk/atala/agent => event-notification/src/main/scala/io/iohk/atala/event}/notification/EventNotificationService.scala (86%) rename {prism-agent/service/server/src/main/scala/io/iohk/atala/agent => event-notification/src/main/scala/io/iohk/atala/event}/notification/EventNotificationServiceInMemoryImpl.scala (94%) delete mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/Event.scala diff --git a/build.sbt b/build.sbt index 9ddcccf675..2b6cd2ca8e 100644 --- a/build.sbt +++ b/build.sbt @@ -273,6 +273,12 @@ lazy val D_Pollux_VC_JWT = new { lazy val polluxVcJwtDependencies: Seq[ModuleID] = baseDependencies } +lazy val D_EventNotification = new { + val zio = "dev.zio" %% "zio" % V.zio + val zioDependencies: Seq[ModuleID] = Seq(zio) + val baseDependencies: Seq[ModuleID] = zioDependencies +} + lazy val D_PrismAgent = new { // Added here to make prism-crypto works. @@ -603,7 +609,7 @@ lazy val polluxCore = project ) .dependsOn(shared) .dependsOn(polluxVcJWT) - .dependsOn(protocolIssueCredential, protocolPresentProof, resolver, agentDidcommx) + .dependsOn(protocolIssueCredential, protocolPresentProof, resolver, agentDidcommx, eventNotification) lazy val polluxDoobie = project .in(file("pollux/lib/sql-doobie")) @@ -642,6 +648,17 @@ lazy val connectDoobie = project .dependsOn(shared) .dependsOn(connectCore % "compile->compile;test->test") +// ############################ +// #### Event Notification #### +// ############################ + +lazy val eventNotification = project + .in(file("event-notification")) + .settings( + name := "event-notification", + libraryDependencies ++= D_EventNotification.baseDependencies + ) + // ##################### // #### Prism Agent #### // ##################### @@ -683,7 +700,8 @@ lazy val prismAgentServer = project polluxDoobie, connectCore, connectDoobie, - castorCore + castorCore, + eventNotification ) // ################## diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala new file mode 100644 index 0000000000..0beec8bc7b --- /dev/null +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala @@ -0,0 +1,3 @@ +package io.iohk.atala.event.notification + +case class Event(content: String) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventConsumer.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventConsumer.scala similarity index 77% rename from prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventConsumer.scala rename to event-notification/src/main/scala/io/iohk/atala/event/notification/EventConsumer.scala index 1dfda54250..fdcab53a86 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventConsumer.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventConsumer.scala @@ -1,4 +1,4 @@ -package io.iohk.atala.agent.notification +package io.iohk.atala.event.notification import zio.IO diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationService.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationService.scala similarity index 86% rename from prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationService.scala rename to event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationService.scala index e2f4252a56..b70f9f82c7 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationService.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationService.scala @@ -1,4 +1,4 @@ -package io.iohk.atala.agent.notification +package io.iohk.atala.event.notification import zio.IO diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationServiceInMemoryImpl.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala similarity index 94% rename from prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationServiceInMemoryImpl.scala rename to event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala index d4add04e8c..78607b0403 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/EventNotificationServiceInMemoryImpl.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala @@ -1,4 +1,5 @@ -package io.iohk.atala.agent.notification +package io.iohk.atala.event.notification + import zio.{IO, Queue, ULayer, URLayer, ZIO, ZLayer} class EventNotificationServiceInMemoryImpl(queue: Queue[Event]) extends EventNotificationService { diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/Event.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/Event.scala deleted file mode 100644 index 9489f9837c..0000000000 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/Event.scala +++ /dev/null @@ -1,3 +0,0 @@ -package io.iohk.atala.agent.notification - -case class Event(content: String) From 04f53feae3c51f569c6e520eb82352d0e9203ff7 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Tue, 20 Jun 2023 17:28:51 +0200 Subject: [PATCH 11/56] feat(pollux): add credential service subclass that notifies of events --- ...tialServiceWithEventNotificationImpl.scala | 101 ++++++++++++++++++ .../agent/notification/WebhookPublisher.scala | 1 + .../io/iohk/atala/agent/server/Main.scala | 10 +- .../io/iohk/atala/agent/server/Modules.scala | 28 +++-- .../agent/server/jobs/BackgroundJobs.scala | 38 ++----- 5 files changed, 132 insertions(+), 46 deletions(-) create mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala new file mode 100644 index 0000000000..c794778558 --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala @@ -0,0 +1,101 @@ +package io.iohk.atala.pollux.core.service + +import io.circe.Json +import io.iohk.atala.castor.core.model.did.CanonicalPrismDID +import io.iohk.atala.event.notification.{Event, EventNotificationService} +import io.iohk.atala.iris.proto.service.IrisServiceGrpc.IrisServiceStub +import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.mercury.protocol.issuecredential.{IssueCredential, OfferCredential, RequestCredential} +import io.iohk.atala.pollux.core.model.error.CredentialServiceError +import io.iohk.atala.pollux.core.model.{DidCommID, IssueCredentialRecord} +import io.iohk.atala.pollux.core.repository.CredentialRepository +import io.iohk.atala.pollux.vc.jwt.{DidResolver, JWT} +import zio.{IO, Task, ULayer, URLayer, ZIO, ZLayer} + +class CredentialServiceWithEventNotificationImpl( + irisClient: IrisServiceStub, + credentialRepository: CredentialRepository[Task], + didResolver: DidResolver, + uriDereferencer: URIDereferencer, + eventNotificationService: EventNotificationService +) extends CredentialServiceImpl(irisClient, credentialRepository, didResolver, uriDereferencer) { + + override def createIssueCredentialRecord( + pairwiseIssuerDID: DidId, + pairwiseHolderDID: DidId, + thid: DidCommID, + maybeSchemaId: Option[_root_.java.lang.String], + claims: Json, + validityPeriod: Option[Double], + automaticIssuance: Option[Boolean], + awaitConfirmation: Option[Boolean], + issuingDID: Option[CanonicalPrismDID] + ): IO[CredentialServiceError, IssueCredentialRecord] = + notify( + super.createIssueCredentialRecord( + pairwiseIssuerDID, + pairwiseHolderDID, + thid, + maybeSchemaId, + claims, + validityPeriod, + automaticIssuance, + awaitConfirmation, + issuingDID + ) + ) + + override def markOfferSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + notify(super.markOfferSent(recordId)) + + override def receiveCredentialOffer(offer: OfferCredential): IO[CredentialServiceError, IssueCredentialRecord] = + notify(super.receiveCredentialOffer(offer)) + + override def acceptCredentialOffer( + recordId: DidCommID, + subjectId: String + ): IO[CredentialServiceError, IssueCredentialRecord] = + notify(super.acceptCredentialOffer(recordId, subjectId)) + + override def generateCredentialRequest( + recordId: DidCommID, + signedPresentation: JWT + ): IO[CredentialServiceError, IssueCredentialRecord] = + notify(super.generateCredentialRequest(recordId, signedPresentation)) + + override def markRequestSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + notify(super.markRequestSent(recordId)) + + override def receiveCredentialRequest(request: RequestCredential): IO[CredentialServiceError, IssueCredentialRecord] = + notify(super.receiveCredentialRequest(request)) + + override def acceptCredentialRequest(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + notify(super.acceptCredentialRequest(recordId)) + + override def markCredentialGenerated( + recordId: DidCommID, + issueCredential: IssueCredential + ): IO[CredentialServiceError, IssueCredentialRecord] = + notify(super.markCredentialGenerated(recordId, issueCredential)) + + override def markCredentialSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + notify(super.markCredentialSent(recordId)) + + override def receiveCredentialIssue(issue: IssueCredential): IO[CredentialServiceError, IssueCredentialRecord] = + notify(super.receiveCredentialIssue(issue)) + + // Notification method + private[this] def notify(effect: IO[CredentialServiceError, IssueCredentialRecord]) = for { + record <- effect + _ <- eventNotificationService + .notify(Event(s"${record.protocolState.toString} [${record.id}]")) + .catchAll(e => ZIO.logError(s"Notification service error: $e")) + } yield record +} + +object CredentialServiceWithEventNotificationImpl { + val layer: URLayer[ + IrisServiceStub with CredentialRepository[Task] with DidResolver with URIDereferencer with EventNotificationService, + CredentialServiceWithEventNotificationImpl + ] = ZLayer.fromFunction(CredentialServiceWithEventNotificationImpl(_, _, _, _, _)) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index 670db87ae5..a70ddb495c 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -1,6 +1,7 @@ package io.iohk.atala.agent.notification import io.iohk.atala.agent.notification.WebhookPublisherError.{InvalidWebhookURL, UnexpectedError} import io.iohk.atala.agent.server.config.{AppConfig, WebhookPublisherConfig} +import io.iohk.atala.event.notification.{Event, EventNotificationService} import zio.{IO, UIO, URLayer, ZIO, ZLayer} import java.net.{URI, URL} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index c93d3a9736..82c4d3906d 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -5,7 +5,7 @@ import io.circe.* import io.circe.generic.auto.* import io.circe.parser.* import io.circe.syntax.* -import io.iohk.atala.agent.notification.{Event, EventNotificationService, EventNotificationServiceInMemoryImpl, WebhookPublisher} +import io.iohk.atala.agent.notification.WebhookPublisher import io.iohk.atala.agent.server.buildinfo.BuildInfo import io.iohk.atala.agent.server.config.AppConfig import io.iohk.atala.agent.server.http.ZioHttpClient @@ -15,10 +15,16 @@ import io.iohk.atala.agent.walletapi.sql.JdbcDIDSecretStorage import io.iohk.atala.castor.controller.{DIDControllerImpl, DIDRegistrarControllerImpl} import io.iohk.atala.connect.controller.ConnectionControllerImpl import io.iohk.atala.connect.sql.repository.Migrations as ConnectMigrations +import io.iohk.atala.event.notification.{Event, EventNotificationServiceInMemoryImpl} import io.iohk.atala.issue.controller.IssueControllerImpl import io.iohk.atala.mercury.* import io.iohk.atala.pollux.core.service.URIDereferencerError.{ConnectionError, ResourceNotFound, UnexpectedError} -import io.iohk.atala.pollux.core.service.{CredentialSchemaServiceImpl, HttpURIDereferencerImpl, URIDereferencer, URIDereferencerError} +import io.iohk.atala.pollux.core.service.{ + CredentialSchemaServiceImpl, + HttpURIDereferencerImpl, + URIDereferencer, + URIDereferencerError +} import io.iohk.atala.pollux.sql.repository.{JdbcCredentialSchemaRepository, Migrations as PolluxMigrations} import io.iohk.atala.presentproof.controller.PresentProofControllerImpl import io.iohk.atala.resolvers.{DIDResolver, UniversalDidResolver} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala index a0270c638a..fdace7db46 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala @@ -6,7 +6,6 @@ import com.typesafe.config.ConfigFactory import doobie.util.transactor.Transactor import io.circe.{DecodingFailure, ParsingFailure} import io.grpc.ManagedChannelBuilder -import io.iohk.atala.agent.notification.{Event, EventNotificationService} import io.iohk.atala.agent.server.config.{AgentConfig, AppConfig} import io.iohk.atala.agent.server.http.{ZHttp4sBlazeServer, ZHttpEndpoints} import io.iohk.atala.agent.server.jobs.* @@ -29,6 +28,7 @@ import io.iohk.atala.connect.core.model.error.ConnectionServiceError import io.iohk.atala.connect.core.repository.ConnectionRepository import io.iohk.atala.connect.core.service.{ConnectionService, ConnectionServiceImpl} import io.iohk.atala.connect.sql.repository.{JdbcConnectionRepository, DbConfig as ConnectDbConfig} +import io.iohk.atala.event.notification.EventNotificationService import io.iohk.atala.iris.proto.service.IrisServiceGrpc import io.iohk.atala.iris.proto.service.IrisServiceGrpc.IrisServiceStub import io.iohk.atala.issue.controller.{IssueController, IssueControllerImpl, IssueEndpoints, IssueServerEndpoints} @@ -112,7 +112,7 @@ object Modules { def didCommServiceEndpoint: HttpApp[ DidOps & DidAgent & CredentialService & PresentationService & ConnectionService & ManagedDIDService & HttpClient & - DidAgent & DIDResolver & EventNotificationService, + DidAgent & DIDResolver, Throwable ] = Http.collectZIO[Request] { case Method.GET -> !! / "did" => @@ -144,7 +144,7 @@ object Modules { val issueCredentialDidCommExchangesJob: RIO[ AppConfig & DidOps & DIDResolver & JwtDidResolver & HttpClient & CredentialService & DIDService & - ManagedDIDService & PresentationService & EventNotificationService, + ManagedDIDService & PresentationService, Unit ] = for { @@ -208,7 +208,7 @@ object Modules { def webServerProgram(jsonString: String): ZIO[ DidOps & CredentialService & PresentationService & ConnectionService & ManagedDIDService & HttpClient & DidAgent & - DIDResolver & EventNotificationService, + DIDResolver, MercuryThrowable | DIDSecretStorageError, Unit ] = { @@ -269,7 +269,8 @@ object Modules { _ <- ZIO.logInfo("As an Holder in issue-credential:") _ <- ZIO.logInfo("Got OfferCredential: " + msg) offerFromIssuer = OfferCredential.readFromMessage(msg) - _ <- notifyIfSuccessful(credentialService.receiveCredentialOffer(offerFromIssuer)) + _ <- credentialService + .receiveCredentialOffer(offerFromIssuer) .catchSome { case CredentialServiceError.RepositoryError(cause) => ZIO.logError(cause.getMessage()) *> ZIO.fail(cause) @@ -285,7 +286,8 @@ object Modules { requestCredential = RequestCredential.readFromMessage(msg) _ <- ZIO.logInfo("Got RequestCredential: " + requestCredential) credentialService <- ZIO.service[CredentialService] - _ <- notifyIfSuccessful(credentialService.receiveCredentialRequest(requestCredential)) + _ <- credentialService + .receiveCredentialRequest(requestCredential) .catchSome { case CredentialServiceError.RepositoryError(cause) => ZIO.logError(cause.getMessage()) *> ZIO.fail(cause) @@ -302,7 +304,8 @@ object Modules { issueCredential = IssueCredential.readFromMessage(msg) _ <- ZIO.logInfo("Got IssueCredential: " + issueCredential) credentialService <- ZIO.service[CredentialService] - _ <- notifyIfSuccessful(credentialService.receiveCredentialIssue(issueCredential)) + _ <- credentialService + .receiveCredentialIssue(issueCredential) .catchSome { case CredentialServiceError.RepositoryError(cause) => ZIO.logError(cause.getMessage()) *> ZIO.fail(cause) @@ -408,12 +411,6 @@ object Modules { } } - private[this] def notifyIfSuccessful(effect: IO[_, IssueCredentialRecord]) = for { - notificationService <- ZIO.service[EventNotificationService] - record <- effect - _ <- notificationService.notify(Event(s"Record updated => $record")) - } yield () - } object SystemModule { val configLayer: Layer[ReadError[String], AppConfig] = ZLayer.fromZIO { @@ -445,8 +442,9 @@ object AppModule { (didOpValidatorLayer ++ didServiceLayer ++ secretStorageLayer ++ nonSecretStorageLayer ++ apolloLayer ++ seedResolverLayer) >>> ManagedDIDServiceImpl.layer } - val credentialServiceLayer: RLayer[DidOps & DidAgent & JwtDidResolver & URIDereferencer, CredentialService] = - (GrpcModule.layers ++ RepoModule.credentialRepoLayer) >>> CredentialServiceImpl.layer + val credentialServiceLayer + : RLayer[DidOps & DidAgent & JwtDidResolver & URIDereferencer & EventNotificationService, CredentialService] = + (GrpcModule.layers ++ RepoModule.credentialRepoLayer) >>> CredentialServiceWithEventNotificationImpl.layer def presentationServiceLayer = (RepoModule.presentationRepoLayer ++ RepoModule.credentialRepoLayer) >>> PresentationServiceImpl.layer diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala index c58b42336b..aeb8b97e93 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala @@ -5,13 +5,8 @@ import com.ionspin.kotlin.bignum.integer.{BigInteger, Sign} import io.circe.Json import io.circe.parser.* import io.circe.syntax.* -import io.iohk.atala.agent.notification.{Event, EventNotificationService} import io.iohk.atala.agent.server.config.AppConfig -import io.iohk.atala.agent.server.jobs.BackgroundJobError.{ - ErrorResponseReceivedFromPeerAgent, - InvalidState, - NotImplemented -} +import io.iohk.atala.agent.server.jobs.BackgroundJobError.{ErrorResponseReceivedFromPeerAgent, InvalidState, NotImplemented} import io.iohk.atala.agent.walletapi.model.* import io.iohk.atala.agent.walletapi.model.error.* import io.iohk.atala.agent.walletapi.model.error.DIDSecretStorageError.KeyNotFoundError @@ -28,16 +23,7 @@ import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.error.PresentationError.* import io.iohk.atala.pollux.core.model.error.{CredentialServiceError, PresentationError} import io.iohk.atala.pollux.core.service.{CredentialService, PresentationService} -import io.iohk.atala.pollux.vc.jwt.{ - CredentialVerification, - ES256KSigner, - JWT, - JwtPresentation, - W3CCredential, - W3cCredentialPayload, - DidResolver as JwtDidResolver, - Issuer as JwtIssuer -} +import io.iohk.atala.pollux.vc.jwt.{CredentialVerification, ES256KSigner, JWT, JwtPresentation, W3CCredential, W3cCredentialPayload, DidResolver as JwtDidResolver, Issuer as JwtIssuer} import io.iohk.atala.resolvers.{DIDResolver, UniversalDidResolver} import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -135,7 +121,7 @@ object BackgroundJobs { .provideSomeLayer(didCommAgent) credentialService <- ZIO.service[CredentialService] _ <- { - if (resp.status >= 200 && resp.status < 300) notifyIfSuccessful(credentialService.markOfferSent(id)) + if (resp.status >= 200 && resp.status < 300) credentialService.markOfferSent(id) else ZIO.fail(ErrorResponseReceivedFromPeerAgent(resp)) } } yield () @@ -172,7 +158,7 @@ object BackgroundJobs { jwtIssuer <- createJwtIssuer(longFormPrismDID, VerificationRelationship.Authentication) presentationPayload <- credentialService.createPresentationPayload(id, jwtIssuer) signedPayload = JwtPresentation.encodeJwt(presentationPayload.toJwtPresentationPayload, jwtIssuer) - _ <- notifyIfSuccessful(credentialService.generateCredentialRequest(id, signedPayload)) + _ <- credentialService.generateCredentialRequest(id, signedPayload) } yield () // Request should be sent from Holder to Issuer @@ -205,7 +191,7 @@ object BackgroundJobs { .provideSomeLayer(didCommAgent) credentialService <- ZIO.service[CredentialService] _ <- { - if (resp.status >= 200 && resp.status < 300) notifyIfSuccessful(credentialService.markRequestSent(id)) + if (resp.status >= 200 && resp.status < 300) credentialService.markRequestSent(id) else ZIO.fail(ErrorResponseReceivedFromPeerAgent(resp)) } } yield () @@ -235,7 +221,7 @@ object BackgroundJobs { ) => for { credentialService <- ZIO.service[CredentialService] - _ <- notifyIfSuccessful(credentialService.acceptCredentialRequest(id)) + _ <- credentialService.acceptCredentialRequest(id) } yield () // Credential is pending, can be generated by Issuer and optionally published on-chain @@ -280,7 +266,7 @@ object BackgroundJobs { thid = issue.thid, credentials = Map("prims/jwt" -> signedJwtCredential.value.getBytes) ) - _ <- notifyIfSuccessful(credentialService.markCredentialGenerated(id, issueCredential)) + _ <- credentialService.markCredentialGenerated(id, issueCredential) } yield () @@ -314,7 +300,7 @@ object BackgroundJobs { .provideSomeLayer(didCommAgent) credentialService <- ZIO.service[CredentialService] _ <- { - if (resp.status >= 200 && resp.status < 300) notifyIfSuccessful(credentialService.markCredentialSent(id)) + if (resp.status >= 200 && resp.status < 300) credentialService.markCredentialSent(id) else ZIO.fail(ErrorResponseReceivedFromPeerAgent(resp)) } } yield () @@ -347,7 +333,7 @@ object BackgroundJobs { resp <- MessagingService.send(issue.makeMessage).provideSomeLayer(didCommAgent) credentialService <- ZIO.service[CredentialService] _ <- { - if (resp.status >= 200 && resp.status < 300) notifyIfSuccessful(credentialService.markCredentialSent(id)) + if (resp.status >= 200 && resp.status < 300) credentialService.markCredentialSent(id) else ZIO.fail(ErrorResponseReceivedFromPeerAgent(resp)) } } yield () @@ -711,12 +697,6 @@ object BackgroundJobs { .catchAllDefect(d => ZIO.logErrorCause(s"Present Proof - Defect processing record: ${record.id}", Cause.fail(d))) } - private[this] def notifyIfSuccessful(effect: IO[_, IssueCredentialRecord]) = for { - notificationService <- ZIO.service[EventNotificationService] - record <- effect - _ <- notificationService.notify(Event(s"Record updated => $record")) - } yield () - private[this] def buildDIDCommAgent( myDid: DidId ): ZIO[ManagedDIDService, KeyNotFoundError, ZLayer[Any, Nothing, DidAgent]] = { From 544a81ffc6a90756fd59210f26d47c4b2f4713b0 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Thu, 22 Jun 2023 10:26:25 +0200 Subject: [PATCH 12/56] feat(pollux): use global ZIO http client layer in HttpURIDereferencerImpl --- .../core/service/HttpURIDereferencerImpl.scala | 15 +++++---------- .../scala/io/iohk/atala/agent/server/Main.scala | 3 ++- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/HttpURIDereferencerImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/HttpURIDereferencerImpl.scala index 2dd9d34a8f..fab8e102bb 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/HttpURIDereferencerImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/HttpURIDereferencerImpl.scala @@ -3,14 +3,14 @@ import io.iohk.atala.pollux.core.service.URIDereferencerError.{ConnectionError, import zio.http.* import zio.http.ZClient.ClientLive import zio.http.model.* -import zio.{IO, Layer, Scope, ULayer, ZIO, ZLayer} +import zio.{IO, Layer, Scope, ULayer, URLayer, ZIO, ZLayer} import java.net.URI -class HttpURIDereferencerImpl extends URIDereferencer { +class HttpURIDereferencerImpl(client: Client) extends URIDereferencer { override def dereference(uri: URI): IO[URIDereferencerError, String] = { - val result = for { + val result: ZIO[Client, URIDereferencerError, String] = for { response <- Client.request(uri.toString).mapError(t => ConnectionError(t.getMessage)) body <- response match case Response(Status.Ok, _, body, _, None) => @@ -20,16 +20,11 @@ class HttpURIDereferencerImpl extends URIDereferencer { case Response(_, _, _, _, httpError) => ZIO.fail(UnexpectedError(s"HTTP response error: $httpError")) } yield body - result - .provide(Scope.default >>> Client.default) - .mapError { - case e: URIDereferencerError => e - case t => UnexpectedError(t.toString) - } + result.provide(ZLayer.succeed(client)) } } object HttpURIDereferencerImpl { - val layer: ULayer[URIDereferencer] = ZLayer.succeed(HttpURIDereferencerImpl()) + val layer: URLayer[Client, URIDereferencer] = ZLayer.fromFunction(HttpURIDereferencerImpl(_)) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 82c4d3906d..d3aa3ec410 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -159,7 +159,8 @@ object MainApp extends ZIOAppDefault { ZLayer.succeed(MetricsConfig(5.seconds)), DefaultJvmMetrics.live.unit, SystemControllerImpl.layer, - ZLayer.fromZIO(Queue.bounded[Event](500)) >>> EventNotificationServiceInMemoryImpl.layer + ZLayer.fromZIO(Queue.bounded[Event](500)) >>> EventNotificationServiceInMemoryImpl.layer, + Scope.default >>> Client.default ) } yield app From 905e6298d1a519ba719a4f4be2edcfed0b611366 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Thu, 22 Jun 2023 10:28:54 +0200 Subject: [PATCH 13/56] feat(prism-agent): implement real HTTP calls in Webhook publisher --- .../agent/notification/WebhookPublisher.scala | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index a70ddb495c..a2c040d701 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -2,7 +2,10 @@ package io.iohk.atala.agent.notification import io.iohk.atala.agent.notification.WebhookPublisherError.{InvalidWebhookURL, UnexpectedError} import io.iohk.atala.agent.server.config.{AppConfig, WebhookPublisherConfig} import io.iohk.atala.event.notification.{Event, EventNotificationService} -import zio.{IO, UIO, URLayer, ZIO, ZLayer} +import zio.* +import zio.http.* +import zio.http.ZClient.ClientLive +import zio.http.model.* import java.net.{URI, URL} @@ -17,7 +20,7 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat case None => 1 } - val run: IO[WebhookPublisherError, Unit] = config.url match { + val run: ZIO[Client, WebhookPublisherError, Unit] = config.url match { case Some(url) => for { url <- ZIO.attempt(URL(url)).mapError(th => InvalidWebhookURL(s"$url [${th.getMessage}]")) @@ -26,15 +29,35 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat _ <- ZIO.log(s"Polling $parallelism event(s)") events <- consumer.poll(parallelism).mapError(e => UnexpectedError(e.toString)) _ <- ZIO.log(s"Got ${events.size} event(s)") - _ <- ZIO.foreachPar(events)(e => notifyWebhook(e, url)) + _ <- ZIO.foreachPar(events)(e => + notifyWebhook(e, url) + .retry(Schedule.spaced(5.second) && Schedule.recurs(2)) + .catchAll(e => ZIO.logError(s"Webhook permanently failing, with last error being: ${e.msg}")) + ) } yield () poll <- pollAndNotify.forever } yield poll case None => ZIO.unit } - private[this] def notifyWebhook(event: Event, url: URL): UIO[Unit] = - ZIO.log(s"Sending event: $event to webhook URL: $url with API key ${config.apiKey}") + private[this] def notifyWebhook(event: Event, url: URL): ZIO[Client, UnexpectedError, Unit] = { + for { + _ <- ZIO.log(s"Sending event: $event to HTTP webhook URL: $url with API key ${config.apiKey}") + response <- Client + // TODO serialize event to JSON here + .request(url = url.toString, method = Method.POST, content = Body.fromString(event.content)) + .mapError(t => UnexpectedError(s"Webhook request error: $t")) + resp <- response match + case Response(status, _, _, _, _) if status.isSuccess => + ZIO.unit + case Response(status, _, _, _, maybeHttpError) => + ZIO.fail( + UnexpectedError( + s"Unsuccessful webhook response: [status: $status] [error: ${maybeHttpError.getOrElse("none")}]" + ) + ) + } yield resp + } } object WebhookPublisher { From 88a7bd41dcead61e8ebe25e13a7da5641a7bb500 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Thu, 22 Jun 2023 15:31:43 +0200 Subject: [PATCH 14/56] feat(prism-agent): include WEBHOOK_API_KEY in request Authorization header when provided --- .../atala/agent/notification/WebhookPublisher.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index a2c040d701..d77d590105 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -5,13 +5,14 @@ import io.iohk.atala.event.notification.{Event, EventNotificationService} import zio.* import zio.http.* import zio.http.ZClient.ClientLive -import zio.http.model.* +import zio.http.model.{Header, Headers, Method} import java.net.{URI, URL} class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificationService) { private val config = appConfig.agent.webhookPublisher + private val baseHeaders = config.apiKey.map(key => Headers.authorization(key)).getOrElse(Headers.empty) private val parallelism = config.parallelism match { case Some(p) if p < 1 => 1 @@ -45,7 +46,12 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat _ <- ZIO.log(s"Sending event: $event to HTTP webhook URL: $url with API key ${config.apiKey}") response <- Client // TODO serialize event to JSON here - .request(url = url.toString, method = Method.POST, content = Body.fromString(event.content)) + .request( + url = url.toString, + method = Method.POST, + headers = baseHeaders, + content = Body.fromString(event.content) + ) .mapError(t => UnexpectedError(s"Webhook request error: $t")) resp <- response match case Response(status, _, _, _, _) if status.isSuccess => From 1c49fcbfd7d3789f6c96eecbe43facf3637439bd Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Thu, 22 Jun 2023 15:33:08 +0200 Subject: [PATCH 15/56] chore(prism-agent): declare WALLET_SEED and WEBHOOK_API_KEY in docker compose config --- infrastructure/shared/docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/infrastructure/shared/docker-compose.yml b/infrastructure/shared/docker-compose.yml index 0e1c8bcdeb..adbf124e2c 100644 --- a/infrastructure/shared/docker-compose.yml +++ b/infrastructure/shared/docker-compose.yml @@ -98,7 +98,9 @@ services: VAULT_ADDR: ${VAULT_ADDR:-http://vault-server:8200} VAULT_TOKEN: ${VAULT_DEV_ROOT_TOKEN_ID:-root} DEV_MODE: "true" - WEBHOOK_URL: https://localhost:8080/my-webhook + WALLET_SEED: + WEBHOOK_URL: + WEBHOOK_API_KEY: depends_on: db: condition: service_healthy From de36607bfa4ab4683ba01da6249530bde54bed29 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Mon, 26 Jun 2023 13:43:04 +0200 Subject: [PATCH 16/56] chore(prism-agent): complete main branch merging --- .../io/iohk/atala/agent/server/Main.scala | 28 ++++++++----------- .../atala/agent/server/PrismAgentApp.scala | 22 ++++++--------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 5adaeeb6f7..497eb01ccd 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -3,39 +3,31 @@ package io.iohk.atala.agent.server import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton import io.iohk.atala.agent.server.http.ZioHttpClient import io.iohk.atala.agent.server.sql.Migrations as AgentMigrations -import io.iohk.atala.agent.walletapi.service.ManagedDIDService -import io.iohk.atala.agent.walletapi.service.ManagedDIDServiceImpl +import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, ManagedDIDServiceImpl} import io.iohk.atala.agent.walletapi.sql.JdbcDIDNonSecretStorage import io.iohk.atala.castor.controller.{DIDControllerImpl, DIDRegistrarControllerImpl} import io.iohk.atala.castor.core.service.DIDServiceImpl import io.iohk.atala.castor.core.util.DIDOperationValidator import io.iohk.atala.connect.controller.ConnectionControllerImpl import io.iohk.atala.connect.core.service.ConnectionServiceImpl -import io.iohk.atala.connect.sql.repository.JdbcConnectionRepository -import io.iohk.atala.connect.sql.repository.Migrations as ConnectMigrations +import io.iohk.atala.connect.sql.repository.{JdbcConnectionRepository, Migrations as ConnectMigrations} +import io.iohk.atala.event.notification.{Event, EventNotificationServiceInMemoryImpl} import io.iohk.atala.issue.controller.IssueControllerImpl import io.iohk.atala.mercury.* -import io.iohk.atala.pollux.core.service.CredentialServiceImpl -import io.iohk.atala.pollux.core.service.PresentationServiceImpl -import io.iohk.atala.pollux.core.service.VerificationPolicyServiceImpl -import io.iohk.atala.pollux.core.service.{CredentialSchemaServiceImpl, URIDereferencer, HttpURIDereferencerImpl} -import io.iohk.atala.pollux.credentialschema.controller.CredentialSchemaController -import io.iohk.atala.pollux.credentialschema.controller.CredentialSchemaControllerImpl -import io.iohk.atala.pollux.credentialschema.controller.VerificationPolicyControllerImpl -import io.iohk.atala.pollux.sql.repository.JdbcCredentialRepository -import io.iohk.atala.pollux.sql.repository.JdbcPresentationRepository -import io.iohk.atala.pollux.sql.repository.JdbcVerificationPolicyRepository -import io.iohk.atala.pollux.sql.repository.{JdbcCredentialSchemaRepository, Migrations as PolluxMigrations} +import io.iohk.atala.pollux.core.service.* +import io.iohk.atala.pollux.credentialschema.controller.{CredentialSchemaController, CredentialSchemaControllerImpl, VerificationPolicyControllerImpl} +import io.iohk.atala.pollux.sql.repository.{JdbcCredentialRepository, JdbcCredentialSchemaRepository, JdbcPresentationRepository, JdbcVerificationPolicyRepository, Migrations as PolluxMigrations} import io.iohk.atala.presentproof.controller.PresentProofControllerImpl import io.iohk.atala.resolvers.DIDResolver import io.iohk.atala.system.controller.SystemControllerImpl -import java.security.Security import zio.* import zio.http.Client import zio.metrics.connectors.prometheus.PrometheusPublisher import zio.metrics.connectors.{MetricsConfig, prometheus} import zio.metrics.jvm.DefaultJvmMetrics +import java.security.Security + object MainApp extends ZIOAppDefault { Security.insertProviderAt(BouncyCastleProviderSingleton.getInstance(), 2) @@ -125,7 +117,7 @@ object MainApp extends ZIOAppDefault { // service ConnectionServiceImpl.layer, CredentialSchemaServiceImpl.layer, - CredentialServiceImpl.layer, + CredentialServiceWithEventNotificationImpl.layer, DIDServiceImpl.layer, ManagedDIDServiceImpl.layer, PresentationServiceImpl.layer, @@ -141,6 +133,8 @@ object MainApp extends ZIOAppDefault { RepoModule.polluxTransactorLayer >>> JdbcCredentialSchemaRepository.layer, RepoModule.polluxTransactorLayer >>> JdbcPresentationRepository.layer, RepoModule.polluxTransactorLayer >>> JdbcVerificationPolicyRepository.layer, + // event notification service + ZLayer.fromZIO(Queue.bounded[Event](500)) >>> EventNotificationServiceInMemoryImpl.layer, // HTTP client Scope.default >>> Client.default, ) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala index ca2cde8d86..9e9dce8727 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala @@ -1,24 +1,19 @@ package io.iohk.atala.agent.server +import io.iohk.atala.agent.notification.WebhookPublisher import io.iohk.atala.agent.server.config.AppConfig -import io.iohk.atala.agent.server.http.ZHttp4sBlazeServer -import io.iohk.atala.agent.server.http.ZHttpEndpoints -import io.iohk.atala.agent.server.jobs.BackgroundJobs -import io.iohk.atala.agent.server.jobs.ConnectBackgroundJobs +import io.iohk.atala.agent.server.http.{ZHttp4sBlazeServer, ZHttpEndpoints} +import io.iohk.atala.agent.server.jobs.{BackgroundJobs, ConnectBackgroundJobs} import io.iohk.atala.agent.walletapi.service.ManagedDIDService -import io.iohk.atala.castor.controller.DIDRegistrarServerEndpoints -import io.iohk.atala.castor.controller.DIDServerEndpoints +import io.iohk.atala.castor.controller.{DIDRegistrarServerEndpoints, DIDServerEndpoints} import io.iohk.atala.castor.core.service.DIDService import io.iohk.atala.connect.controller.ConnectionServerEndpoints import io.iohk.atala.connect.core.service.ConnectionService import io.iohk.atala.issue.controller.IssueServerEndpoints -import io.iohk.atala.mercury.DidOps -import io.iohk.atala.mercury.HttpClient -import io.iohk.atala.pollux.core.service.CredentialService -import io.iohk.atala.pollux.core.service.PresentationService -import io.iohk.atala.pollux.credentialschema.SchemaRegistryServerEndpoints -import io.iohk.atala.pollux.credentialschema.VerificationPolicyServerEndpoints -import io.iohk.atala.pollux.vc.jwt.{DidResolver as JwtDidResolver} +import io.iohk.atala.mercury.{DidOps, HttpClient} +import io.iohk.atala.pollux.core.service.{CredentialService, PresentationService} +import io.iohk.atala.pollux.credentialschema.{SchemaRegistryServerEndpoints, VerificationPolicyServerEndpoints} +import io.iohk.atala.pollux.vc.jwt.DidResolver as JwtDidResolver import io.iohk.atala.presentproof.controller.PresentProofServerEndpoints import io.iohk.atala.resolvers.DIDResolver import io.iohk.atala.system.controller.SystemServerEndpoints @@ -33,6 +28,7 @@ object PrismAgentApp { _ <- syncDIDPublicationStateFromDltJob.fork _ <- AgentHttpServer.run.fork fiber <- DidCommHttpServer.run(didCommServicePort).fork + _ <- ZIO.scoped(WebhookPublisher.layer.build.map(_.get[WebhookPublisher])).flatMap(_.run.debug.fork) _ <- fiber.join *> ZIO.log(s"Server End") _ <- ZIO.never } yield () From d542e0d7d2fbeceeff8f21b8886233b6a042d685 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Tue, 27 Jun 2023 21:05:25 +0200 Subject: [PATCH 17/56] feature(prism-agent): refactor event notification and make it more generic --- .../iohk/atala/event/notification/Event.scala | 2 +- .../event/notification/EventConsumer.scala | 7 +-- .../EventNotificationService.scala | 12 +++-- .../EventNotificationServiceError.scala | 9 ++++ ...EventNotificationServiceInMemoryImpl.scala | 46 ++++++++++++---- .../event/notification/EventProducer.scala | 6 +++ ...tialServiceWithEventNotificationImpl.scala | 54 +++++++++++-------- .../agent/notification/WebhookPublisher.scala | 17 ++++-- .../io/iohk/atala/agent/server/Main.scala | 2 +- 9 files changed, 109 insertions(+), 46 deletions(-) create mode 100644 event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceError.scala create mode 100644 event-notification/src/main/scala/io/iohk/atala/event/notification/EventProducer.scala diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala index 0beec8bc7b..40d49d2538 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala @@ -1,3 +1,3 @@ package io.iohk.atala.event.notification -case class Event(content: String) +case class Event[A](data: A) diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventConsumer.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventConsumer.scala index fdcab53a86..3b65858bd5 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventConsumer.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventConsumer.scala @@ -2,8 +2,5 @@ package io.iohk.atala.event.notification import zio.IO -trait EventConsumer: - def poll(count: Int): IO[EventConsumer.Error, Seq[Event]] - -object EventConsumer: - sealed trait Error +trait EventConsumer[A]: + def poll(count: Int): IO[EventNotificationServiceError, Seq[Event[A]]] diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationService.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationService.scala index b70f9f82c7..61066d3955 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationService.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationService.scala @@ -1,10 +1,14 @@ package io.iohk.atala.event.notification +import io.iohk.atala.event.notification.EventNotificationServiceError.{DecoderError, EncoderError} import zio.IO trait EventNotificationService: - def notify(event: Event): IO[EventNotificationService.Error, Unit] - def subscribe(topic: String): IO[EventNotificationService.Error, EventConsumer] + def consumer[A](topic: String)(using decoder: EventDecoder[A]): IO[EventNotificationServiceError, EventConsumer[A]] + def producer[A](topic: String)(using encoder: EventEncoder[A]): IO[EventNotificationServiceError, EventProducer[A]] -object EventNotificationService: - sealed trait Error +trait EventEncoder[A]: + def encode(data: A): IO[EncoderError, Any] + +trait EventDecoder[A]: + def decode(data: Any): IO[DecoderError, A] diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceError.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceError.scala new file mode 100644 index 0000000000..00aaf1a817 --- /dev/null +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceError.scala @@ -0,0 +1,9 @@ +package io.iohk.atala.event.notification + +sealed trait EventNotificationServiceError + +object EventNotificationServiceError { + case class EventSendingFailed(msg: String) extends EventNotificationServiceError + case class EncoderError(msg: String) extends EventNotificationServiceError + case class DecoderError(msg: String) extends EventNotificationServiceError +} diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala index 78607b0403..439bdc7651 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala @@ -1,19 +1,47 @@ package io.iohk.atala.event.notification +import io.iohk.atala.event.notification.EventNotificationServiceError.EventSendingFailed import zio.{IO, Queue, ULayer, URLayer, ZIO, ZLayer} -class EventNotificationServiceInMemoryImpl(queue: Queue[Event]) extends EventNotificationService { +import scala.collection.mutable - override def notify(event: Event): IO[EventNotificationService.Error, Unit] = - queue.offer(event).unit +class EventNotificationServiceInMemoryImpl extends EventNotificationService: + private[this] val queueMap = mutable.Map.empty[String, Queue[Event[Any]]] - override def subscribe(topic: String): IO[EventNotificationService.Error, EventConsumer] = - ZIO.succeed(new EventConsumer { - override def poll(count: Int): IO[EventConsumer.Error, Seq[Event]] = queue.takeBetween(1, count) + private[this] def getOrCreateQueue(topic: String): IO[EventNotificationServiceError, Queue[Event[Any]]] = { + for { + maybeQueue <- ZIO.succeed(queueMap.get(topic)) + queue <- maybeQueue match + case Some(value) => ZIO.succeed(value) + case None => Queue.bounded(500) + _ <- ZIO.succeed(queueMap.put(topic, queue)) + } yield queue + } + + override def consumer[A]( + topic: String + )(using decoder: EventDecoder[A]): IO[EventNotificationServiceError, EventConsumer[A]] = + ZIO.succeed(new EventConsumer[A] { + override def poll(count: Int): IO[EventNotificationServiceError, Seq[Event[A]]] = for { + queue <- getOrCreateQueue(topic) + events <- queue.takeBetween(1, count) + decodedEvents <- ZIO.foreach(events)(e => decoder.decode(e.data).map(d => Event(d))) + } yield decodedEvents + }) + + override def producer[A]( + topic: String + )(using encoder: EventEncoder[A]): IO[EventNotificationServiceError, EventProducer[A]] = + ZIO.succeed(new EventProducer[A] { + override def send(event: Event[A]): IO[EventNotificationServiceError, Unit] = for { + queue <- getOrCreateQueue(topic) + encodedEvent <- encoder.encode(event.data).map(e => Event(e)) + succeeded <- queue.offer(encodedEvent) + _ <- if (succeeded) ZIO.unit else ZIO.fail(EventSendingFailed("Queue.offer returned 'false'")) + } yield () }) -} object EventNotificationServiceInMemoryImpl { - val layer: URLayer[Queue[Event], EventNotificationServiceInMemoryImpl] = - ZLayer.fromFunction(new EventNotificationServiceInMemoryImpl(_)) + val layer: ULayer[EventNotificationServiceInMemoryImpl] = + ZLayer.succeed(new EventNotificationServiceInMemoryImpl()) } diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventProducer.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventProducer.scala new file mode 100644 index 0000000000..9fec9c2f8d --- /dev/null +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventProducer.scala @@ -0,0 +1,6 @@ +package io.iohk.atala.event.notification + +import zio.IO + +trait EventProducer[A]: + def send(event: Event[A]): IO[EventNotificationServiceError, Unit] diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala index c794778558..282ae320bd 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala @@ -2,15 +2,17 @@ package io.iohk.atala.pollux.core.service import io.circe.Json import io.iohk.atala.castor.core.model.did.CanonicalPrismDID -import io.iohk.atala.event.notification.{Event, EventNotificationService} +import io.iohk.atala.event.notification.* +import io.iohk.atala.event.notification.EventNotificationServiceError.EncoderError import io.iohk.atala.iris.proto.service.IrisServiceGrpc.IrisServiceStub import io.iohk.atala.mercury.model.DidId import io.iohk.atala.mercury.protocol.issuecredential.{IssueCredential, OfferCredential, RequestCredential} import io.iohk.atala.pollux.core.model.error.CredentialServiceError import io.iohk.atala.pollux.core.model.{DidCommID, IssueCredentialRecord} import io.iohk.atala.pollux.core.repository.CredentialRepository +import io.iohk.atala.pollux.core.service.CredentialServiceWithEventNotificationImpl.given import io.iohk.atala.pollux.vc.jwt.{DidResolver, JWT} -import zio.{IO, Task, ULayer, URLayer, ZIO, ZLayer} +import zio.{IO, Task, UIO, ULayer, URLayer, ZIO, ZLayer} class CredentialServiceWithEventNotificationImpl( irisClient: IrisServiceStub, @@ -31,7 +33,7 @@ class CredentialServiceWithEventNotificationImpl( awaitConfirmation: Option[Boolean], issuingDID: Option[CanonicalPrismDID] ): IO[CredentialServiceError, IssueCredentialRecord] = - notify( + notifyOnSuccess( super.createIssueCredentialRecord( pairwiseIssuerDID, pairwiseHolderDID, @@ -46,54 +48,64 @@ class CredentialServiceWithEventNotificationImpl( ) override def markOfferSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = - notify(super.markOfferSent(recordId)) + notifyOnSuccess(super.markOfferSent(recordId)) override def receiveCredentialOffer(offer: OfferCredential): IO[CredentialServiceError, IssueCredentialRecord] = - notify(super.receiveCredentialOffer(offer)) + notifyOnSuccess(super.receiveCredentialOffer(offer)) override def acceptCredentialOffer( recordId: DidCommID, subjectId: String ): IO[CredentialServiceError, IssueCredentialRecord] = - notify(super.acceptCredentialOffer(recordId, subjectId)) + notifyOnSuccess(super.acceptCredentialOffer(recordId, subjectId)) override def generateCredentialRequest( recordId: DidCommID, signedPresentation: JWT ): IO[CredentialServiceError, IssueCredentialRecord] = - notify(super.generateCredentialRequest(recordId, signedPresentation)) + notifyOnSuccess(super.generateCredentialRequest(recordId, signedPresentation)) override def markRequestSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = - notify(super.markRequestSent(recordId)) + notifyOnSuccess(super.markRequestSent(recordId)) override def receiveCredentialRequest(request: RequestCredential): IO[CredentialServiceError, IssueCredentialRecord] = - notify(super.receiveCredentialRequest(request)) + notifyOnSuccess(super.receiveCredentialRequest(request)) override def acceptCredentialRequest(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = - notify(super.acceptCredentialRequest(recordId)) + notifyOnSuccess(super.acceptCredentialRequest(recordId)) override def markCredentialGenerated( recordId: DidCommID, issueCredential: IssueCredential ): IO[CredentialServiceError, IssueCredentialRecord] = - notify(super.markCredentialGenerated(recordId, issueCredential)) + notifyOnSuccess(super.markCredentialGenerated(recordId, issueCredential)) override def markCredentialSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = - notify(super.markCredentialSent(recordId)) + notifyOnSuccess(super.markCredentialSent(recordId)) override def receiveCredentialIssue(issue: IssueCredential): IO[CredentialServiceError, IssueCredentialRecord] = - notify(super.receiveCredentialIssue(issue)) - - // Notification method - private[this] def notify(effect: IO[CredentialServiceError, IssueCredentialRecord]) = for { - record <- effect - _ <- eventNotificationService - .notify(Event(s"${record.protocolState.toString} [${record.id}]")) - .catchAll(e => ZIO.logError(s"Notification service error: $e")) - } yield record + notifyOnSuccess(super.receiveCredentialIssue(issue)) + + private[this] def notifyOnSuccess(effect: IO[CredentialServiceError, IssueCredentialRecord]) = + for { + record <- effect + _ <- notify(record) + } yield record + + private[this] def notify(record: IssueCredentialRecord) = { + val result = for { + producer <- eventNotificationService.producer[IssueCredentialRecord]("Connect") + _ <- producer.send(Event(record)) + } yield () + result.catchAll(e => ZIO.logError(s"Notification service error: $e")) + } } object CredentialServiceWithEventNotificationImpl { + + given EventEncoder[IssueCredentialRecord] = (data: IssueCredentialRecord) => + ZIO.attempt(data.asInstanceOf[Any]).mapError(t => EncoderError(t.getMessage)) + val layer: URLayer[ IrisServiceStub with CredentialRepository[Task] with DidResolver with URIDereferencer with EventNotificationService, CredentialServiceWithEventNotificationImpl diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index d77d590105..6bdd02e0ce 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -1,7 +1,10 @@ package io.iohk.atala.agent.notification +import io.iohk.atala.agent.notification.WebhookPublisher.given import io.iohk.atala.agent.notification.WebhookPublisherError.{InvalidWebhookURL, UnexpectedError} import io.iohk.atala.agent.server.config.{AppConfig, WebhookPublisherConfig} -import io.iohk.atala.event.notification.{Event, EventNotificationService} +import io.iohk.atala.event.notification.EventNotificationServiceError.DecoderError +import io.iohk.atala.event.notification.{Event, EventDecoder, EventNotificationService} +import io.iohk.atala.pollux.core.model.IssueCredentialRecord import zio.* import zio.http.* import zio.http.ZClient.ClientLive @@ -25,7 +28,7 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat case Some(url) => for { url <- ZIO.attempt(URL(url)).mapError(th => InvalidWebhookURL(s"$url [${th.getMessage}]")) - consumer <- notificationService.subscribe("ALL").mapError(e => UnexpectedError(e.toString)) + consumer <- notificationService.consumer("Connect").mapError(e => UnexpectedError(e.toString)) pollAndNotify = for { _ <- ZIO.log(s"Polling $parallelism event(s)") events <- consumer.poll(parallelism).mapError(e => UnexpectedError(e.toString)) @@ -41,16 +44,16 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat case None => ZIO.unit } - private[this] def notifyWebhook(event: Event, url: URL): ZIO[Client, UnexpectedError, Unit] = { + private[this] def notifyWebhook[A](event: Event[A], url: URL): ZIO[Client, UnexpectedError, Unit] = { for { _ <- ZIO.log(s"Sending event: $event to HTTP webhook URL: $url with API key ${config.apiKey}") response <- Client - // TODO serialize event to JSON here .request( url = url.toString, method = Method.POST, headers = baseHeaders, - content = Body.fromString(event.content) + // TODO serialize event to JSON here + content = Body.fromString(event.data.toString) ) .mapError(t => UnexpectedError(s"Webhook request error: $t")) resp <- response match @@ -67,6 +70,10 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat } object WebhookPublisher { + + given EventDecoder[IssueCredentialRecord] = (data: Any) => + ZIO.attempt(data.asInstanceOf[IssueCredentialRecord]).mapError(t => DecoderError(t.getMessage)) + val layer: URLayer[AppConfig & EventNotificationService, WebhookPublisher] = ZLayer.fromFunction(WebhookPublisher(_, _)) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 497eb01ccd..0521502da6 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -134,7 +134,7 @@ object MainApp extends ZIOAppDefault { RepoModule.polluxTransactorLayer >>> JdbcPresentationRepository.layer, RepoModule.polluxTransactorLayer >>> JdbcVerificationPolicyRepository.layer, // event notification service - ZLayer.fromZIO(Queue.bounded[Event](500)) >>> EventNotificationServiceInMemoryImpl.layer, + EventNotificationServiceInMemoryImpl.layer, // HTTP client Scope.default >>> Client.default, ) From 8100b74b9bbd57ffe7f24a719aa807c606bb7bf1 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Wed, 28 Jun 2023 16:51:30 +0200 Subject: [PATCH 18/56] feature(prism-agent): add connect & presentation consumers in Webhook publisher --- ...tialServiceWithEventNotificationImpl.scala | 2 +- .../agent/notification/WebhookPublisher.scala | 35 +++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala index 282ae320bd..69bc3e3a69 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala @@ -94,7 +94,7 @@ class CredentialServiceWithEventNotificationImpl( private[this] def notify(record: IssueCredentialRecord) = { val result = for { - producer <- eventNotificationService.producer[IssueCredentialRecord]("Connect") + producer <- eventNotificationService.producer[IssueCredentialRecord]("Issue") _ <- producer.send(Event(record)) } yield () result.catchAll(e => ZIO.logError(s"Notification service error: $e")) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index 6bdd02e0ce..f24e9e8159 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -3,7 +3,7 @@ import io.iohk.atala.agent.notification.WebhookPublisher.given import io.iohk.atala.agent.notification.WebhookPublisherError.{InvalidWebhookURL, UnexpectedError} import io.iohk.atala.agent.server.config.{AppConfig, WebhookPublisherConfig} import io.iohk.atala.event.notification.EventNotificationServiceError.DecoderError -import io.iohk.atala.event.notification.{Event, EventDecoder, EventNotificationService} +import io.iohk.atala.event.notification.{Event, EventConsumer, EventDecoder, EventNotificationService} import io.iohk.atala.pollux.core.model.IssueCredentialRecord import zio.* import zio.http.* @@ -28,22 +28,29 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat case Some(url) => for { url <- ZIO.attempt(URL(url)).mapError(th => InvalidWebhookURL(s"$url [${th.getMessage}]")) - consumer <- notificationService.consumer("Connect").mapError(e => UnexpectedError(e.toString)) - pollAndNotify = for { - _ <- ZIO.log(s"Polling $parallelism event(s)") - events <- consumer.poll(parallelism).mapError(e => UnexpectedError(e.toString)) - _ <- ZIO.log(s"Got ${events.size} event(s)") - _ <- ZIO.foreachPar(events)(e => - notifyWebhook(e, url) - .retry(Schedule.spaced(5.second) && Schedule.recurs(2)) - .catchAll(e => ZIO.logError(s"Webhook permanently failing, with last error being: ${e.msg}")) - ) - } yield () - poll <- pollAndNotify.forever - } yield poll + connectConsumer <- notificationService.consumer("Connect").mapError(e => UnexpectedError(e.toString)) + issueConsumer <- notificationService.consumer("Issue").mapError(e => UnexpectedError(e.toString)) + presentationConsumer <- notificationService.consumer("Presentation").mapError(e => UnexpectedError(e.toString)) + _ <- pollAndNotify(connectConsumer, url).forever.debug.forkDaemon + _ <- pollAndNotify(issueConsumer, url).forever.debug.forkDaemon + _ <- pollAndNotify(presentationConsumer, url).forever.debug.forkDaemon + } yield () case None => ZIO.unit } + private[this] def pollAndNotify[A](consumer: EventConsumer[A], url: URL) = { + for { + _ <- ZIO.log(s"Polling $parallelism event(s)") + events <- consumer.poll(parallelism).mapError(e => UnexpectedError(e.toString)) + _ <- ZIO.log(s"Got ${events.size} event(s)") + _ <- ZIO.foreachPar(events)(e => + notifyWebhook(e, url) + .retry(Schedule.spaced(5.second) && Schedule.recurs(2)) + .catchAll(e => ZIO.logError(s"Webhook permanently failing, with last error being: ${e.msg}")) + ) + } yield () + } + private[this] def notifyWebhook[A](event: Event[A], url: URL): ZIO[Client, UnexpectedError, Unit] = { for { _ <- ZIO.log(s"Sending event: $event to HTTP webhook URL: $url with API key ${config.apiKey}") From 4d3fef58b7783f8cb7b78378764dc396c3c2a5c6 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Wed, 28 Jun 2023 17:07:58 +0200 Subject: [PATCH 19/56] chore(prism-agent): use postgres secret storage implementation in default docker-compose config --- infrastructure/shared/docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infrastructure/shared/docker-compose.yml b/infrastructure/shared/docker-compose.yml index c3739abb4b..f365a0271b 100644 --- a/infrastructure/shared/docker-compose.yml +++ b/infrastructure/shared/docker-compose.yml @@ -97,7 +97,8 @@ services: PRISM_NODE_PORT: 50053 VAULT_ADDR: ${VAULT_ADDR:-http://vault-server:8200} VAULT_TOKEN: ${VAULT_DEV_ROOT_TOKEN_ID:-root} - DEV_MODE: "true" + SECRET_STORAGE_BACKEND: postgres + DEV_MODE: true WALLET_SEED: WEBHOOK_URL: WEBHOOK_API_KEY: From f869d6f98c6cf138815843f244c7ccf691ee1303 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Thu, 29 Jun 2023 14:44:15 +0200 Subject: [PATCH 20/56] feat(prism-agent): add event notification sending for connect flow --- build.sbt | 2 +- ...tionServiceWithEventNotificationImpl.scala | 70 +++++++++++++++++++ .../agent/notification/WebhookPublisher.scala | 20 ++++-- .../io/iohk/atala/agent/server/Main.scala | 4 +- 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImpl.scala diff --git a/build.sbt b/build.sbt index 9d4d8cde63..335fdb821c 100644 --- a/build.sbt +++ b/build.sbt @@ -665,7 +665,7 @@ lazy val connectCore = project Test / publishArtifact := true ) .dependsOn(shared) - .dependsOn(protocolConnection, protocolReportProblem) + .dependsOn(protocolConnection, protocolReportProblem, eventNotification) lazy val connectDoobie = project .in(file("connect/lib/sql-doobie")) diff --git a/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImpl.scala b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImpl.scala new file mode 100644 index 0000000000..4b34e9a02a --- /dev/null +++ b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImpl.scala @@ -0,0 +1,70 @@ +package io.iohk.atala.connect.core.service + +import io.iohk.atala.connect.core.model.ConnectionRecord +import io.iohk.atala.connect.core.model.error.ConnectionServiceError +import io.iohk.atala.connect.core.repository.ConnectionRepository +import io.iohk.atala.connect.core.service.ConnectionServiceWithEventNotificationImpl.given +import io.iohk.atala.event.notification.EventNotificationServiceError.EncoderError +import io.iohk.atala.event.notification.{Event, EventEncoder, EventNotificationService} +import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.mercury.protocol.connection.{ConnectionRequest, ConnectionResponse} +import zio.{IO, Task, URLayer, ZIO, ZLayer} + +import java.util.UUID + +class ConnectionServiceWithEventNotificationImpl( + connectionRepository: ConnectionRepository[Task], + eventNotificationService: EventNotificationService +) extends ConnectionServiceImpl(connectionRepository) { + override def createConnectionInvitation( + label: Option[String], + pairwiseDID: DidId + ): IO[ConnectionServiceError, ConnectionRecord] = + notifyOnSuccess(super.createConnectionInvitation(label, pairwiseDID)) + + override def receiveConnectionInvitation(invitation: String): IO[ConnectionServiceError, ConnectionRecord] = + notifyOnSuccess(super.receiveConnectionInvitation(invitation)) + + override def acceptConnectionInvitation( + recordId: UUID, + pairwiseDid: DidId + ): IO[ConnectionServiceError, ConnectionRecord] = + notifyOnSuccess(super.acceptConnectionInvitation(recordId, pairwiseDid)) + + override def markConnectionRequestSent(recordId: UUID): IO[ConnectionServiceError, ConnectionRecord] = + notifyOnSuccess(super.markConnectionRequestSent(recordId)) + + override def receiveConnectionRequest(request: ConnectionRequest): IO[ConnectionServiceError, ConnectionRecord] = + notifyOnSuccess(super.receiveConnectionRequest(request)) + + override def acceptConnectionRequest(recordId: UUID): IO[ConnectionServiceError, ConnectionRecord] = + notifyOnSuccess(super.acceptConnectionRequest(recordId)) + + override def markConnectionResponseSent(recordId: UUID): IO[ConnectionServiceError, ConnectionRecord] = + notifyOnSuccess(super.markConnectionResponseSent(recordId)) + + override def receiveConnectionResponse(response: ConnectionResponse): IO[ConnectionServiceError, ConnectionRecord] = + notifyOnSuccess(super.receiveConnectionResponse(response)) + + private[this] def notifyOnSuccess(effect: IO[ConnectionServiceError, ConnectionRecord]) = + for { + record <- effect + _ <- notify(record) + } yield record + + private[this] def notify(record: ConnectionRecord) = { + val result = for { + producer <- eventNotificationService.producer[ConnectionRecord]("Connect") + _ <- producer.send(Event(record)) + } yield () + result.catchAll(e => ZIO.logError(s"Notification service error: $e")) + } +} + +object ConnectionServiceWithEventNotificationImpl { + given EventEncoder[ConnectionRecord] = (data: ConnectionRecord) => + ZIO.attempt(data.asInstanceOf[Any]).mapError(t => EncoderError(t.getMessage)) + + val layer: URLayer[ConnectionRepository[Task] with EventNotificationService, ConnectionService] = + ZLayer.fromFunction(ConnectionServiceWithEventNotificationImpl(_, _)) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index f24e9e8159..2e1cca2fd7 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -2,9 +2,10 @@ package io.iohk.atala.agent.notification import io.iohk.atala.agent.notification.WebhookPublisher.given import io.iohk.atala.agent.notification.WebhookPublisherError.{InvalidWebhookURL, UnexpectedError} import io.iohk.atala.agent.server.config.{AppConfig, WebhookPublisherConfig} +import io.iohk.atala.connect.core.model.ConnectionRecord import io.iohk.atala.event.notification.EventNotificationServiceError.DecoderError import io.iohk.atala.event.notification.{Event, EventConsumer, EventDecoder, EventNotificationService} -import io.iohk.atala.pollux.core.model.IssueCredentialRecord +import io.iohk.atala.pollux.core.model.{IssueCredentialRecord, PresentationRecord} import zio.* import zio.http.* import zio.http.ZClient.ClientLive @@ -28,9 +29,15 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat case Some(url) => for { url <- ZIO.attempt(URL(url)).mapError(th => InvalidWebhookURL(s"$url [${th.getMessage}]")) - connectConsumer <- notificationService.consumer("Connect").mapError(e => UnexpectedError(e.toString)) - issueConsumer <- notificationService.consumer("Issue").mapError(e => UnexpectedError(e.toString)) - presentationConsumer <- notificationService.consumer("Presentation").mapError(e => UnexpectedError(e.toString)) + connectConsumer <- notificationService + .consumer[ConnectionRecord]("Connect") + .mapError(e => UnexpectedError(e.toString)) + issueConsumer <- notificationService + .consumer[IssueCredentialRecord]("Issue") + .mapError(e => UnexpectedError(e.toString)) + presentationConsumer <- notificationService + .consumer[PresentationRecord]("Presentation") + .mapError(e => UnexpectedError(e.toString)) _ <- pollAndNotify(connectConsumer, url).forever.debug.forkDaemon _ <- pollAndNotify(issueConsumer, url).forever.debug.forkDaemon _ <- pollAndNotify(presentationConsumer, url).forever.debug.forkDaemon @@ -77,10 +84,15 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat } object WebhookPublisher { + given EventDecoder[ConnectionRecord] = (data: Any) => + ZIO.attempt(data.asInstanceOf[ConnectionRecord]).mapError(t => DecoderError(t.getMessage)) given EventDecoder[IssueCredentialRecord] = (data: Any) => ZIO.attempt(data.asInstanceOf[IssueCredentialRecord]).mapError(t => DecoderError(t.getMessage)) + given EventDecoder[PresentationRecord] = (data: Any) => + ZIO.attempt(data.asInstanceOf[PresentationRecord]).mapError(t => DecoderError(t.getMessage)) + val layer: URLayer[AppConfig & EventNotificationService, WebhookPublisher] = ZLayer.fromFunction(WebhookPublisher(_, _)) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 0521502da6..c600128743 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -9,7 +9,7 @@ import io.iohk.atala.castor.controller.{DIDControllerImpl, DIDRegistrarControlle import io.iohk.atala.castor.core.service.DIDServiceImpl import io.iohk.atala.castor.core.util.DIDOperationValidator import io.iohk.atala.connect.controller.ConnectionControllerImpl -import io.iohk.atala.connect.core.service.ConnectionServiceImpl +import io.iohk.atala.connect.core.service.{ConnectionServiceImpl, ConnectionServiceWithEventNotificationImpl} import io.iohk.atala.connect.sql.repository.{JdbcConnectionRepository, Migrations as ConnectMigrations} import io.iohk.atala.event.notification.{Event, EventNotificationServiceInMemoryImpl} import io.iohk.atala.issue.controller.IssueControllerImpl @@ -115,7 +115,7 @@ object MainApp extends ZIOAppDefault { DIDResolver.layer, HttpURIDereferencerImpl.layer, // service - ConnectionServiceImpl.layer, + ConnectionServiceWithEventNotificationImpl.layer, CredentialSchemaServiceImpl.layer, CredentialServiceWithEventNotificationImpl.layer, DIDServiceImpl.layer, From 1bdb9ac534a412466d46eb294aad70e37638b725 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Thu, 29 Jun 2023 15:57:17 +0200 Subject: [PATCH 21/56] chore(pollux): use RecordIdNotFound in error channel instead of an Option return value, in presentation service methods --- .../core/service/PresentationService.scala | 32 ++++----- .../service/PresentationServiceImpl.scala | 65 ++++++++++++------- .../service/PresentationServiceSpec.scala | 50 +++++++------- .../PresentProofControllerImpl.scala | 6 +- 4 files changed, 85 insertions(+), 68 deletions(-) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala index 27c55acb09..6796f4404e 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala @@ -64,40 +64,40 @@ trait PresentationService { def acceptRequestPresentation( recordId: DidCommID, crecentialsToUse: Seq[String] - ): IO[PresentationError, Option[PresentationRecord]] + ): IO[PresentationError, PresentationRecord] - def rejectRequestPresentation(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def rejectRequestPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] - def receiveProposePresentation(request: ProposePresentation): IO[PresentationError, Option[PresentationRecord]] + def receiveProposePresentation(request: ProposePresentation): IO[PresentationError, PresentationRecord] - def acceptProposePresentation(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def acceptProposePresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] - def receivePresentation(presentation: Presentation): IO[PresentationError, Option[PresentationRecord]] + def receivePresentation(presentation: Presentation): IO[PresentationError, PresentationRecord] - def acceptPresentation(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def acceptPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] - def rejectPresentation(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def rejectPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] - def markRequestPresentationSent(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def markRequestPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] - def markRequestPresentationRejected(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def markRequestPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] - def markProposePresentationSent(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def markProposePresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] - def markPresentationSent(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def markPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] def markPresentationGenerated( recordId: DidCommID, presentation: Presentation - ): IO[PresentationError, Option[PresentationRecord]] + ): IO[PresentationError, PresentationRecord] - def markPresentationVerified(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def markPresentationVerified(recordId: DidCommID): IO[PresentationError, PresentationRecord] - def markPresentationRejected(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def markPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] - def markPresentationAccepted(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def markPresentationAccepted(recordId: DidCommID): IO[PresentationError, PresentationRecord] - def markPresentationVerificationFailed(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] + def markPresentationVerificationFailed(recordId: DidCommID): IO[PresentationError, PresentationRecord] def reportProcessingFailure(recordId: DidCommID, failReason: Option[String]): IO[PresentationError, Unit] diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceImpl.scala index 008b62fee3..500052428d 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceImpl.scala @@ -37,7 +37,7 @@ private class PresentationServiceImpl( override def markPresentationGenerated( recordId: DidCommID, presentation: Presentation - ): IO[PresentationError, Option[PresentationRecord]] = { + ): IO[PresentationError, PresentationRecord] = { for { record <- getRecordWithState(recordId, ProtocolState.PresentationPending) count <- presentationRepository @@ -49,7 +49,10 @@ private class PresentationServiceImpl( record <- presentationRepository .getPresentationRecord(recordId) .mapError(RepositoryError.apply) - + .flatMap { + case None => ZIO.fail(RecordIdNotFound(record.id)) + case Some(value) => ZIO.succeed(value) + } } yield record } @@ -66,7 +69,6 @@ private class PresentationServiceImpl( record <- ZIO .fromOption(maybeRecord) .mapError(_ => RecordIdNotFound(recordId)) - _ <- ZIO.log(record.toString()) credentialsToUse <- ZIO .fromOption(record.credentialsToUse) .mapError(_ => InvalidFlowStateError(s"No request found for this record: $recordId")) @@ -112,10 +114,10 @@ private class PresentationServiceImpl( } yield record } - override def rejectRequestPresentation(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = { + override def rejectRequestPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = { markRequestPresentationRejected(recordId) } - def rejectPresentation(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = { + def rejectPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = { markPresentationRejected(recordId) } @@ -284,7 +286,7 @@ private class PresentationServiceImpl( def acceptRequestPresentation( recordId: DidCommID, credentialsToUse: Seq[String] - ): IO[PresentationError, Option[PresentationRecord]] = { + ): IO[PresentationError, PresentationRecord] = { for { record <- getRecordWithState(recordId, ProtocolState.RequestReceived) @@ -318,10 +320,14 @@ private class PresentationServiceImpl( record <- presentationRepository .getPresentationRecord(recordId) .mapError(RepositoryError.apply) + .flatMap { + case None => ZIO.fail(RecordIdNotFound(record.id)) + case Some(value) => ZIO.succeed(value) + } } yield record } - override def acceptPresentation(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = { + override def acceptPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = { for { maybeRecord <- presentationRepository .getPresentationRecord(recordId) @@ -329,8 +335,7 @@ private class PresentationServiceImpl( record <- ZIO .fromOption(maybeRecord) .mapError(_ => RecordIdNotFound(recordId)) - _ <- ZIO.log(record.toString()) - presentationRequest <- ZIO + _ <- ZIO .fromOption(record.presentationData) .mapError(_ => InvalidFlowStateError(s"No request found for this record: $recordId")) recordUpdated <- markPresentationAccepted(record.id) @@ -338,7 +343,7 @@ private class PresentationServiceImpl( } override def receivePresentation( presentation: Presentation - ): IO[PresentationError, Option[PresentationRecord]] = { + ): IO[PresentationError, PresentationRecord] = { for { record <- getRecordFromThreadId(presentation.thid) _ <- presentationRepository @@ -351,10 +356,14 @@ private class PresentationServiceImpl( record <- presentationRepository .getPresentationRecord(record.id) .mapError(RepositoryError.apply) + .flatMap { + case None => ZIO.fail(RecordIdNotFound(record.id)) + case Some(value) => ZIO.succeed(value) + } } yield record } - override def acceptProposePresentation(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = { + override def acceptProposePresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = { for { maybeRecord <- presentationRepository .getPresentationRecord(recordId) @@ -376,12 +385,16 @@ private class PresentationServiceImpl( record <- presentationRepository .getPresentationRecord(record.id) .mapError(RepositoryError.apply) + .flatMap { + case None => ZIO.fail(RecordIdNotFound(record.id)) + case Some(value) => ZIO.succeed(value) + } } yield record } override def receiveProposePresentation( proposePresentation: ProposePresentation - ): IO[PresentationError, Option[PresentationRecord]] = { + ): IO[PresentationError, PresentationRecord] = { for { record <- getRecordFromThreadId(proposePresentation.thid) _ <- presentationRepository @@ -394,6 +407,10 @@ private class PresentationServiceImpl( record <- presentationRepository .getPresentationRecord(record.id) .mapError(RepositoryError.apply) + .flatMap { + case None => ZIO.fail(RecordIdNotFound(record.id)) + case Some(value) => ZIO.succeed(value) + } } yield record } @@ -415,48 +432,48 @@ private class PresentationServiceImpl( } yield record } - override def markRequestPresentationSent(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = + override def markRequestPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = updatePresentationRecordProtocolState( recordId, PresentationRecord.ProtocolState.RequestPending, PresentationRecord.ProtocolState.RequestSent ) - override def markProposePresentationSent(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = + override def markProposePresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = updatePresentationRecordProtocolState( recordId, PresentationRecord.ProtocolState.ProposalPending, PresentationRecord.ProtocolState.ProposalSent ) - override def markPresentationVerified(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = + override def markPresentationVerified(recordId: DidCommID): IO[PresentationError, PresentationRecord] = updatePresentationRecordProtocolState( recordId, PresentationRecord.ProtocolState.PresentationReceived, PresentationRecord.ProtocolState.PresentationVerified ) - override def markPresentationAccepted(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = + override def markPresentationAccepted(recordId: DidCommID): IO[PresentationError, PresentationRecord] = updatePresentationRecordProtocolState( recordId, PresentationRecord.ProtocolState.PresentationVerified, PresentationRecord.ProtocolState.PresentationAccepted ) - override def markPresentationSent(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = + override def markPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = updatePresentationRecordProtocolState( recordId, PresentationRecord.ProtocolState.PresentationGenerated, PresentationRecord.ProtocolState.PresentationSent ) - override def markPresentationRejected(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = + override def markPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = updatePresentationRecordProtocolState( recordId, - PresentationRecord.ProtocolState.PresentationReceived, + PresentationRecord.ProtocolState.PresentationVerified, PresentationRecord.ProtocolState.PresentationRejected ) - override def markRequestPresentationRejected(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = + override def markRequestPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = updatePresentationRecordProtocolState( recordId, PresentationRecord.ProtocolState.RequestReceived, @@ -465,7 +482,7 @@ private class PresentationServiceImpl( override def markPresentationVerificationFailed( recordId: DidCommID - ): IO[PresentationError, Option[PresentationRecord]] = + ): IO[PresentationError, PresentationRecord] = updatePresentationRecordProtocolState( recordId, PresentationRecord.ProtocolState.PresentationReceived, @@ -582,7 +599,7 @@ private class PresentationServiceImpl( id: DidCommID, from: PresentationRecord.ProtocolState, to: PresentationRecord.ProtocolState - ): IO[PresentationError, Option[PresentationRecord]] = { + ): IO[PresentationError, PresentationRecord] = { for { outcome <- presentationRepository .updatePresentationRecordProtocolState(id, from, to) @@ -594,6 +611,10 @@ private class PresentationServiceImpl( record <- presentationRepository .getPresentationRecord(id) .mapError(RepositoryError.apply) + .flatMap { + case None => ZIO.fail(RecordIdNotFound(id)) + case Some(value) => ZIO.succeed(value) + } } yield record } diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala index 01b5490614..bf754479b1 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala @@ -211,7 +211,7 @@ object PresentationServiceSpec extends ZIOSpecDefault { record <- svc.markRequestPresentationSent(record.id) } yield { - assertTrue(record.get.protocolState == PresentationRecord.ProtocolState.RequestSent) + assertTrue(record.protocolState == PresentationRecord.ProtocolState.RequestSent) } }, test("markRequestPresentationRejected returns updated PresentationRecord") { @@ -228,7 +228,7 @@ object PresentationServiceSpec extends ZIOSpecDefault { record <- svc.markRequestPresentationRejected(record.id) } yield { - assertTrue(record.get.protocolState == PresentationRecord.ProtocolState.RequestRejected) + assertTrue(record.protocolState == PresentationRecord.ProtocolState.RequestRejected) }) }, test("receiveRequestPresentation updates the RequestPresentation in PresentatinRecord") { @@ -284,9 +284,9 @@ object PresentationServiceSpec extends ZIOSpecDefault { updateRecord <- svc.acceptRequestPresentation(aRecord.id, credentialsToUse) } yield { - assertTrue(updateRecord.get.connectionId == connectionId) - assertTrue(updateRecord.get.requestPresentationData == Some(requestPresentation)) - assertTrue(updateRecord.get.credentialsToUse.contains(credentialsToUse)) + assertTrue(updateRecord.connectionId == connectionId) + assertTrue(updateRecord.requestPresentationData == Some(requestPresentation)) + assertTrue(updateRecord.credentialsToUse.contains(credentialsToUse)) } ) @@ -300,9 +300,9 @@ object PresentationServiceSpec extends ZIOSpecDefault { updateRecord <- svc.rejectRequestPresentation(aRecord.id) } yield { - assertTrue(updateRecord.get.connectionId == connectionId) - assertTrue(updateRecord.get.requestPresentationData == Some(requestPresentation)) - assertTrue(updateRecord.get.protocolState == PresentationRecord.ProtocolState.RequestRejected) + assertTrue(updateRecord.connectionId == connectionId) + assertTrue(updateRecord.requestPresentationData == Some(requestPresentation)) + assertTrue(updateRecord.protocolState == PresentationRecord.ProtocolState.RequestRejected) } ) }, @@ -320,7 +320,7 @@ object PresentationServiceSpec extends ZIOSpecDefault { record <- svc.markPresentationSent(record.id) } yield { - assertTrue(record.get.protocolState == PresentationRecord.ProtocolState.PresentationSent) + assertTrue(record.protocolState == PresentationRecord.ProtocolState.PresentationSent) }) }, test("receivePresentation updates the PresentatinRecord") { @@ -332,8 +332,8 @@ object PresentationServiceSpec extends ZIOSpecDefault { aRecordReceived <- svc.receivePresentation(p) } yield { - assertTrue(aRecordReceived.get.id == aRecord.id) - assertTrue(aRecordReceived.get.presentationData == Some(p)) + assertTrue(aRecordReceived.id == aRecord.id) + assertTrue(aRecordReceived.presentationData == Some(p)) } ) }, @@ -353,8 +353,8 @@ object PresentationServiceSpec extends ZIOSpecDefault { aRecordAccept <- svc.acceptPresentation(aRecord.id) } yield { - assertTrue(aRecordReceived.get.id == aRecord.id) - assertTrue(aRecordReceived.get.presentationData == Some(p)) + assertTrue(aRecordReceived.id == aRecord.id) + assertTrue(aRecordReceived.presentationData == Some(p)) } ) }, @@ -368,9 +368,9 @@ object PresentationServiceSpec extends ZIOSpecDefault { aRecordAccept <- svc.markPresentationRejected(aRecord.id) } yield { - assertTrue(aRecordAccept.get.id == aRecord.id) - assertTrue(aRecordAccept.get.presentationData == Some(p)) - assertTrue(aRecordAccept.get.protocolState == PresentationRecord.ProtocolState.PresentationRejected) + assertTrue(aRecordAccept.id == aRecord.id) + assertTrue(aRecordAccept.presentationData == Some(p)) + assertTrue(aRecordAccept.protocolState == PresentationRecord.ProtocolState.PresentationRejected) } ) }, @@ -384,9 +384,9 @@ object PresentationServiceSpec extends ZIOSpecDefault { aRecordAccept <- svc.rejectPresentation(aRecord.id) } yield { - assertTrue(aRecordAccept.get.id == aRecord.id) - assertTrue(aRecordAccept.get.presentationData == Some(p)) - assertTrue(aRecordAccept.get.protocolState == PresentationRecord.ProtocolState.PresentationRejected) + assertTrue(aRecordAccept.id == aRecord.id) + assertTrue(aRecordAccept.presentationData == Some(p)) + assertTrue(aRecordAccept.protocolState == PresentationRecord.ProtocolState.PresentationRejected) } ) }, @@ -405,7 +405,7 @@ object PresentationServiceSpec extends ZIOSpecDefault { record <- svc.markPresentationGenerated(record.id, p) } yield { - assertTrue(record.get.protocolState == PresentationRecord.ProtocolState.PresentationGenerated) + assertTrue(record.protocolState == PresentationRecord.ProtocolState.PresentationGenerated) }) }, test("markProposePresentationSent returns updated PresentationRecord") { @@ -422,7 +422,7 @@ object PresentationServiceSpec extends ZIOSpecDefault { record <- svc.markProposePresentationSent(record.id) } yield { - assertTrue(record.get.protocolState == PresentationRecord.ProtocolState.ProposalSent) + assertTrue(record.protocolState == PresentationRecord.ProtocolState.ProposalSent) }) }, test("receiveProposePresentation updates the PresentatinRecord") { @@ -434,8 +434,8 @@ object PresentationServiceSpec extends ZIOSpecDefault { aRecordReceived <- svc.receiveProposePresentation(p) } yield { - assertTrue(aRecordReceived.get.id == aRecord.id) - assertTrue(aRecordReceived.get.proposePresentationData == Some(p)) + assertTrue(aRecordReceived.id == aRecord.id) + assertTrue(aRecordReceived.proposePresentationData == Some(p)) } ) }, @@ -455,8 +455,8 @@ object PresentationServiceSpec extends ZIOSpecDefault { aRecordAccept <- svc.acceptProposePresentation(aRecord.id) } yield { - assertTrue(aRecordReceived.get.id == aRecord.id) - assertTrue(aRecordReceived.get.proposePresentationData == Some(p)) + assertTrue(aRecordReceived.id == aRecord.id) + assertTrue(aRecordReceived.proposePresentationData == Some(p)) } ) }, diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala index b6b460a800..db451c8211 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala @@ -84,7 +84,7 @@ class PresentProofControllerImpl( ): IO[ErrorResponse, PresentationStatus] = { val result: ZIO[Any, ErrorResponse | PresentationError, PresentationStatus] = for { didCommId <- ZIO.succeed(DidCommID(id.toString)) - maybeRecord <- requestPresentationAction.action match { + record <- requestPresentationAction.action match { case "request-accept" => presentationService.acceptRequestPresentation( recordId = didCommId, @@ -102,10 +102,6 @@ class PresentProofControllerImpl( ) ) } - record <- ZIO - .fromOption(maybeRecord) - .mapError(_ => ErrorResponse.notFound(detail = Some(s"Presentation record not found: $id"))) - } yield PresentationStatus.fromDomain(record) result.mapError { From d9c9beba98d3545226ea2dbd04c615fa57d8c92c Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Thu, 29 Jun 2023 15:58:03 +0200 Subject: [PATCH 22/56] feat(prism-agent): implement event notifiation sending in presentation protocol --- ...tionServiceWithEventNotificationImpl.scala | 101 ++++++++++++++++++ .../io/iohk/atala/agent/server/Main.scala | 16 ++- 2 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala new file mode 100644 index 0000000000..878934ae82 --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala @@ -0,0 +1,101 @@ +package io.iohk.atala.pollux.core.service + +import io.iohk.atala.event.notification.EventNotificationServiceError.EncoderError +import io.iohk.atala.event.notification.{Event, EventEncoder, EventNotificationService} +import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.mercury.protocol.presentproof.{Presentation, ProofType, RequestPresentation} +import io.iohk.atala.pollux.core.model.error.PresentationError +import io.iohk.atala.pollux.core.model.presentation.Options +import io.iohk.atala.pollux.core.model.{DidCommID, PresentationRecord} +import io.iohk.atala.pollux.core.repository.{CredentialRepository, PresentationRepository} +import io.iohk.atala.pollux.core.service.PresentationServiceWithEventNotificationImpl.given +import zio.{IO, Task, URLayer, ZIO, ZLayer} + +class PresentationServiceWithEventNotificationImpl( + presentationRepository: PresentationRepository[Task], + credentialRepository: CredentialRepository[Task], + eventNotificationService: EventNotificationService +) extends PresentationServiceImpl(presentationRepository, credentialRepository) { + override def createPresentationRecord( + pairwiseVerifierDID: DidId, + pairwiseProverDID: DidId, + thid: DidCommID, + connectionId: Option[String], + proofTypes: Seq[ProofType], + options: Option[Options] + ): IO[PresentationError, PresentationRecord] = + notifyOnSuccess( + super.createPresentationRecord(pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, proofTypes, options) + ) + + override def markRequestPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(super.markRequestPresentationSent(recordId)) + + override def receiveRequestPresentation( + connectionId: Option[_root_.java.lang.String], + request: RequestPresentation + ): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(super.receiveRequestPresentation(connectionId, request)) + + override def markRequestPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(super.markRequestPresentationRejected(recordId)) + + override def acceptRequestPresentation( + recordId: DidCommID, + credentialsToUse: Seq[_root_.java.lang.String] + ): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(super.acceptRequestPresentation(recordId, credentialsToUse)) + + override def markPresentationGenerated( + recordId: DidCommID, + presentation: Presentation + ): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(super.markPresentationGenerated(recordId, presentation)) + + override def markPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(super.markPresentationSent(recordId)) + + override def receivePresentation(presentation: Presentation): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(super.receivePresentation(presentation)) + + override def markPresentationVerified(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(super.markPresentationVerified(recordId)) + + override def markPresentationVerificationFailed( + recordId: DidCommID + ): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(super.markPresentationVerificationFailed(recordId)) + + override def markPresentationAccepted(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(super.markPresentationAccepted(recordId)) + + override def markPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(super.markPresentationRejected(recordId)) + + private[this] def notifyOnSuccess(effect: IO[PresentationError, PresentationRecord]) = + for { + record <- effect + _ <- notify(record) + } yield record + + private[this] def notify(record: PresentationRecord) = { + val result = for { + producer <- eventNotificationService.producer[PresentationRecord]("Presentation") + _ <- producer.send(Event(record)) + } yield () + result.catchAll(e => ZIO.logError(s"Notification service error: $e")) + } +} + +object PresentationServiceWithEventNotificationImpl { + + given EventEncoder[PresentationRecord] = (data: PresentationRecord) => + ZIO.attempt(data.asInstanceOf[Any]).mapError(t => EncoderError(t.getMessage)) + + val layer: URLayer[ + PresentationRepository[Task] & CredentialRepository[Task] & EventNotificationService, + PresentationService + ] = + ZLayer.fromFunction(PresentationServiceWithEventNotificationImpl(_, _, _)) + +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index c600128743..920fffeed0 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -15,8 +15,18 @@ import io.iohk.atala.event.notification.{Event, EventNotificationServiceInMemory import io.iohk.atala.issue.controller.IssueControllerImpl import io.iohk.atala.mercury.* import io.iohk.atala.pollux.core.service.* -import io.iohk.atala.pollux.credentialschema.controller.{CredentialSchemaController, CredentialSchemaControllerImpl, VerificationPolicyControllerImpl} -import io.iohk.atala.pollux.sql.repository.{JdbcCredentialRepository, JdbcCredentialSchemaRepository, JdbcPresentationRepository, JdbcVerificationPolicyRepository, Migrations as PolluxMigrations} +import io.iohk.atala.pollux.credentialschema.controller.{ + CredentialSchemaController, + CredentialSchemaControllerImpl, + VerificationPolicyControllerImpl +} +import io.iohk.atala.pollux.sql.repository.{ + JdbcCredentialRepository, + JdbcCredentialSchemaRepository, + JdbcPresentationRepository, + JdbcVerificationPolicyRepository, + Migrations as PolluxMigrations +} import io.iohk.atala.presentproof.controller.PresentProofControllerImpl import io.iohk.atala.resolvers.DIDResolver import io.iohk.atala.system.controller.SystemControllerImpl @@ -120,7 +130,7 @@ object MainApp extends ZIOAppDefault { CredentialServiceWithEventNotificationImpl.layer, DIDServiceImpl.layer, ManagedDIDServiceImpl.layer, - PresentationServiceImpl.layer, + PresentationServiceWithEventNotificationImpl.layer, VerificationPolicyServiceImpl.layer, // grpc GrpcModule.irisStubLayer, From 4ec0384498c0364460d758490ddad4a00210d3f5 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Thu, 29 Jun 2023 16:36:03 +0200 Subject: [PATCH 23/56] test(pollux): fix unit test related to presentation rejection --- .../service/PresentationServiceSpec.scala | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala index bf754479b1..6ad2fe048b 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala @@ -364,13 +364,18 @@ object PresentationServiceSpec extends ZIOSpecDefault { svc <- ZIO.service[PresentationService] aRecord <- svc.createRecord() p = presentation(aRecord.thid.value) - aRecordReceived <- svc.receivePresentation(p) - aRecordAccept <- svc.markPresentationRejected(aRecord.id) - + _ <- svc.receivePresentation(p) + repo <- ZIO.service[PresentationRepository[Task]] + _ <- repo.updatePresentationRecordProtocolState( + aRecord.id, + PresentationRecord.ProtocolState.PresentationReceived, + PresentationRecord.ProtocolState.PresentationVerified + ) + aRecordReject <- svc.markPresentationRejected(aRecord.id) } yield { - assertTrue(aRecordAccept.id == aRecord.id) - assertTrue(aRecordAccept.presentationData == Some(p)) - assertTrue(aRecordAccept.protocolState == PresentationRecord.ProtocolState.PresentationRejected) + assertTrue(aRecordReject.id == aRecord.id) + assertTrue(aRecordReject.presentationData == Some(p)) + assertTrue(aRecordReject.protocolState == PresentationRecord.ProtocolState.PresentationRejected) } ) }, @@ -381,12 +386,17 @@ object PresentationServiceSpec extends ZIOSpecDefault { aRecord <- svc.createRecord() p = presentation(aRecord.thid.value) aRecordReceived <- svc.receivePresentation(p) - aRecordAccept <- svc.rejectPresentation(aRecord.id) - + repo <- ZIO.service[PresentationRepository[Task]] + _ <- repo.updatePresentationRecordProtocolState( + aRecord.id, + PresentationRecord.ProtocolState.PresentationReceived, + PresentationRecord.ProtocolState.PresentationVerified + ) + aRecordReject <- svc.rejectPresentation(aRecord.id) } yield { - assertTrue(aRecordAccept.id == aRecord.id) - assertTrue(aRecordAccept.presentationData == Some(p)) - assertTrue(aRecordAccept.protocolState == PresentationRecord.ProtocolState.PresentationRejected) + assertTrue(aRecordReject.id == aRecord.id) + assertTrue(aRecordReject.presentationData == Some(p)) + assertTrue(aRecordReject.protocolState == PresentationRecord.ProtocolState.PresentationRejected) } ) }, From 6f49a61564ac30c0db43b214071aba593d7cba9c Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Thu, 29 Jun 2023 16:46:06 +0200 Subject: [PATCH 24/56] chore(prism-agent): get rid of event encoder/decoder (useless for now) --- ...ConnectionServiceWithEventNotificationImpl.scala | 5 +---- .../notification/EventNotificationService.scala | 10 ++-------- .../EventNotificationServiceInMemoryImpl.scala | 13 ++++++------- ...CredentialServiceWithEventNotificationImpl.scala | 4 ---- ...esentationServiceWithEventNotificationImpl.scala | 6 +----- .../atala/agent/notification/WebhookPublisher.scala | 11 +---------- 6 files changed, 11 insertions(+), 38 deletions(-) diff --git a/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImpl.scala b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImpl.scala index 4b34e9a02a..c136a90bd0 100644 --- a/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImpl.scala +++ b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImpl.scala @@ -5,7 +5,7 @@ import io.iohk.atala.connect.core.model.error.ConnectionServiceError import io.iohk.atala.connect.core.repository.ConnectionRepository import io.iohk.atala.connect.core.service.ConnectionServiceWithEventNotificationImpl.given import io.iohk.atala.event.notification.EventNotificationServiceError.EncoderError -import io.iohk.atala.event.notification.{Event, EventEncoder, EventNotificationService} +import io.iohk.atala.event.notification.{Event, EventNotificationService} import io.iohk.atala.mercury.model.DidId import io.iohk.atala.mercury.protocol.connection.{ConnectionRequest, ConnectionResponse} import zio.{IO, Task, URLayer, ZIO, ZLayer} @@ -62,9 +62,6 @@ class ConnectionServiceWithEventNotificationImpl( } object ConnectionServiceWithEventNotificationImpl { - given EventEncoder[ConnectionRecord] = (data: ConnectionRecord) => - ZIO.attempt(data.asInstanceOf[Any]).mapError(t => EncoderError(t.getMessage)) - val layer: URLayer[ConnectionRepository[Task] with EventNotificationService, ConnectionService] = ZLayer.fromFunction(ConnectionServiceWithEventNotificationImpl(_, _)) } diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationService.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationService.scala index 61066d3955..07f5bbb8ff 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationService.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationService.scala @@ -4,11 +4,5 @@ import io.iohk.atala.event.notification.EventNotificationServiceError.{DecoderEr import zio.IO trait EventNotificationService: - def consumer[A](topic: String)(using decoder: EventDecoder[A]): IO[EventNotificationServiceError, EventConsumer[A]] - def producer[A](topic: String)(using encoder: EventEncoder[A]): IO[EventNotificationServiceError, EventProducer[A]] - -trait EventEncoder[A]: - def encode(data: A): IO[EncoderError, Any] - -trait EventDecoder[A]: - def decode(data: Any): IO[DecoderError, A] + def consumer[A](topic: String): IO[EventNotificationServiceError, EventConsumer[A]] + def producer[A](topic: String): IO[EventNotificationServiceError, EventProducer[A]] diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala index 439bdc7651..208c99c8ae 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala @@ -6,9 +6,9 @@ import zio.{IO, Queue, ULayer, URLayer, ZIO, ZLayer} import scala.collection.mutable class EventNotificationServiceInMemoryImpl extends EventNotificationService: - private[this] val queueMap = mutable.Map.empty[String, Queue[Event[Any]]] + private[this] val queueMap = mutable.Map.empty[String, Queue[Event[_]]] - private[this] def getOrCreateQueue(topic: String): IO[EventNotificationServiceError, Queue[Event[Any]]] = { + private[this] def getOrCreateQueue(topic: String): IO[EventNotificationServiceError, Queue[Event[_]]] = { for { maybeQueue <- ZIO.succeed(queueMap.get(topic)) queue <- maybeQueue match @@ -20,23 +20,22 @@ class EventNotificationServiceInMemoryImpl extends EventNotificationService: override def consumer[A]( topic: String - )(using decoder: EventDecoder[A]): IO[EventNotificationServiceError, EventConsumer[A]] = + ): IO[EventNotificationServiceError, EventConsumer[A]] = ZIO.succeed(new EventConsumer[A] { override def poll(count: Int): IO[EventNotificationServiceError, Seq[Event[A]]] = for { queue <- getOrCreateQueue(topic) events <- queue.takeBetween(1, count) - decodedEvents <- ZIO.foreach(events)(e => decoder.decode(e.data).map(d => Event(d))) + decodedEvents <- ZIO.foreach(events)(e => ZIO.succeed(e.asInstanceOf[Event[A]])) } yield decodedEvents }) override def producer[A]( topic: String - )(using encoder: EventEncoder[A]): IO[EventNotificationServiceError, EventProducer[A]] = + ): IO[EventNotificationServiceError, EventProducer[A]] = ZIO.succeed(new EventProducer[A] { override def send(event: Event[A]): IO[EventNotificationServiceError, Unit] = for { queue <- getOrCreateQueue(topic) - encodedEvent <- encoder.encode(event.data).map(e => Event(e)) - succeeded <- queue.offer(encodedEvent) + succeeded <- queue.offer(event) _ <- if (succeeded) ZIO.unit else ZIO.fail(EventSendingFailed("Queue.offer returned 'false'")) } yield () }) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala index 69bc3e3a69..9e9092f72f 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala @@ -102,10 +102,6 @@ class CredentialServiceWithEventNotificationImpl( } object CredentialServiceWithEventNotificationImpl { - - given EventEncoder[IssueCredentialRecord] = (data: IssueCredentialRecord) => - ZIO.attempt(data.asInstanceOf[Any]).mapError(t => EncoderError(t.getMessage)) - val layer: URLayer[ IrisServiceStub with CredentialRepository[Task] with DidResolver with URIDereferencer with EventNotificationService, CredentialServiceWithEventNotificationImpl diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala index 878934ae82..57648efde8 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala @@ -1,7 +1,7 @@ package io.iohk.atala.pollux.core.service import io.iohk.atala.event.notification.EventNotificationServiceError.EncoderError -import io.iohk.atala.event.notification.{Event, EventEncoder, EventNotificationService} +import io.iohk.atala.event.notification.{Event, EventNotificationService} import io.iohk.atala.mercury.model.DidId import io.iohk.atala.mercury.protocol.presentproof.{Presentation, ProofType, RequestPresentation} import io.iohk.atala.pollux.core.model.error.PresentationError @@ -88,10 +88,6 @@ class PresentationServiceWithEventNotificationImpl( } object PresentationServiceWithEventNotificationImpl { - - given EventEncoder[PresentationRecord] = (data: PresentationRecord) => - ZIO.attempt(data.asInstanceOf[Any]).mapError(t => EncoderError(t.getMessage)) - val layer: URLayer[ PresentationRepository[Task] & CredentialRepository[Task] & EventNotificationService, PresentationService diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index 2e1cca2fd7..c85f1c9b9d 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -4,7 +4,7 @@ import io.iohk.atala.agent.notification.WebhookPublisherError.{InvalidWebhookURL import io.iohk.atala.agent.server.config.{AppConfig, WebhookPublisherConfig} import io.iohk.atala.connect.core.model.ConnectionRecord import io.iohk.atala.event.notification.EventNotificationServiceError.DecoderError -import io.iohk.atala.event.notification.{Event, EventConsumer, EventDecoder, EventNotificationService} +import io.iohk.atala.event.notification.{Event, EventConsumer, EventNotificationService} import io.iohk.atala.pollux.core.model.{IssueCredentialRecord, PresentationRecord} import zio.* import zio.http.* @@ -84,15 +84,6 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat } object WebhookPublisher { - given EventDecoder[ConnectionRecord] = (data: Any) => - ZIO.attempt(data.asInstanceOf[ConnectionRecord]).mapError(t => DecoderError(t.getMessage)) - - given EventDecoder[IssueCredentialRecord] = (data: Any) => - ZIO.attempt(data.asInstanceOf[IssueCredentialRecord]).mapError(t => DecoderError(t.getMessage)) - - given EventDecoder[PresentationRecord] = (data: Any) => - ZIO.attempt(data.asInstanceOf[PresentationRecord]).mapError(t => DecoderError(t.getMessage)) - val layer: URLayer[AppConfig & EventNotificationService, WebhookPublisher] = ZLayer.fromFunction(WebhookPublisher(_, _)) } From a8719738220214d92565e0e1025fabbc11da7436 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 30 Jun 2023 09:40:08 +0200 Subject: [PATCH 25/56] chore(pollux): rename event notification service implementation --- ...emoryImpl.scala => EventNotificationServiceImpl.scala} | 8 ++++---- .../src/main/scala/io/iohk/atala/agent/server/Main.scala | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) rename event-notification/src/main/scala/io/iohk/atala/event/notification/{EventNotificationServiceInMemoryImpl.scala => EventNotificationServiceImpl.scala} (86%) diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala similarity index 86% rename from event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala rename to event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala index 208c99c8ae..3ccf47b93d 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceInMemoryImpl.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala @@ -5,7 +5,7 @@ import zio.{IO, Queue, ULayer, URLayer, ZIO, ZLayer} import scala.collection.mutable -class EventNotificationServiceInMemoryImpl extends EventNotificationService: +class EventNotificationServiceImpl extends EventNotificationService: private[this] val queueMap = mutable.Map.empty[String, Queue[Event[_]]] private[this] def getOrCreateQueue(topic: String): IO[EventNotificationServiceError, Queue[Event[_]]] = { @@ -40,7 +40,7 @@ class EventNotificationServiceInMemoryImpl extends EventNotificationService: } yield () }) -object EventNotificationServiceInMemoryImpl { - val layer: ULayer[EventNotificationServiceInMemoryImpl] = - ZLayer.succeed(new EventNotificationServiceInMemoryImpl()) +object EventNotificationServiceImpl { + val layer: ULayer[EventNotificationServiceImpl] = + ZLayer.succeed(new EventNotificationServiceImpl()) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 920fffeed0..5b07ccf24b 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -11,7 +11,7 @@ import io.iohk.atala.castor.core.util.DIDOperationValidator import io.iohk.atala.connect.controller.ConnectionControllerImpl import io.iohk.atala.connect.core.service.{ConnectionServiceImpl, ConnectionServiceWithEventNotificationImpl} import io.iohk.atala.connect.sql.repository.{JdbcConnectionRepository, Migrations as ConnectMigrations} -import io.iohk.atala.event.notification.{Event, EventNotificationServiceInMemoryImpl} +import io.iohk.atala.event.notification.{Event, EventNotificationServiceImpl} import io.iohk.atala.issue.controller.IssueControllerImpl import io.iohk.atala.mercury.* import io.iohk.atala.pollux.core.service.* @@ -144,7 +144,7 @@ object MainApp extends ZIOAppDefault { RepoModule.polluxTransactorLayer >>> JdbcPresentationRepository.layer, RepoModule.polluxTransactorLayer >>> JdbcVerificationPolicyRepository.layer, // event notification service - EventNotificationServiceInMemoryImpl.layer, + EventNotificationServiceImpl.layer, // HTTP client Scope.default >>> Client.default, ) From 4b0318795b73d15b0ae15ab40915b58a3ec99977 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 30 Jun 2023 13:17:49 +0200 Subject: [PATCH 26/56] test(prism-agent): make capacity of event queue configurable and add unit tests --- build.sbt | 6 +- .../EventNotificationServiceImpl.scala | 8 +- .../EventNotificationServiceImplSpec.scala | 98 +++++++++++++++++++ .../io/iohk/atala/agent/server/Main.scala | 2 +- 4 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala diff --git a/build.sbt b/build.sbt index 335fdb821c..5cd6b6b298 100644 --- a/build.sbt +++ b/build.sbt @@ -276,7 +276,11 @@ lazy val D_Pollux_VC_JWT = new { lazy val D_EventNotification = new { val zio = "dev.zio" %% "zio" % V.zio - val zioDependencies: Seq[ModuleID] = Seq(zio) + val zioTest = "dev.zio" %% "zio-test" % V.zio % Test + val zioTestSbt = "dev.zio" %% "zio-test-sbt" % V.zio % Test + val zioTestMagnolia = "dev.zio" %% "zio-test-magnolia" % V.zio % Test + + val zioDependencies: Seq[ModuleID] = Seq(zio, zioTest, zioTestSbt, zioTestMagnolia) val baseDependencies: Seq[ModuleID] = zioDependencies } diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala index 3ccf47b93d..f21987a260 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala @@ -5,7 +5,7 @@ import zio.{IO, Queue, ULayer, URLayer, ZIO, ZLayer} import scala.collection.mutable -class EventNotificationServiceImpl extends EventNotificationService: +class EventNotificationServiceImpl(queueCapacity: Int) extends EventNotificationService: private[this] val queueMap = mutable.Map.empty[String, Queue[Event[_]]] private[this] def getOrCreateQueue(topic: String): IO[EventNotificationServiceError, Queue[Event[_]]] = { @@ -13,7 +13,7 @@ class EventNotificationServiceImpl extends EventNotificationService: maybeQueue <- ZIO.succeed(queueMap.get(topic)) queue <- maybeQueue match case Some(value) => ZIO.succeed(value) - case None => Queue.bounded(500) + case None => Queue.bounded(queueCapacity) _ <- ZIO.succeed(queueMap.put(topic, queue)) } yield queue } @@ -41,6 +41,6 @@ class EventNotificationServiceImpl extends EventNotificationService: }) object EventNotificationServiceImpl { - val layer: ULayer[EventNotificationServiceImpl] = - ZLayer.succeed(new EventNotificationServiceImpl()) + val layer: URLayer[Int, EventNotificationServiceImpl] = + ZLayer.fromFunction(new EventNotificationServiceImpl(_)) } diff --git a/event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala b/event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala new file mode 100644 index 0000000000..1752c9de2c --- /dev/null +++ b/event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala @@ -0,0 +1,98 @@ +package io.iohk.atala.event.notification + +import zio.* +import zio.test.* + +object EventNotificationServiceImplSpec extends ZIOSpecDefault { + + private val eventNotificationServiceLayer = ZLayer.succeed(10) >>> EventNotificationServiceImpl.layer + + override def spec: Spec[TestEnvironment with Scope, Any] = { + suite("EventNotificationServiceImpl")( + test("should send events between a producer and a consumer of the same topic") { + for { + svc <- ZIO.service[EventNotificationService] + producer <- svc.producer[String]("TopicA") + consumer <- svc.consumer[String]("TopicA") + _ <- producer.send(Event("event #1")) + _ <- producer.send(Event("event #2")) + events <- consumer.poll(2) + } yield assertTrue(events == Seq(Event("event #1"), Event("event #2"))) + }, + test("should not mix-up events from different topics") { + for { + svc <- ZIO.service[EventNotificationService] + producerA <- svc.producer[String]("TopicA") + consumerA <- svc.consumer[String]("TopicA") + producerB <- svc.producer[String]("TopicB") + consumerB <- svc.consumer[String]("TopicB") + _ <- producerA.send(Event("event #1")) + _ <- producerA.send(Event("event #2")) + _ <- producerB.send(Event("event #3")) + eventsA <- consumerA.poll(5) + eventsB <- consumerB.poll(5) + } yield { + assertTrue(eventsA.size == 2) && + assertTrue(eventsB.size == 1) && + assertTrue(eventsA == Seq(Event("event #1"), Event("event #2"))) && + assertTrue(eventsB == Seq(Event("event #3"))) + } + }, + test("should only deliver the requested messages number to a consumer") { + for { + svc <- ZIO.service[EventNotificationService] + producer <- svc.producer[String]("TopicA") + consumer <- svc.consumer[String]("TopicA") + _ <- producer.send(Event("event #1")) + _ <- producer.send(Event("event #2")) + events <- consumer.poll(1) + } yield assertTrue(events == Seq(Event("event #1"))) + }, + test("should remove consumed messages from the queue") { + for { + svc <- ZIO.service[EventNotificationService] + producer <- svc.producer[String]("TopicA") + consumer <- svc.consumer[String]("TopicA") + _ <- producer.send(Event("event #1")) + _ <- producer.send(Event("event #2")) + _ <- consumer.poll(1) + events <- consumer.poll(1) + } yield assertTrue(events == Seq(Event("event #2"))) + }, + test("should send event even when consumer is created and polling first") { + for { + svc <- ZIO.service[EventNotificationService] + // Consuming in a fiber + consumer <- svc.consumer[String]("TopicA") + consumerFiber <- consumer.poll(1).fork + // Producing in another fiber, after 3 seconds + producer <- svc.producer[String]("TopicA") + producerFiber <- producer.send(Event("event #1")).delay(3.seconds).fork + _ <- TestClock.adjust(3.seconds) + events <- consumerFiber.join + _ <- producerFiber.join + } yield assertTrue(events == Seq(Event("event #1"))) + }, + test("should block on sending new messages when queue is full") { + for { + svc <- ZIO.service[EventNotificationService] + producer <- svc.producer[String]("TopicA") + _ <- ZIO.collectAll((1 to 10).map(i => producer.send(Event(s"event #$i")))) + fiber <- producer.send(Event("One more event")).timeout(5.seconds).fork + _ <- TestClock.adjust(5.seconds) + res <- fiber.join + } yield assertTrue(res.isEmpty) + }, + test("should block on reading new messages when queue is empty") { + for { + svc <- ZIO.service[EventNotificationService] + consumer <- svc.consumer[String]("TopicA") + fiber <- consumer.poll(1).timeout(5.seconds).fork + _ <- TestClock.adjust(5.seconds) + res <- fiber.join + } yield assertTrue(res.isEmpty) + } + ) + }.provideLayer(eventNotificationServiceLayer) + +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 5b07ccf24b..73384c97ef 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -144,7 +144,7 @@ object MainApp extends ZIOAppDefault { RepoModule.polluxTransactorLayer >>> JdbcPresentationRepository.layer, RepoModule.polluxTransactorLayer >>> JdbcVerificationPolicyRepository.layer, // event notification service - EventNotificationServiceImpl.layer, + ZLayer.succeed(500) >>> EventNotificationServiceImpl.layer, // HTTP client Scope.default >>> Client.default, ) From 67954db4e8e943a8f7a74cdec7d60bbdd11ddb04 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 30 Jun 2023 16:17:14 +0200 Subject: [PATCH 27/56] chore(prism-agent): add event notification service dependency to wallet api --- build.sbt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 5cd6b6b298..67cac6e523 100644 --- a/build.sbt +++ b/build.sbt @@ -704,8 +704,11 @@ lazy val prismAgentWalletAPI = project name := "prism-agent-wallet-api", libraryDependencies ++= D_PrismAgent.keyManagementDependencies ) - .dependsOn(agentDidcommx) - .dependsOn(castorCore) + .dependsOn( + agentDidcommx, + castorCore, + eventNotification + ) lazy val prismAgentServer = project .in(file("prism-agent/service/server")) From 7c28764ddf09c158b7a1c84eaa63add121c9a5cb Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 30 Jun 2023 16:21:47 +0200 Subject: [PATCH 28/56] feat(prism-agent): add new 'DIDState' topic and notify of DID published events --- .../io/iohk/atala/agent/server/Main.scala | 18 +-- .../service/ManagedDIDServiceImpl.scala | 23 ++-- ...dDIDServiceWithEventNotificationImpl.scala | 106 ++++++++++++++++++ 3 files changed, 121 insertions(+), 26 deletions(-) create mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 73384c97ef..ec2dd95b5e 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -3,7 +3,7 @@ package io.iohk.atala.agent.server import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton import io.iohk.atala.agent.server.http.ZioHttpClient import io.iohk.atala.agent.server.sql.Migrations as AgentMigrations -import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, ManagedDIDServiceImpl} +import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, ManagedDIDServiceImpl, ManagedDIDServiceWithEventNotificationImpl} import io.iohk.atala.agent.walletapi.sql.JdbcDIDNonSecretStorage import io.iohk.atala.castor.controller.{DIDControllerImpl, DIDRegistrarControllerImpl} import io.iohk.atala.castor.core.service.DIDServiceImpl @@ -15,18 +15,8 @@ import io.iohk.atala.event.notification.{Event, EventNotificationServiceImpl} import io.iohk.atala.issue.controller.IssueControllerImpl import io.iohk.atala.mercury.* import io.iohk.atala.pollux.core.service.* -import io.iohk.atala.pollux.credentialschema.controller.{ - CredentialSchemaController, - CredentialSchemaControllerImpl, - VerificationPolicyControllerImpl -} -import io.iohk.atala.pollux.sql.repository.{ - JdbcCredentialRepository, - JdbcCredentialSchemaRepository, - JdbcPresentationRepository, - JdbcVerificationPolicyRepository, - Migrations as PolluxMigrations -} +import io.iohk.atala.pollux.credentialschema.controller.{CredentialSchemaController, CredentialSchemaControllerImpl, VerificationPolicyControllerImpl} +import io.iohk.atala.pollux.sql.repository.{JdbcCredentialRepository, JdbcCredentialSchemaRepository, JdbcPresentationRepository, JdbcVerificationPolicyRepository, Migrations as PolluxMigrations} import io.iohk.atala.presentproof.controller.PresentProofControllerImpl import io.iohk.atala.resolvers.DIDResolver import io.iohk.atala.system.controller.SystemControllerImpl @@ -129,7 +119,7 @@ object MainApp extends ZIOAppDefault { CredentialSchemaServiceImpl.layer, CredentialServiceWithEventNotificationImpl.layer, DIDServiceImpl.layer, - ManagedDIDServiceImpl.layer, + ManagedDIDServiceWithEventNotificationImpl.layer, PresentationServiceWithEventNotificationImpl.layer, VerificationPolicyServiceImpl.layer, // grpc diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceImpl.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceImpl.scala index f97cc3deb3..b7a2281ea7 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceImpl.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceImpl.scala @@ -4,7 +4,7 @@ import io.iohk.atala.agent.walletapi.crypto.Apollo import io.iohk.atala.agent.walletapi.model.* import io.iohk.atala.agent.walletapi.model.error.{*, given} import io.iohk.atala.agent.walletapi.service.ManagedDIDService.DEFAULT_MASTER_KEY_ID -import io.iohk.atala.agent.walletapi.service.handler.{DIDUpdateHandler, PublicationHandler} +import io.iohk.atala.agent.walletapi.service.handler.{DIDCreateHandler, DIDUpdateHandler, PublicationHandler} import io.iohk.atala.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage} import io.iohk.atala.agent.walletapi.util.* import io.iohk.atala.castor.core.model.did.* @@ -14,16 +14,15 @@ import io.iohk.atala.castor.core.util.DIDOperationValidator import io.iohk.atala.mercury.PeerDID import io.iohk.atala.mercury.model.DidId import zio.* -import scala.language.implicitConversions import java.security.{PrivateKey as JavaPrivateKey, PublicKey as JavaPublicKey} import scala.collection.immutable.ArraySeq -import io.iohk.atala.agent.walletapi.service.handler.DIDCreateHandler +import scala.language.implicitConversions /** A wrapper around Castor's DIDService providing key-management capability. Analogous to the secretAPI in * indy-wallet-sdk. */ -final class ManagedDIDServiceImpl private[walletapi] ( +class ManagedDIDServiceImpl private[walletapi] ( didService: DIDService, didOpValidator: DIDOperationValidator, private[walletapi] val secretStorage: DIDSecretStorage, @@ -266,28 +265,28 @@ final class ManagedDIDServiceImpl private[walletapi] ( } yield awaitingConfirmationOps ++ pendingOps } - private def computeNewDIDLineageStatusAndPersist[E]( + protected def computeNewDIDLineageStatusAndPersist[E]( updateLineage: DIDUpdateLineage - )(using c1: Conversion[DIDOperationError, E], c2: Conversion[CommonWalletStorageError, E]): IO[E, Unit] = { + )(using c1: Conversion[DIDOperationError, E], c2: Conversion[CommonWalletStorageError, E]): IO[E, Boolean] = { for { maybeOperationDetail <- didService .getScheduledDIDOperationDetail(updateLineage.operationId.toArray) .mapError[E](e => e) newStatus = maybeOperationDetail.fold(ScheduledDIDOperationStatus.Rejected)(_.status) - _ <- nonSecretStorage + updated <- nonSecretStorage .setDIDUpdateLineageStatus(updateLineage.operationId.toArray, newStatus) .mapError[E](CommonWalletStorageError.apply) .when(updateLineage.status != newStatus) - } yield () + } yield updated.isDefined } /** Reconcile state with DLT and write new state to the storage */ - private def computeNewDIDStateFromDLTAndPersist[E]( + protected def computeNewDIDStateFromDLTAndPersist[E]( did: CanonicalPrismDID )(using c1: Conversion[CommonWalletStorageError, E], c2: Conversion[DIDOperationError, E] - ): IO[E, Unit] = { + ): IO[E, Boolean] = { for { maybeCurrentState <- nonSecretStorage .getManagedDIDState(did) @@ -295,13 +294,13 @@ final class ManagedDIDServiceImpl private[walletapi] ( maybeNewPubState <- ZIO .foreach(maybeCurrentState)(i => computeNewDIDStateFromDLT(i.publicationState)) .mapError[E](e => e) - _ <- ZIO.foreach(maybeCurrentState zip maybeNewPubState) { case (currentState, newPubState) => + updated <- ZIO.foreach(maybeCurrentState zip maybeNewPubState) { case (currentState, newPubState) => nonSecretStorage .updateManagedDID(did, ManagedDIDStatePatch(newPubState)) .mapError[E](CommonWalletStorageError.apply) .when(currentState.publicationState != newPubState) } - } yield () + } yield updated.flatten.isDefined } /** Reconcile state with DLT and return an updated state */ diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala new file mode 100644 index 0000000000..43189d4014 --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala @@ -0,0 +1,106 @@ +package io.iohk.atala.agent.walletapi.service + +import io.iohk.atala.agent.walletapi.crypto.Apollo +import io.iohk.atala.agent.walletapi.model.error.CommonWalletStorageError +import io.iohk.atala.agent.walletapi.model.{DIDUpdateLineage, ManagedDIDState} +import io.iohk.atala.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage} +import io.iohk.atala.agent.walletapi.util.SeedResolver +import io.iohk.atala.castor.core.model.did.CanonicalPrismDID +import io.iohk.atala.castor.core.model.error +import io.iohk.atala.castor.core.model.error.DIDOperationError +import io.iohk.atala.castor.core.service.DIDService +import io.iohk.atala.castor.core.util.DIDOperationValidator +import io.iohk.atala.event.notification.{Event, EventNotificationService} +import zio.{IO, RLayer, Semaphore, Task, URLayer, ZIO, ZLayer} + +class ManagedDIDServiceWithEventNotificationImpl( + didService: DIDService, + didOpValidator: DIDOperationValidator, + override private[walletapi] val secretStorage: DIDSecretStorage, + override private[walletapi] val nonSecretStorage: DIDNonSecretStorage, + apollo: Apollo, + seed: Array[Byte], + createDIDSem: Semaphore, + eventNotificationService: EventNotificationService +) extends ManagedDIDServiceImpl( + didService, + didOpValidator, + secretStorage, + nonSecretStorage, + apollo, + seed, + createDIDSem + ) { + +// override protected def computeNewDIDLineageStatusAndPersist[E]( +// updateLineage: DIDUpdateLineage +// )(using +// c1: Conversion[DIDOperationError, E], +// c2: Conversion[CommonWalletStorageError, E] +// ): IO[E, Boolean] = { +// for { +// updated <- super.computeNewDIDLineageStatusAndPersist(updateLineage) +// maybeOperationDetail <- didService +// .getScheduledDIDOperationDetail(updateLineage.operationId.toArray) +// .mapError[E](e => e) +// _ <- ZIO.when(updated) { +// val result = for { +// maybeUpdatedDID <- nonSecretStorage.getManagedDIDState(updateLineage.???) +// updatedDID <- ZIO.fromOption(maybeUpdatedDID) +// producer <- eventNotificationService.producer[ManagedDIDState]("DIDState") +// _ <- producer.send(Event(updatedDID)) +// } yield () +// result.catchAll(e => ZIO.logError(s"Notification service error: $e")) +// } +// } yield updated +// } + + override protected def computeNewDIDStateFromDLTAndPersist[E]( + did: CanonicalPrismDID + )(using + c1: Conversion[CommonWalletStorageError, E], + c2: Conversion[DIDOperationError, E] + ): IO[E, Boolean] = { + for { + updated <- super.computeNewDIDStateFromDLTAndPersist(did) + _ <- ZIO.when(updated) { + val result = for { + maybeUpdatedDID <- nonSecretStorage.getManagedDIDState(did) + updatedDID <- ZIO.fromOption(maybeUpdatedDID) + producer <- eventNotificationService.producer[ManagedDIDState]("DIDState") + _ <- producer.send(Event(updatedDID)) + } yield () + result.catchAll(e => ZIO.logError(s"Notification service error: $e")) + } + } yield updated + } + +} + +object ManagedDIDServiceWithEventNotificationImpl { + val layer: RLayer[ + DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & Apollo & SeedResolver & + EventNotificationService, + ManagedDIDService + ] = ZLayer.fromZIO { + for { + didService <- ZIO.service[DIDService] + didOpValidator <- ZIO.service[DIDOperationValidator] + secretStorage <- ZIO.service[DIDSecretStorage] + nonSecretStorage <- ZIO.service[DIDNonSecretStorage] + apollo <- ZIO.service[Apollo] + seed <- ZIO.serviceWithZIO[SeedResolver](_.resolve) + createDIDSem <- Semaphore.make(1) + eventNotificationService <- ZIO.service[EventNotificationService] + } yield ManagedDIDServiceWithEventNotificationImpl( + didService, + didOpValidator, + secretStorage, + nonSecretStorage, + apollo, + seed, + createDIDSem, + eventNotificationService + ) + } +} From 43655194aae80999d29ff584f4e50840331c3f9a Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 30 Jun 2023 16:22:35 +0200 Subject: [PATCH 29/56] feat(prism-agent): consume new 'DIDState' topic events (only DID published for now) --- .../io/iohk/atala/agent/notification/WebhookPublisher.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index c85f1c9b9d..ed7785487d 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -2,6 +2,7 @@ package io.iohk.atala.agent.notification import io.iohk.atala.agent.notification.WebhookPublisher.given import io.iohk.atala.agent.notification.WebhookPublisherError.{InvalidWebhookURL, UnexpectedError} import io.iohk.atala.agent.server.config.{AppConfig, WebhookPublisherConfig} +import io.iohk.atala.agent.walletapi.model.ManagedDIDState import io.iohk.atala.connect.core.model.ConnectionRecord import io.iohk.atala.event.notification.EventNotificationServiceError.DecoderError import io.iohk.atala.event.notification.{Event, EventConsumer, EventNotificationService} @@ -38,9 +39,13 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat presentationConsumer <- notificationService .consumer[PresentationRecord]("Presentation") .mapError(e => UnexpectedError(e.toString)) + didStateConsumer <- notificationService + .consumer[ManagedDIDState]("DIDState") + .mapError(e => UnexpectedError(e.toString)) _ <- pollAndNotify(connectConsumer, url).forever.debug.forkDaemon _ <- pollAndNotify(issueConsumer, url).forever.debug.forkDaemon _ <- pollAndNotify(presentationConsumer, url).forever.debug.forkDaemon + _ <- pollAndNotify(didStateConsumer, url).forever.debug.forkDaemon } yield () case None => ZIO.unit } From c08c9ab726aa85eea0eb7407882b9725899b0687 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 30 Jun 2023 16:34:15 +0200 Subject: [PATCH 30/56] chore(prism-agent): run scalafmt --- .../io/iohk/atala/agent/server/Main.scala | 20 ++++++++++++++++--- .../agent/server/jobs/BackgroundJobs.scala | 17 ++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index ec2dd95b5e..083556a647 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -3,7 +3,11 @@ package io.iohk.atala.agent.server import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton import io.iohk.atala.agent.server.http.ZioHttpClient import io.iohk.atala.agent.server.sql.Migrations as AgentMigrations -import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, ManagedDIDServiceImpl, ManagedDIDServiceWithEventNotificationImpl} +import io.iohk.atala.agent.walletapi.service.{ + ManagedDIDService, + ManagedDIDServiceImpl, + ManagedDIDServiceWithEventNotificationImpl +} import io.iohk.atala.agent.walletapi.sql.JdbcDIDNonSecretStorage import io.iohk.atala.castor.controller.{DIDControllerImpl, DIDRegistrarControllerImpl} import io.iohk.atala.castor.core.service.DIDServiceImpl @@ -15,8 +19,18 @@ import io.iohk.atala.event.notification.{Event, EventNotificationServiceImpl} import io.iohk.atala.issue.controller.IssueControllerImpl import io.iohk.atala.mercury.* import io.iohk.atala.pollux.core.service.* -import io.iohk.atala.pollux.credentialschema.controller.{CredentialSchemaController, CredentialSchemaControllerImpl, VerificationPolicyControllerImpl} -import io.iohk.atala.pollux.sql.repository.{JdbcCredentialRepository, JdbcCredentialSchemaRepository, JdbcPresentationRepository, JdbcVerificationPolicyRepository, Migrations as PolluxMigrations} +import io.iohk.atala.pollux.credentialschema.controller.{ + CredentialSchemaController, + CredentialSchemaControllerImpl, + VerificationPolicyControllerImpl +} +import io.iohk.atala.pollux.sql.repository.{ + JdbcCredentialRepository, + JdbcCredentialSchemaRepository, + JdbcPresentationRepository, + JdbcVerificationPolicyRepository, + Migrations as PolluxMigrations +} import io.iohk.atala.presentproof.controller.PresentProofControllerImpl import io.iohk.atala.resolvers.DIDResolver import io.iohk.atala.system.controller.SystemControllerImpl diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala index aeb8b97e93..6873dd2cba 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala @@ -6,7 +6,11 @@ import io.circe.Json import io.circe.parser.* import io.circe.syntax.* import io.iohk.atala.agent.server.config.AppConfig -import io.iohk.atala.agent.server.jobs.BackgroundJobError.{ErrorResponseReceivedFromPeerAgent, InvalidState, NotImplemented} +import io.iohk.atala.agent.server.jobs.BackgroundJobError.{ + ErrorResponseReceivedFromPeerAgent, + InvalidState, + NotImplemented +} import io.iohk.atala.agent.walletapi.model.* import io.iohk.atala.agent.walletapi.model.error.* import io.iohk.atala.agent.walletapi.model.error.DIDSecretStorageError.KeyNotFoundError @@ -23,7 +27,16 @@ import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.error.PresentationError.* import io.iohk.atala.pollux.core.model.error.{CredentialServiceError, PresentationError} import io.iohk.atala.pollux.core.service.{CredentialService, PresentationService} -import io.iohk.atala.pollux.vc.jwt.{CredentialVerification, ES256KSigner, JWT, JwtPresentation, W3CCredential, W3cCredentialPayload, DidResolver as JwtDidResolver, Issuer as JwtIssuer} +import io.iohk.atala.pollux.vc.jwt.{ + CredentialVerification, + ES256KSigner, + JWT, + JwtPresentation, + W3CCredential, + W3cCredentialPayload, + DidResolver as JwtDidResolver, + Issuer as JwtIssuer +} import io.iohk.atala.resolvers.{DIDResolver, UniversalDidResolver} import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.provider.BouncyCastleProvider From ae8372a1ccca351b0aa545754b8e8cb8aa91f607 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Tue, 4 Jul 2023 08:45:16 +0200 Subject: [PATCH 31/56] chore(prism-agent): scalafmtAll --- .../scala/io/iohk/atala/agent/server/Main.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 68f045d187..8b41046d47 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -15,8 +15,18 @@ import io.iohk.atala.event.notification.EventNotificationServiceImpl import io.iohk.atala.issue.controller.IssueControllerImpl import io.iohk.atala.mercury.* import io.iohk.atala.pollux.core.service.* -import io.iohk.atala.pollux.credentialschema.controller.{CredentialSchemaController, CredentialSchemaControllerImpl, VerificationPolicyControllerImpl} -import io.iohk.atala.pollux.sql.repository.{JdbcCredentialRepository, JdbcCredentialSchemaRepository, JdbcPresentationRepository, JdbcVerificationPolicyRepository, Migrations as PolluxMigrations} +import io.iohk.atala.pollux.credentialschema.controller.{ + CredentialSchemaController, + CredentialSchemaControllerImpl, + VerificationPolicyControllerImpl +} +import io.iohk.atala.pollux.sql.repository.{ + JdbcCredentialRepository, + JdbcCredentialSchemaRepository, + JdbcPresentationRepository, + JdbcVerificationPolicyRepository, + Migrations as PolluxMigrations +} import io.iohk.atala.presentproof.controller.PresentProofControllerImpl import io.iohk.atala.resolvers.DIDResolver import io.iohk.atala.system.controller.SystemControllerImpl From a6e2e14776b4937a8d6e927cdb3a3a80441db0db Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Tue, 4 Jul 2023 14:44:19 +0200 Subject: [PATCH 32/56] test(connect): add unit tests for connection service with event notif --- ...ServiceWithEventNotificationImplSpec.scala | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 connect/lib/core/src/test/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImplSpec.scala diff --git a/connect/lib/core/src/test/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImplSpec.scala b/connect/lib/core/src/test/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImplSpec.scala new file mode 100644 index 0000000000..abf7e593e1 --- /dev/null +++ b/connect/lib/core/src/test/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImplSpec.scala @@ -0,0 +1,86 @@ +package io.iohk.atala.connect.core.service + +import io.iohk.atala.connect.core.model.ConnectionRecord +import io.iohk.atala.connect.core.model.ConnectionRecord.ProtocolState +import io.iohk.atala.connect.core.repository.ConnectionRepositoryInMemory +import io.iohk.atala.event.notification.* +import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.mercury.protocol.connection.{ConnectionRequest, ConnectionResponse} +import zio.* +import zio.ZIO.* +import zio.test.* + +object ConnectionServiceWithEventNotificationImplSpec extends ZIOSpecDefault { + + override def spec: Spec[TestEnvironment with Scope, Any] = { + suite("ConnectionServiceWithEventNotificationImpl")( + test("should send relevant events during flow execution on the inviter side") { + for { + cs <- ZIO.service[ConnectionService] + ens <- ZIO.service[EventNotificationService] + did = DidId("did:peer:INVITER") + connectionRecord <- cs.createConnectionInvitation(Some("test"), did) + _ <- cs.receiveConnectionRequest( + ConnectionRequest( + from = DidId("did:peer:INVITER"), + to = DidId("did:peer:INVITEE"), + thid = connectionRecord.thid.map(_.toString), + pthid = None, + body = ConnectionRequest.Body() + ) + ) + _ <- cs.acceptConnectionRequest(connectionRecord.id) + _ <- cs.markConnectionResponseSent(connectionRecord.id) + consumer <- ens.consumer[ConnectionRecord]("Connect") + events <- consumer.poll(50) + } yield { + assertTrue(events.size == 4) && + assertTrue(events.head.data.protocolState == ProtocolState.InvitationGenerated) && + assertTrue(events(1).data.protocolState == ProtocolState.ConnectionRequestReceived) && + assertTrue(events(2).data.protocolState == ProtocolState.ConnectionResponsePending) && + assertTrue(events(3).data.protocolState == ProtocolState.ConnectionResponseSent) + } + }, + test("should send relevant events during flow execution on the invitee side") { + for { + inviterSvc <- ZIO + .service[ConnectionService] + .provideLayer(ConnectionRepositoryInMemory.layer >>> ConnectionServiceImpl.layer) + inviterDID = DidId("did:peer:INVITER") + inviterRecord <- inviterSvc.createConnectionInvitation( + Some("Test connection invitation"), + inviterDID + ) + inviteeSvc <- ZIO.service[ConnectionService] + inviteeDID = DidId("did:peer:INVITEE") + ens <- ZIO.service[EventNotificationService] + connectionRecord <- inviteeSvc.receiveConnectionInvitation(inviterRecord.invitation.toBase64) + _ <- inviteeSvc.acceptConnectionInvitation(connectionRecord.id, inviteeDID) + _ <- inviteeSvc.markConnectionRequestSent(connectionRecord.id) + _ <- inviteeSvc.receiveConnectionResponse( + ConnectionResponse( + from = inviterDID, + to = inviteeDID, + thid = connectionRecord.thid.map(_.toString), + pthid = None, + body = ConnectionResponse.Body() + ) + ) + consumer <- ens.consumer[ConnectionRecord]("Connect") + events <- consumer.poll(50) + } yield { + assertTrue(events.size == 4) && + assertTrue(events.head.data.protocolState == ProtocolState.InvitationReceived) && + assertTrue(events(1).data.protocolState == ProtocolState.ConnectionRequestPending) && + assertTrue(events(2).data.protocolState == ProtocolState.ConnectionRequestSent) && + assertTrue(events(3).data.protocolState == ProtocolState.ConnectionResponseReceived) + } + } + ).provide( + ConnectionRepositoryInMemory.layer, + ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer, + ConnectionServiceWithEventNotificationImpl.layer + ) + } + +} From 87d7d030c7206d43c2f5639da5a67f5a76611b0e Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Tue, 4 Jul 2023 16:50:21 +0200 Subject: [PATCH 33/56] test(pollux): add unit tests for credential service with event notif --- .../service/CredentialServiceImplSpec.scala | 104 +--------------- .../service/CredentialServiceSpecHelper.scala | 116 ++++++++++++++++++ ...ServiceWithEventNotificationImplSpec.scala | 97 +++++++++++++++ 3 files changed, 216 insertions(+), 101 deletions(-) create mode 100644 pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceSpecHelper.scala create mode 100644 pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImplSpec.scala diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceImplSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceImplSpec.scala index 23849ea37c..67ec81bce6 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceImplSpec.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceImplSpec.scala @@ -2,31 +2,21 @@ package io.iohk.atala.pollux.core.service import io.circe.Json import io.circe.syntax.* -import io.grpc.ManagedChannelBuilder import io.iohk.atala.castor.core.model.did.CanonicalPrismDID -import io.iohk.atala.iris.proto.service.IrisServiceGrpc -import io.iohk.atala.mercury.model.{AttachmentDescriptor, DidId, Message} +import io.iohk.atala.mercury.model.{DidId, Message} import io.iohk.atala.mercury.protocol.issuecredential.* import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.IssueCredentialRecord.* import io.iohk.atala.pollux.core.model.error.CredentialServiceError import io.iohk.atala.pollux.core.model.error.CredentialServiceError.* -import io.iohk.atala.pollux.core.model.presentation.{ClaimFormat, Ldp, Options, PresentationDefinition} -import io.iohk.atala.pollux.core.repository.CredentialRepositoryInMemory import io.iohk.atala.pollux.vc.jwt.* import zio.* import zio.test.* + import java.nio.charset.StandardCharsets import java.util.{Base64, UUID} -object CredentialServiceImplSpec extends ZIOSpecDefault { - - val irisStubLayer = ZLayer.fromZIO( - ZIO.succeed(IrisServiceGrpc.stub(ManagedChannelBuilder.forAddress("localhost", 9999).usePlaintext.build)) - ) - val didResolverLayer = ZLayer.fromZIO(ZIO.succeed(makeResolver(Map.empty))) - val credentialServiceLayer = - irisStubLayer ++ CredentialRepositoryInMemory.layer ++ didResolverLayer ++ ResourceURIDereferencerImpl.layer >>> CredentialServiceImpl.layer +object CredentialServiceImplSpec extends ZIOSpecDefault with CredentialServiceSpecHelper { override def spec = { suite("CredentialServiceImpl")( @@ -527,92 +517,4 @@ object CredentialServiceImplSpec extends ZIOSpecDefault { ).provideLayer(credentialServiceLayer) } - private[this] def offerCredential( - thid: Option[UUID] = Some(UUID.randomUUID()) - ) = OfferCredential( - from = DidId("did:prism:issuer"), - to = DidId("did:prism:holder"), - thid = thid.map(_.toString), - attachments = Seq( - AttachmentDescriptor.buildJsonAttachment( - payload = CredentialOfferAttachment( - Options(UUID.randomUUID().toString(), "my-domain"), - PresentationDefinition(format = Some(ClaimFormat(ldp = Some(Ldp(Seq("EcdsaSecp256k1Signature2019")))))) - ) - ) - ), - body = OfferCredential.Body( - goal_code = Some("Offer Credential"), - credential_preview = CredentialPreview(attributes = Seq(Attribute("name", "Alice"))) - ) - ) - - private[this] def requestCredential(thid: Option[DidCommID] = Some(DidCommID())) = RequestCredential( - from = DidId("did:prism:holder"), - to = DidId("did:prism:issuer"), - thid = thid.map(_.toString), - attachments = Nil, - body = RequestCredential.Body() - ) - - private[this] def issueCredential(thid: Option[DidCommID] = Some(DidCommID())) = IssueCredential( - from = DidId("did:prism:issuer"), - to = DidId("did:prism:holder"), - thid = thid.map(_.toString), - attachments = Nil, - body = IssueCredential.Body() - ) - - private[this] def makeResolver(lookup: Map[String, DIDDocument]): DidResolver = (didUrl: String) => { - lookup - .get(didUrl) - .fold( - ZIO.succeed(DIDResolutionFailed(NotFound(s"DIDDocument not found for $didUrl"))) - )((didDocument: DIDDocument) => { - ZIO.succeed( - DIDResolutionSucceeded( - didDocument, - DIDDocumentMetadata() - ) - ) - }) - } - - val defaultClaims = io.circe.parser - .parse(""" - |{ - | "name":"Alice", - | "address": { - | "street": "Street Name", - | "number": "12" - | } - |} - |""".stripMargin) - .getOrElse(Json.Null) - - extension (svc: CredentialService) - def createRecord( - pairwiseIssuerDID: DidId = DidId("did:prism:issuer"), - pairwiseHolderDID: DidId = DidId("did:prism:holder-pairwise"), - thid: DidCommID = DidCommID(), - schemaId: Option[String] = None, - claims: Json = defaultClaims, - validityPeriod: Option[Double] = None, - automaticIssuance: Option[Boolean] = None, - awaitConfirmation: Option[Boolean] = None, - issuingDID: Option[CanonicalPrismDID] = None - ) = { - svc.createIssueCredentialRecord( - pairwiseIssuerDID = pairwiseIssuerDID, - pairwiseHolderDID = pairwiseHolderDID, - thid = thid, - maybeSchemaId = schemaId, - claims = claims, - validityPeriod = validityPeriod, - automaticIssuance = automaticIssuance, - awaitConfirmation = awaitConfirmation, - issuingDID = issuingDID - ) - } - } diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceSpecHelper.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceSpecHelper.scala new file mode 100644 index 0000000000..a4530d9394 --- /dev/null +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceSpecHelper.scala @@ -0,0 +1,116 @@ +package io.iohk.atala.pollux.core.service + +import io.circe.Json +import io.grpc.ManagedChannelBuilder +import io.iohk.atala.castor.core.model.did.CanonicalPrismDID +import io.iohk.atala.iris.proto.service.IrisServiceGrpc +import io.iohk.atala.mercury.model.{AttachmentDescriptor, DidId} +import io.iohk.atala.mercury.protocol.issuecredential.* +import io.iohk.atala.pollux.core.model.* +import io.iohk.atala.pollux.core.model.error.CredentialServiceError +import io.iohk.atala.pollux.core.model.error.CredentialServiceError.* +import io.iohk.atala.pollux.core.model.presentation.{ClaimFormat, Ldp, Options, PresentationDefinition} +import io.iohk.atala.pollux.core.repository.CredentialRepositoryInMemory +import io.iohk.atala.pollux.vc.jwt.* +import zio.* + +import java.util.UUID + +trait CredentialServiceSpecHelper { + + protected val irisStubLayer = ZLayer.fromZIO( + ZIO.succeed(IrisServiceGrpc.stub(ManagedChannelBuilder.forAddress("localhost", 9999).usePlaintext.build)) + ) + protected val didResolverLayer = ZLayer.fromZIO(ZIO.succeed(makeResolver(Map.empty))) + protected val credentialServiceLayer = + irisStubLayer ++ CredentialRepositoryInMemory.layer ++ didResolverLayer ++ ResourceURIDereferencerImpl.layer >>> CredentialServiceImpl.layer + + protected def offerCredential( + thid: Option[UUID] = Some(UUID.randomUUID()) + ) = OfferCredential( + from = DidId("did:prism:issuer"), + to = DidId("did:prism:holder"), + thid = thid.map(_.toString), + attachments = Seq( + AttachmentDescriptor.buildJsonAttachment( + payload = CredentialOfferAttachment( + Options(UUID.randomUUID().toString(), "my-domain"), + PresentationDefinition(format = Some(ClaimFormat(ldp = Some(Ldp(Seq("EcdsaSecp256k1Signature2019")))))) + ) + ) + ), + body = OfferCredential.Body( + goal_code = Some("Offer Credential"), + credential_preview = CredentialPreview(attributes = Seq(Attribute("name", "Alice"))) + ) + ) + + protected def requestCredential(thid: Option[DidCommID] = Some(DidCommID())) = RequestCredential( + from = DidId("did:prism:holder"), + to = DidId("did:prism:issuer"), + thid = thid.map(_.toString), + attachments = Nil, + body = RequestCredential.Body() + ) + + protected def issueCredential(thid: Option[DidCommID] = Some(DidCommID())) = IssueCredential( + from = DidId("did:prism:issuer"), + to = DidId("did:prism:holder"), + thid = thid.map(_.toString), + attachments = Nil, + body = IssueCredential.Body() + ) + + protected def makeResolver(lookup: Map[String, DIDDocument]): DidResolver = (didUrl: String) => { + lookup + .get(didUrl) + .fold( + ZIO.succeed(DIDResolutionFailed(NotFound(s"DIDDocument not found for $didUrl"))) + )((didDocument: DIDDocument) => { + ZIO.succeed( + DIDResolutionSucceeded( + didDocument, + DIDDocumentMetadata() + ) + ) + }) + } + + val defaultClaims = io.circe.parser + .parse(""" + |{ + | "name":"Alice", + | "address": { + | "street": "Street Name", + | "number": "12" + | } + |} + |""".stripMargin) + .getOrElse(Json.Null) + + extension (svc: CredentialService) + def createRecord( + pairwiseIssuerDID: DidId = DidId("did:prism:issuer"), + pairwiseHolderDID: DidId = DidId("did:prism:holder-pairwise"), + thid: DidCommID = DidCommID(), + schemaId: Option[String] = None, + claims: Json = defaultClaims, + validityPeriod: Option[Double] = None, + automaticIssuance: Option[Boolean] = None, + awaitConfirmation: Option[Boolean] = None, + issuingDID: Option[CanonicalPrismDID] = None + ) = { + svc.createIssueCredentialRecord( + pairwiseIssuerDID = pairwiseIssuerDID, + pairwiseHolderDID = pairwiseHolderDID, + thid = thid, + maybeSchemaId = schemaId, + claims = claims, + validityPeriod = validityPeriod, + automaticIssuance = automaticIssuance, + awaitConfirmation = awaitConfirmation, + issuingDID = issuingDID + ) + } + +} diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImplSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImplSpec.scala new file mode 100644 index 0000000000..7f5ed36ce2 --- /dev/null +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImplSpec.scala @@ -0,0 +1,97 @@ +package io.iohk.atala.pollux.core.service + +import io.circe.syntax.* +import io.iohk.atala.event.notification.{EventNotificationService, EventNotificationServiceImpl} +import io.iohk.atala.mercury.model.Message +import io.iohk.atala.mercury.protocol.issuecredential.* +import io.iohk.atala.pollux.core.model.* +import io.iohk.atala.pollux.core.model.IssueCredentialRecord.ProtocolState +import io.iohk.atala.pollux.core.model.error.CredentialServiceError +import io.iohk.atala.pollux.core.repository.CredentialRepositoryInMemory +import io.iohk.atala.pollux.vc.jwt.* +import zio.* +import zio.test.* + +object CredentialServiceWithEventNotificationImplSpec extends ZIOSpecDefault with CredentialServiceSpecHelper { + + private val eventNotificationService = ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer + private val credentialServiceWithEventNotificationLayer = + irisStubLayer ++ CredentialRepositoryInMemory.layer ++ didResolverLayer ++ ResourceURIDereferencerImpl.layer + >>> CredentialServiceWithEventNotificationImpl.layer + + override def spec: Spec[TestEnvironment with Scope, Any] = { + suite("CredentialServiceWithEventNotificationImpl") { + test("Happy flow generates relevant events") { + for { + // Get issuer services + issuerServices <- (for { + issuerSvc <- ZIO.service[CredentialService] + issuerEns <- ZIO.service[EventNotificationService] + } yield (issuerSvc, issuerEns)) + .provide(eventNotificationService, credentialServiceWithEventNotificationLayer) + issuerSvc = issuerServices._1 + issuerEns = issuerServices._2 + + // Get Holder services + holderServices <- (for { + holderSvc <- ZIO.service[CredentialService] + holderEns <- ZIO.service[EventNotificationService] + } yield (holderSvc, holderEns)) + .provide(eventNotificationService, credentialServiceWithEventNotificationLayer) + holderSvc = holderServices._1 + holderEns = holderServices._2 + + // Issuer creates offer + offerCreatedRecord <- issuerSvc.createRecord() + issuerRecordId = offerCreatedRecord.id + // Issuer sends offer + _ <- issuerSvc.markOfferSent(issuerRecordId) + msg <- ZIO.fromEither(offerCreatedRecord.offerCredentialData.get.makeMessage.asJson.as[Message]) + // Holder receives offer + offerReceivedRecord <- holderSvc.receiveCredentialOffer(OfferCredential.readFromMessage(msg)) + holderRecordId = offerReceivedRecord.id + subjectId = "did:prism:60821d6833158c93fde5bb6a40d69996a683bf1fa5cdf32c458395b2887597c3" + // Holder accepts offer + _ <- holderSvc.acceptCredentialOffer(holderRecordId, subjectId) + // Holder generates proof + requestGeneratedRecord <- holderSvc.generateCredentialRequest(offerReceivedRecord.id, JWT("Fake JWT")) + // Holder sends offer + _ <- holderSvc.markRequestSent(holderRecordId) + msg <- ZIO.fromEither(requestGeneratedRecord.requestCredentialData.get.makeMessage.asJson.as[Message]) + // Issuer receives request + requestReceivedRecord <- issuerSvc.receiveCredentialRequest(RequestCredential.readFromMessage(msg)) + // Issuer accepts request + requestAcceptedRecord <- issuerSvc.acceptCredentialRequest(issuerRecordId) + // Issuer generates credential + issue = issueCredential(Some(requestAcceptedRecord.thid)) + credentialGenerateRecord <- issuerSvc.markCredentialGenerated(issuerRecordId, issue) + // Issuer sends credential + _ <- issuerSvc.markCredentialSent(issuerRecordId) + msg <- ZIO.fromEither(credentialGenerateRecord.issueCredentialData.get.makeMessage.asJson.as[Message]) + // Holder receives credential + _ <- holderSvc.receiveCredentialIssue(IssueCredential.readFromMessage(msg)) + // Get generated events + issuerConsumer <- issuerEns.consumer[IssueCredentialRecord]("Issue") + issuerEvents <- issuerConsumer.poll(50) + holderConsumer <- holderEns.consumer[IssueCredentialRecord]("Issue") + holderEvents <- holderConsumer.poll(50) + } yield { + assertTrue(issuerEvents.size == 6) && + assertTrue(issuerEvents.head.data.protocolState == ProtocolState.OfferPending) && + assertTrue(issuerEvents(1).data.protocolState == ProtocolState.OfferSent) && + assertTrue(issuerEvents(2).data.protocolState == ProtocolState.RequestReceived) && + assertTrue(issuerEvents(3).data.protocolState == ProtocolState.CredentialPending) && + assertTrue(issuerEvents(4).data.protocolState == ProtocolState.CredentialGenerated) && + assertTrue(issuerEvents(5).data.protocolState == ProtocolState.CredentialSent) && + assertTrue(holderEvents.size == 5) && + assertTrue(holderEvents.head.data.protocolState == ProtocolState.OfferReceived) && + assertTrue(holderEvents(1).data.protocolState == ProtocolState.RequestPending) && + assertTrue(holderEvents(2).data.protocolState == ProtocolState.RequestGenerated) && + assertTrue(holderEvents(3).data.protocolState == ProtocolState.RequestSent) && + assertTrue(holderEvents(4).data.protocolState == ProtocolState.CredentialReceived) + } + } + } + } + +} From a66217c82a0f5c7e68846233342fc05f1c5eb2ab Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Tue, 4 Jul 2023 17:48:22 +0200 Subject: [PATCH 34/56] test(pollux): move presentation service spec utility methods to helper trait --- .../service/PresentationServiceSpec.scala | 162 ++---------------- .../PresentationServiceSpecHelper.scala | 149 ++++++++++++++++ 2 files changed, 162 insertions(+), 149 deletions(-) create mode 100644 pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala index e94705a13a..dc4f905acb 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala @@ -1,44 +1,26 @@ package io.iohk.atala.pollux.core.service import io.circe.parser.decode -import io.circe.syntax._ -import io.iohk.atala.mercury.model.DidId -import io.iohk.atala.mercury.protocol.presentproof._ -import io.iohk.atala.pollux.core.model._ -import io.iohk.atala.pollux.core.model.IssueCredentialRecord._ -import io.iohk.atala.pollux.core.model.PresentationRecord._ +import io.circe.syntax.* +import io.iohk.atala.mercury.model.{AttachmentDescriptor, DidId} +import io.iohk.atala.mercury.protocol.issuecredential.IssueCredential +import io.iohk.atala.mercury.protocol.presentproof.* +import io.iohk.atala.pollux.core.model.* +import io.iohk.atala.pollux.core.model.IssueCredentialRecord.* +import io.iohk.atala.pollux.core.model.PresentationRecord.* import io.iohk.atala.pollux.core.model.error.PresentationError -import io.iohk.atala.pollux.core.model.error.PresentationError._ -import io.iohk.atala.pollux.core.repository.PresentationRepositoryInMemory -import io.iohk.atala.pollux.core.repository.PresentationRepository +import io.iohk.atala.pollux.core.model.error.PresentationError.* +import io.iohk.atala.pollux.core.model.presentation.Options +import io.iohk.atala.pollux.core.repository.{CredentialRepository, PresentationRepository} +import io.iohk.atala.pollux.vc.jwt.* import zio.* import zio.test.* -import java.util.UUID -import io.iohk.atala.mercury.model.AttachmentDescriptor -import io.iohk.atala.pollux.core.model.presentation.Options -import io.iohk.atala.pollux.vc.jwt._ -import io.iohk.atala.pollux.vc.jwt.JwtPresentationPayload -import io.iohk.atala.pollux.core.repository.CredentialRepositoryInMemory -import io.iohk.atala.pollux.core.repository.CredentialRepository -import io.iohk.atala.mercury.DidAgent -import com.nimbusds.jose.jwk.OctetKeyPair -import io.iohk.atala.mercury.PeerDID -import io.iohk.atala.mercury.AgentPeerService + import java.time.Instant -import io.iohk.atala.mercury.protocol.issuecredential.IssueCredential -import java.security.* -import com.nimbusds.jose.jwk.* -object PresentationServiceSpec extends ZIOSpecDefault { +object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSpecHelper { type PresentationEnv = PresentationService with PresentationRepository[Task] with CredentialRepository[Task] - val peerDidAgentLayer = - AgentPeerService.makeLayer(PeerDID.makePeerDid(serviceEndpoint = Some("http://localhost:9099"))) - val presentationServiceLayer = - PresentationRepositoryInMemory.layer ++ CredentialRepositoryInMemory.layer ++ peerDidAgentLayer >>> PresentationServiceImpl.layer - val presentationEnvLayer = - PresentationRepositoryInMemory.layer ++ CredentialRepositoryInMemory.layer ++ presentationServiceLayer - def withEnv[E, A](zio: ZIO[PresentationEnv, E, A]): ZIO[Any, E, A] = zio.provideLayer(presentationEnvLayer) @@ -460,122 +442,4 @@ object PresentationServiceSpec extends ZIOSpecDefault { ).provideLayer(presentationServiceLayer) } - def createIssuer(did: DID) = { - val keyGen = KeyPairGenerator.getInstance("EC") - keyGen.initialize(Curve.P_256.toECParameterSpec) - val keyPair = keyGen.generateKeyPair() - val privateKey = keyPair.getPrivate - val publicKey = keyPair.getPublic - Issuer( - did = did, - signer = ES256Signer(privateKey), - publicKey = publicKey - ) - } - private def requestCredential = io.iohk.atala.mercury.protocol.issuecredential.RequestCredential( - from = DidId("did:prism:aaa"), - to = DidId("did:prism:bbb"), - thid = Some(UUID.randomUUID.toString), - body = - io.iohk.atala.mercury.protocol.issuecredential.RequestCredential.Body(goal_code = Some("credential issuance")), - attachments = Nil - ) - - private def requestPresentation: RequestPresentation = { - val body = RequestPresentation.Body(goal_code = Some("Presentation Request")) - val presentationAttachmentAsJson = """{ - "challenge": "1f44d55f-f161-4938-a659-f8026467f126", - "domain": "us.gov/DriverLicense", - "credential_manifest": {} - }""" - val prover = DidId("did:peer:Prover") - val verifier = DidId("did:peer:Verifier") - - val attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment(payload = presentationAttachmentAsJson) - RequestPresentation( - body = body, - attachments = Seq(attachmentDescriptor), - to = prover, - from = verifier, - ) - } - - private def proposePresentation(thid: String): ProposePresentation = { - val body = ProposePresentation.Body(goal_code = Some("Propose Presentation")) - val presentationAttachmentAsJson = """{ - "id": "1f44d55f-f161-4938-a659-f8026467f126", - "subject": "subject", - "credential_definition": {} - }""" - val attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment(payload = presentationAttachmentAsJson) - val prover = DidId("did:peer:Prover") - val verifier = DidId("did:peer:Verifier") - ProposePresentation( - body = body, - thid = Some(thid), - attachments = Seq(attachmentDescriptor), - to = verifier, - from = prover - ) - } - private def presentation(thid: String): Presentation = { - val body = Presentation.Body(goal_code = Some("Presentation")) - val presentationAttachmentAsJson = """{ - "id": "1f44d55f-f161-4938-a659-f8026467f126", - "subject": "subject", - "credential_definition": {} - }""" - val attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment(payload = presentationAttachmentAsJson) - val prover = DidId("did:peer:Prover") - val verifier = DidId("did:peer:Verifier") - Presentation( - body = body, - thid = Some(thid), - attachments = Seq(attachmentDescriptor), - to = verifier, - from = prover - ) - } - private def issueCredentialRecord = IssueCredentialRecord( - id = DidCommID(), - createdAt = Instant.ofEpochSecond(Instant.now.getEpochSecond()), - updatedAt = None, - thid = DidCommID(), - schemaId = None, - role = IssueCredentialRecord.Role.Issuer, - subjectId = None, - validityPeriod = None, - automaticIssuance = None, - awaitConfirmation = None, - protocolState = IssueCredentialRecord.ProtocolState.OfferPending, - publicationState = None, - offerCredentialData = None, - requestCredentialData = None, - issueCredentialData = None, - issuedCredentialRaw = None, - issuingDID = None, - metaRetries = 5, - metaNextRetry = Some(Instant.now()), - metaLastFailure = None, - ) - - extension (svc: PresentationService) - def createRecord( - pairwiseVerifierDID: DidId = DidId("did:prism:issuer"), - pairwiseProverDID: DidId = DidId("did:prism:prover-pairwise"), - thid: DidCommID = DidCommID(), - schemaId: String = "schemaId", - connectionId: Option[String] = None, - ) = { - val proofType = ProofType(schemaId, None, None) - svc.createPresentationRecord( - thid = thid, - pairwiseVerifierDID = pairwiseVerifierDID, - pairwiseProverDID = pairwiseProverDID, - connectionId = Some("connectionId"), - proofTypes = Seq(proofType), - options = None, - ) - } - } diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala new file mode 100644 index 0000000000..0679fcf3a9 --- /dev/null +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala @@ -0,0 +1,149 @@ +package io.iohk.atala.pollux.core.service + +import com.nimbusds.jose.jwk.* +import io.iohk.atala.mercury.model.{AttachmentDescriptor, DidId} +import io.iohk.atala.mercury.protocol.presentproof.* +import io.iohk.atala.mercury.{AgentPeerService, DidAgent, PeerDID} +import io.iohk.atala.pollux.core.model.* +import io.iohk.atala.pollux.core.repository.{CredentialRepository, CredentialRepositoryInMemory, PresentationRepositoryInMemory} +import io.iohk.atala.pollux.vc.jwt.* +import zio.* + +import java.security.* +import java.time.Instant +import java.util.UUID + +trait PresentationServiceSpecHelper { + + val peerDidAgentLayer = + AgentPeerService.makeLayer(PeerDID.makePeerDid(serviceEndpoint = Some("http://localhost:9099"))) + val presentationServiceLayer = + PresentationRepositoryInMemory.layer ++ CredentialRepositoryInMemory.layer ++ peerDidAgentLayer >>> PresentationServiceImpl.layer + val presentationEnvLayer = + PresentationRepositoryInMemory.layer ++ CredentialRepositoryInMemory.layer ++ presentationServiceLayer + + def createIssuer(did: DID) = { + val keyGen = KeyPairGenerator.getInstance("EC") + keyGen.initialize(Curve.P_256.toECParameterSpec) + val keyPair = keyGen.generateKeyPair() + val privateKey = keyPair.getPrivate + val publicKey = keyPair.getPublic + Issuer( + did = did, + signer = ES256Signer(privateKey), + publicKey = publicKey + ) + } + + protected def requestCredential = io.iohk.atala.mercury.protocol.issuecredential.RequestCredential( + from = DidId("did:prism:aaa"), + to = DidId("did:prism:bbb"), + thid = Some(UUID.randomUUID.toString), + body = + io.iohk.atala.mercury.protocol.issuecredential.RequestCredential.Body(goal_code = Some("credential issuance")), + attachments = Nil + ) + + protected def requestPresentation: RequestPresentation = { + val body = RequestPresentation.Body(goal_code = Some("Presentation Request")) + val presentationAttachmentAsJson = + """{ + "challenge": "1f44d55f-f161-4938-a659-f8026467f126", + "domain": "us.gov/DriverLicense", + "credential_manifest": {} + }""" + val prover = DidId("did:peer:Prover") + val verifier = DidId("did:peer:Verifier") + + val attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment(payload = presentationAttachmentAsJson) + RequestPresentation( + body = body, + attachments = Seq(attachmentDescriptor), + to = prover, + from = verifier, + ) + } + + protected def proposePresentation(thid: String): ProposePresentation = { + val body = ProposePresentation.Body(goal_code = Some("Propose Presentation")) + val presentationAttachmentAsJson = + """{ + "id": "1f44d55f-f161-4938-a659-f8026467f126", + "subject": "subject", + "credential_definition": {} + }""" + val attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment(payload = presentationAttachmentAsJson) + val prover = DidId("did:peer:Prover") + val verifier = DidId("did:peer:Verifier") + ProposePresentation( + body = body, + thid = Some(thid), + attachments = Seq(attachmentDescriptor), + to = verifier, + from = prover + ) + } + + protected def presentation(thid: String): Presentation = { + val body = Presentation.Body(goal_code = Some("Presentation")) + val presentationAttachmentAsJson = + """{ + "id": "1f44d55f-f161-4938-a659-f8026467f126", + "subject": "subject", + "credential_definition": {} + }""" + val attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment(payload = presentationAttachmentAsJson) + val prover = DidId("did:peer:Prover") + val verifier = DidId("did:peer:Verifier") + Presentation( + body = body, + thid = Some(thid), + attachments = Seq(attachmentDescriptor), + to = verifier, + from = prover + ) + } + + protected def issueCredentialRecord = IssueCredentialRecord( + id = DidCommID(), + createdAt = Instant.ofEpochSecond(Instant.now.getEpochSecond()), + updatedAt = None, + thid = DidCommID(), + schemaId = None, + role = IssueCredentialRecord.Role.Issuer, + subjectId = None, + validityPeriod = None, + automaticIssuance = None, + awaitConfirmation = None, + protocolState = IssueCredentialRecord.ProtocolState.OfferPending, + publicationState = None, + offerCredentialData = None, + requestCredentialData = None, + issueCredentialData = None, + issuedCredentialRaw = None, + issuingDID = None, + metaRetries = 5, + metaNextRetry = Some(Instant.now()), + metaLastFailure = None, + ) + + extension (svc: PresentationService) + def createRecord( + pairwiseVerifierDID: DidId = DidId("did:prism:issuer"), + pairwiseProverDID: DidId = DidId("did:prism:prover-pairwise"), + thid: DidCommID = DidCommID(), + schemaId: String = "schemaId", + connectionId: Option[String] = None, + ) = { + val proofType = ProofType(schemaId, None, None) + svc.createPresentationRecord( + thid = thid, + pairwiseVerifierDID = pairwiseVerifierDID, + pairwiseProverDID = pairwiseProverDID, + connectionId = Some("connectionId"), + proofTypes = Seq(proofType), + options = None, + ) + } + +} From 15ce6a550c4afae90e57ebeba5cee74df25d060c Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Tue, 4 Jul 2023 18:12:29 +0200 Subject: [PATCH 35/56] chore(prism-agent): scalafmtAll --- .../pollux/core/service/PresentationServiceSpecHelper.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala index 0679fcf3a9..e5904ed580 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala @@ -5,7 +5,11 @@ import io.iohk.atala.mercury.model.{AttachmentDescriptor, DidId} import io.iohk.atala.mercury.protocol.presentproof.* import io.iohk.atala.mercury.{AgentPeerService, DidAgent, PeerDID} import io.iohk.atala.pollux.core.model.* -import io.iohk.atala.pollux.core.repository.{CredentialRepository, CredentialRepositoryInMemory, PresentationRepositoryInMemory} +import io.iohk.atala.pollux.core.repository.{ + CredentialRepository, + CredentialRepositoryInMemory, + PresentationRepositoryInMemory +} import io.iohk.atala.pollux.vc.jwt.* import zio.* From 6c323111324fa6f83d362f2ccbe43c558d4c6560 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Wed, 5 Jul 2023 10:49:10 +0200 Subject: [PATCH 36/56] chore(pollux): fix method typo --- .../io/iohk/atala/pollux/core/service/PresentationService.scala | 2 +- .../presentproof/controller/PresentProofControllerImpl.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala index 32d9d59e6a..6a63aa807f 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala @@ -48,7 +48,7 @@ trait PresentationService { def acceptRequestPresentation( recordId: DidCommID, - crecentialsToUse: Seq[String] + credentialsToUse: Seq[String] ): IO[PresentationError, PresentationRecord] def rejectRequestPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala index db451c8211..57a8284169 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala @@ -88,7 +88,7 @@ class PresentProofControllerImpl( case "request-accept" => presentationService.acceptRequestPresentation( recordId = didCommId, - crecentialsToUse = requestPresentationAction.proofId.getOrElse(Seq.empty) + credentialsToUse = requestPresentationAction.proofId.getOrElse(Seq.empty) ) case "request-reject" => presentationService.rejectRequestPresentation(didCommId) case "presentation-accept" => presentationService.acceptPresentation(didCommId) From 4901251c46f11ff18ac7372ad633540f91c0e62f Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Wed, 5 Jul 2023 11:00:58 +0200 Subject: [PATCH 37/56] test(pollux): add unit tests for presentation flow notifications, introducing ZIO Mock for PresentationService --- build.sbt | 1 + .../service/MockPresentationService.scala | 145 +++++++++++++ .../service/PresentationServiceNotifier.scala | 139 +++++++++++++ ...tionServiceWithEventNotificationImpl.scala | 95 --------- .../PresentationServiceNotifierSpec.scala | 195 ++++++++++++++++++ .../io/iohk/atala/agent/server/Main.scala | 2 +- 6 files changed, 481 insertions(+), 96 deletions(-) create mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala create mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala delete mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala create mode 100644 pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala diff --git a/build.sbt b/build.sbt index 9be1fccea2..a734514468 100644 --- a/build.sbt +++ b/build.sbt @@ -219,6 +219,7 @@ lazy val D_Pollux = new { D.zioTest, D.zioTestSbt, D.zioTestMagnolia, + D.zioMock, D.munit, D.munitZio, prismCrypto, diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala new file mode 100644 index 0000000000..9be9d8489f --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala @@ -0,0 +1,145 @@ +package io.iohk.atala.pollux.core.service + +import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.mercury.protocol.presentproof.{Presentation, ProofType, ProposePresentation, RequestPresentation} +import io.iohk.atala.pollux.core.model.error.PresentationError +import io.iohk.atala.pollux.core.model.presentation.Options +import io.iohk.atala.pollux.core.model.{DidCommID, PresentationRecord} +import io.iohk.atala.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCredentialPayload} +import zio.mock.{Mock, Proxy} +import zio.{IO, URLayer, ZIO, ZLayer, mock} + +import java.time.Instant +import java.util.UUID + +object MockPresentationService extends Mock[PresentationService] { + + object CreatePresentationRecord + extends Effect[ + (DidId, DidId, DidCommID, Option[String], Seq[ProofType], Option[Options]), + PresentationError, + PresentationRecord + ] + + object MarkRequestPresentationSent extends Effect[DidCommID, PresentationError, PresentationRecord] + + object ReceivePresentation extends Effect[Presentation, PresentationError, PresentationRecord] + + object MarkPresentationVerified extends Effect[DidCommID, PresentationError, PresentationRecord] + + object MarkPresentationAccepted extends Effect[DidCommID, PresentationError, PresentationRecord] + + object MarkPresentationRejected extends Effect[DidCommID, PresentationError, PresentationRecord] + + object MarkPresentationVerificationFailed extends Effect[DidCommID, PresentationError, PresentationRecord] + + object AcceptRequestPresentation extends Effect[(DidCommID, Seq[String]), PresentationError, PresentationRecord] + + object RejectRequestPresentation extends Effect[DidCommID, PresentationError, PresentationRecord] + + object MarkPresentationGenerated extends Effect[(DidCommID, Presentation), PresentationError, PresentationRecord] + + object MarkPresentationSent extends Effect[DidCommID, PresentationError, PresentationRecord] + + object ReceiveRequestPresentation + extends Effect[(Option[String], RequestPresentation), PresentationError, PresentationRecord] + + override val compose: URLayer[mock.Proxy, PresentationService] = ZLayer { + for { + proxy <- ZIO.service[Proxy] + } yield new PresentationService { + + override def createPresentationRecord( + pairwiseVerifierDID: DidId, + pairwiseProverDID: DidId, + thid: DidCommID, + connectionId: Option[String], + proofTypes: Seq[ProofType], + options: Option[Options] + ): IO[PresentationError, PresentationRecord] = + proxy( + CreatePresentationRecord, + (pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, proofTypes, options) + ) + + override def acceptRequestPresentation( + recordId: DidCommID, + credentialsToUse: Seq[String] + ): IO[PresentationError, PresentationRecord] = + proxy(AcceptRequestPresentation, (recordId, credentialsToUse)) + + override def rejectRequestPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + proxy(RejectRequestPresentation, recordId) + + override def receivePresentation(presentation: Presentation): IO[PresentationError, PresentationRecord] = + proxy(ReceivePresentation, presentation) + + override def markRequestPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + proxy(MarkRequestPresentationSent, recordId) + + override def markPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + proxy(MarkPresentationSent, recordId) + + override def markPresentationGenerated( + recordId: DidCommID, + presentation: Presentation + ): IO[PresentationError, PresentationRecord] = + proxy(MarkPresentationGenerated, (recordId, presentation)) + + override def markPresentationVerified(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + proxy(MarkPresentationVerified, recordId) + + override def markPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + proxy(MarkPresentationRejected, recordId) + + override def markPresentationAccepted(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + proxy(MarkPresentationAccepted, recordId) + + override def markPresentationVerificationFailed(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + proxy(MarkPresentationVerificationFailed, recordId) + + override def receiveRequestPresentation( + connectionId: Option[String], + request: RequestPresentation + ): IO[PresentationError, PresentationRecord] = + proxy(ReceiveRequestPresentation, (connectionId, request)) + + override def extractIdFromCredential(credential: W3cCredentialPayload): Option[UUID] = ??? + + override def getPresentationRecords(): IO[PresentationError, Seq[PresentationRecord]] = ??? + + override def createPresentationPayloadFromRecord( + record: DidCommID, + issuer: Issuer, + issuanceDate: Instant + ): IO[PresentationError, PresentationPayload] = ??? + + override def getPresentationRecordsByStates( + ignoreWithZeroRetries: Boolean, + limit: Int, + state: PresentationRecord.ProtocolState* + ): IO[PresentationError, Seq[PresentationRecord]] = ??? + + override def getPresentationRecord(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = ??? + + override def receiveProposePresentation(request: ProposePresentation): IO[PresentationError, PresentationRecord] = + ??? + + override def acceptProposePresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = ??? + + override def acceptPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = ??? + + override def rejectPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = ??? + + override def markRequestPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = ??? + + override def markProposePresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = ??? + + override def reportProcessingFailure( + recordId: DidCommID, + failReason: Option[String] + ): IO[PresentationError, Unit] = ??? + } + } + +} diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala new file mode 100644 index 0000000000..a9af976b6b --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala @@ -0,0 +1,139 @@ +package io.iohk.atala.pollux.core.service +import io.iohk.atala.event.notification.{Event, EventNotificationService} +import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.mercury.protocol.presentproof.{Presentation, ProofType, ProposePresentation, RequestPresentation} +import io.iohk.atala.pollux.core.model.error.PresentationError +import io.iohk.atala.pollux.core.model.presentation.Options +import io.iohk.atala.pollux.core.model.{DidCommID, PresentationRecord} +import io.iohk.atala.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCredentialPayload} +import zio.{IO, ZIO, ZLayer, URLayer} + +import java.time.Instant +import java.util.UUID + +class PresentationServiceNotifier( + svc: PresentationService, + eventNotificationService: EventNotificationService +) extends PresentationService { + + override def createPresentationRecord( + pairwiseVerifierDID: DidId, + pairwiseProverDID: DidId, + thid: DidCommID, + connectionId: Option[String], + proofTypes: Seq[ProofType], + options: Option[Options] + ): IO[PresentationError, PresentationRecord] = + notifyOnSuccess( + svc.createPresentationRecord(pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, proofTypes, options) + ) + + override def markRequestPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.markRequestPresentationSent(recordId)) + + override def receiveRequestPresentation( + connectionId: Option[String], + request: RequestPresentation + ): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.receiveRequestPresentation(connectionId, request)) + + override def markRequestPresentationRejected( + recordId: DidCommID + ): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.markRequestPresentationRejected(recordId)) + + override def acceptRequestPresentation( + recordId: DidCommID, + credentialsToUse: Seq[String] + ): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.acceptRequestPresentation(recordId, credentialsToUse)) + + override def rejectRequestPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.rejectRequestPresentation(recordId)) + + override def markPresentationGenerated( + recordId: DidCommID, + presentation: Presentation + ): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.markPresentationGenerated(recordId, presentation)) + + override def markPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.markPresentationSent(recordId)) + + override def receivePresentation(presentation: Presentation): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.receivePresentation(presentation)) + + override def markPresentationVerified(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.markPresentationVerified(recordId)) + + override def markPresentationVerificationFailed( + recordId: DidCommID + ): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.markPresentationVerificationFailed(recordId)) + + override def markPresentationAccepted(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.markPresentationAccepted(recordId)) + + override def markPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.markPresentationRejected(recordId)) + + private[this] def notifyOnSuccess(effect: IO[PresentationError, PresentationRecord]) = + for { + record <- effect + _ <- notify(record) + } yield record + + private[this] def notify(record: PresentationRecord) = { + val result = for { + producer <- eventNotificationService.producer[PresentationRecord]("Presentation") + _ <- producer.send(Event(record)) + } yield () + result.catchAll(e => ZIO.logError(s"Notification service error: $e")) + } + + override def extractIdFromCredential(credential: W3cCredentialPayload): Option[UUID] = + svc.extractIdFromCredential(credential) + + override def getPresentationRecords(): IO[PresentationError, Seq[PresentationRecord]] = svc.getPresentationRecords() + + override def createPresentationPayloadFromRecord( + record: DidCommID, + issuer: Issuer, + issuanceDate: Instant + ): IO[PresentationError, PresentationPayload] = svc.createPresentationPayloadFromRecord(record, issuer, issuanceDate) + + override def getPresentationRecordsByStates( + ignoreWithZeroRetries: Boolean, + limit: Int, + state: PresentationRecord.ProtocolState* + ): IO[PresentationError, Seq[PresentationRecord]] = + svc.getPresentationRecordsByStates(ignoreWithZeroRetries, limit, state: _*) + + override def getPresentationRecord(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = + svc.getPresentationRecord(recordId) + + override def receiveProposePresentation(request: ProposePresentation): IO[PresentationError, PresentationRecord] = + svc.receiveProposePresentation((request)) + + override def acceptProposePresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + svc.acceptPresentation(recordId) + + override def acceptPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + svc.acceptPresentation(recordId) + + override def rejectPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + svc.rejectPresentation(recordId) + + override def markProposePresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + svc.markProposePresentationSent(recordId) + + override def reportProcessingFailure( + recordId: DidCommID, + failReason: Option[_root_.java.lang.String] + ): IO[PresentationError, Unit] = svc.reportProcessingFailure(recordId, failReason) +} + +object PresentationServiceNotifier { + val layer: URLayer[EventNotificationService & PresentationService, PresentationService] = + ZLayer.fromFunction(PresentationServiceNotifier(_, _)) +} diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala deleted file mode 100644 index cd58a4d71e..0000000000 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceWithEventNotificationImpl.scala +++ /dev/null @@ -1,95 +0,0 @@ -package io.iohk.atala.pollux.core.service - -import io.iohk.atala.event.notification.{Event, EventNotificationService} -import io.iohk.atala.mercury.model.DidId -import io.iohk.atala.mercury.protocol.presentproof.{Presentation, ProofType, RequestPresentation} -import io.iohk.atala.pollux.core.model.error.PresentationError -import io.iohk.atala.pollux.core.model.presentation.Options -import io.iohk.atala.pollux.core.model.{DidCommID, PresentationRecord} -import io.iohk.atala.pollux.core.repository.{CredentialRepository, PresentationRepository} -import zio.{IO, Task, URLayer, ZIO, ZLayer} - -class PresentationServiceWithEventNotificationImpl( - presentationRepository: PresentationRepository[Task], - credentialRepository: CredentialRepository[Task], - eventNotificationService: EventNotificationService -) extends PresentationServiceImpl(presentationRepository, credentialRepository) { - override def createPresentationRecord( - pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, - thid: DidCommID, - connectionId: Option[String], - proofTypes: Seq[ProofType], - options: Option[Options] - ): IO[PresentationError, PresentationRecord] = - notifyOnSuccess( - super.createPresentationRecord(pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, proofTypes, options) - ) - - override def markRequestPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(super.markRequestPresentationSent(recordId)) - - override def receiveRequestPresentation( - connectionId: Option[_root_.java.lang.String], - request: RequestPresentation - ): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(super.receiveRequestPresentation(connectionId, request)) - - override def markRequestPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(super.markRequestPresentationRejected(recordId)) - - override def acceptRequestPresentation( - recordId: DidCommID, - credentialsToUse: Seq[_root_.java.lang.String] - ): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(super.acceptRequestPresentation(recordId, credentialsToUse)) - - override def markPresentationGenerated( - recordId: DidCommID, - presentation: Presentation - ): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(super.markPresentationGenerated(recordId, presentation)) - - override def markPresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(super.markPresentationSent(recordId)) - - override def receivePresentation(presentation: Presentation): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(super.receivePresentation(presentation)) - - override def markPresentationVerified(recordId: DidCommID): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(super.markPresentationVerified(recordId)) - - override def markPresentationVerificationFailed( - recordId: DidCommID - ): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(super.markPresentationVerificationFailed(recordId)) - - override def markPresentationAccepted(recordId: DidCommID): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(super.markPresentationAccepted(recordId)) - - override def markPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(super.markPresentationRejected(recordId)) - - private[this] def notifyOnSuccess(effect: IO[PresentationError, PresentationRecord]) = - for { - record <- effect - _ <- notify(record) - } yield record - - private[this] def notify(record: PresentationRecord) = { - val result = for { - producer <- eventNotificationService.producer[PresentationRecord]("Presentation") - _ <- producer.send(Event(record)) - } yield () - result.catchAll(e => ZIO.logError(s"Notification service error: $e")) - } -} - -object PresentationServiceWithEventNotificationImpl { - val layer: URLayer[ - PresentationRepository[Task] & CredentialRepository[Task] & EventNotificationService, - PresentationService - ] = - ZLayer.fromFunction(PresentationServiceWithEventNotificationImpl(_, _, _)) - -} diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala new file mode 100644 index 0000000000..0bbd4ef3be --- /dev/null +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala @@ -0,0 +1,195 @@ +package io.iohk.atala.pollux.core.service + +import io.iohk.atala.event.notification.{EventNotificationService, EventNotificationServiceImpl} +import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.mercury.protocol.presentproof.{Presentation, RequestPresentation} +import io.iohk.atala.pollux.core.model.PresentationRecord.ProtocolState +import io.iohk.atala.pollux.core.model.{DidCommID, PresentationRecord} +import zio.mock.Expectation +import zio.test.{Assertion, Spec, TestEnvironment, ZIOSpecDefault, assertTrue} +import zio.{Scope, ZIO, ZLayer} + +import java.time.Instant + +object PresentationServiceNotifierSpec extends ZIOSpecDefault with PresentationServiceSpecHelper { + + private val record = PresentationRecord( + DidCommID(""), + Instant.now(), + None, + DidCommID(""), + None, + None, + PresentationRecord.Role.Verifier, + DidId(""), + ProtocolState.RequestPending, + None, + None, + None, + None, + 5, + None, + None + ) + + private val verifierHappyFlowExpectations = + MockPresentationService.CreatePresentationRecord( + assertion = Assertion.anything, + result = Expectation.value(record) + ) ++ + MockPresentationService.MarkRequestPresentationSent( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationSent)) + ) ++ + MockPresentationService.ReceivePresentation( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationReceived)) + ) ++ + MockPresentationService.MarkPresentationVerified( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationVerified)) + ) ++ + MockPresentationService.MarkPresentationAccepted( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationAccepted)) + ) + + private val verifierPresentationVerificationFailedExpectations = + MockPresentationService.MarkPresentationVerificationFailed( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationVerificationFailed)) + ) + + private val verifierRejectPresentationExpectations = + MockPresentationService.MarkPresentationRejected( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationRejected)) + ) + + private val proverHappyFlowExpectations = + MockPresentationService.ReceiveRequestPresentation( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.RequestReceived)) + ) ++ + MockPresentationService.AcceptRequestPresentation( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationAccepted)) + ) ++ + MockPresentationService.MarkPresentationGenerated( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationGenerated)) + ) ++ + MockPresentationService.MarkPresentationSent( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationSent)) + ) + + private val proverRejectPresentationRequestExpectations = + MockPresentationService.RejectRequestPresentation( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.RequestRejected)) + ) + + override def spec: Spec[TestEnvironment with Scope, Any] = + suite("PresentationServiceWithEventNotificationImpl")( + test("Happy flow generates relevant events on the verifier side") { + for { + svc <- ZIO.service[PresentationService] + ens <- ZIO.service[EventNotificationService] + + record <- svc.createPresentationRecord(DidId(""), DidId(""), DidCommID(""), None, Seq.empty, None) + _ <- svc.markRequestPresentationSent(record.id) + _ <- svc.receivePresentation(presentation(record.thid.value)) + _ <- svc.markPresentationVerified(record.id) + _ <- svc.markPresentationAccepted(record.id) + + consumer <- ens.consumer[PresentationRecord]("Presentation") + events <- consumer.poll(50) + } yield { + assertTrue(events.size == 5) && + assertTrue(events.head.data.protocolState == ProtocolState.RequestPending) && + assertTrue(events(1).data.protocolState == ProtocolState.RequestSent) + assertTrue(events(2).data.protocolState == ProtocolState.PresentationReceived) && + assertTrue(events(3).data.protocolState == ProtocolState.PresentationVerified) && + assertTrue(events(4).data.protocolState == ProtocolState.PresentationAccepted) + } + }.provide( + ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer, + verifierHappyFlowExpectations.toLayer >>> PresentationServiceNotifier.layer + ), + test("Generates relevant events on presentation verification failed") { + for { + svc <- ZIO.service[PresentationService] + ens <- ZIO.service[EventNotificationService] + + _ <- svc.markPresentationVerificationFailed(DidCommID()) + + consumer <- ens.consumer[PresentationRecord]("Presentation") + events <- consumer.poll(50) + } yield { + assertTrue(events.size == 1) && + assertTrue(events.head.data.protocolState == ProtocolState.PresentationVerificationFailed) + } + }.provide( + ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer, + verifierPresentationVerificationFailedExpectations.toLayer >>> PresentationServiceNotifier.layer + ), + test("Generates relevant events on presentation rejected") { + for { + svc <- ZIO.service[PresentationService] + ens <- ZIO.service[EventNotificationService] + + _ <- svc.markPresentationRejected(DidCommID()) + + consumer <- ens.consumer[PresentationRecord]("Presentation") + events <- consumer.poll(50) + } yield { + assertTrue(events.size == 1) && + assertTrue(events.head.data.protocolState == ProtocolState.PresentationRejected) + } + }.provide( + ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer, + verifierRejectPresentationExpectations.toLayer >>> PresentationServiceNotifier.layer + ), + test("Happy flow generates relevant events on the prover side") { + for { + svc <- ZIO.service[PresentationService] + ens <- ZIO.service[EventNotificationService] + + _ <- svc.receiveRequestPresentation(None, requestPresentation) + _ <- svc.acceptRequestPresentation(record.id, Seq.empty) + _ <- svc.markPresentationGenerated(record.id, presentation(record.thid.value)) + _ <- svc.markPresentationSent(record.id) + + consumer <- ens.consumer[PresentationRecord]("Presentation") + events <- consumer.poll(50) + } yield { + assertTrue(events.size == 4) && + assertTrue(events.head.data.protocolState == ProtocolState.RequestReceived) && + assertTrue(events(1).data.protocolState == ProtocolState.PresentationPending) + assertTrue(events(2).data.protocolState == ProtocolState.PresentationGenerated) && + assertTrue(events(3).data.protocolState == ProtocolState.PresentationSent) + } + }.provide( + ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer, + proverHappyFlowExpectations.toLayer >>> PresentationServiceNotifier.layer + ), + test("Happy flow generates relevant events on the prover side") { + for { + svc <- ZIO.service[PresentationService] + ens <- ZIO.service[EventNotificationService] + + _ <- svc.rejectRequestPresentation(record.id) + + consumer <- ens.consumer[PresentationRecord]("Presentation") + events <- consumer.poll(50) + } yield { + assertTrue(events.size == 1) && + assertTrue(events.head.data.protocolState == ProtocolState.RequestRejected) + } + }.provide( + ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer, + proverRejectPresentationRequestExpectations.toLayer >>> PresentationServiceNotifier.layer + ) + ) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 8b41046d47..939adaf14f 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -135,7 +135,7 @@ object MainApp extends ZIOAppDefault { CredentialServiceWithEventNotificationImpl.layer, DIDServiceImpl.layer, ManagedDIDServiceWithEventNotificationImpl.layer, - PresentationServiceWithEventNotificationImpl.layer, + PresentationServiceImpl.layer >>> PresentationServiceNotifier.layer, VerificationPolicyServiceImpl.layer, // grpc GrpcModule.irisStubLayer, From ffd21cb9828914c67285f6252cc698d265e85d4d Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Wed, 5 Jul 2023 11:39:16 +0200 Subject: [PATCH 38/56] test(connect): refactor notifier unit tests to use ZIO mock for ConnectionService --- build.sbt | 2 +- ....scala => ConnectionServiceNotifier.scala} | 47 +++++++---- .../core/service/MockConnectionService.scala | 78 +++++++++++++++++++ ...la => ConnectionServiceNotifierSpec.scala} | 67 ++++++++++++++-- .../io/iohk/atala/agent/server/Main.scala | 4 +- 5 files changed, 172 insertions(+), 26 deletions(-) rename connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/{ConnectionServiceWithEventNotificationImpl.scala => ConnectionServiceNotifier.scala} (56%) create mode 100644 connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/MockConnectionService.scala rename connect/lib/core/src/test/scala/io/iohk/atala/connect/core/service/{ConnectionServiceWithEventNotificationImplSpec.scala => ConnectionServiceNotifierSpec.scala} (60%) diff --git a/build.sbt b/build.sbt index a734514468..49cf7be680 100644 --- a/build.sbt +++ b/build.sbt @@ -147,7 +147,7 @@ lazy val D_Connect = new { // Dependency Modules private lazy val baseDependencies: Seq[ModuleID] = - Seq(D.zio, D.zioTest, D.zioTestSbt, D.zioTestMagnolia, D.testcontainersPostgres, logback) + Seq(D.zio, D.zioTest, D.zioTestSbt, D.zioTestMagnolia, D.zioMock, D.testcontainersPostgres, logback) // Project Dependencies lazy val coreDependencies: Seq[ModuleID] = diff --git a/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImpl.scala b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifier.scala similarity index 56% rename from connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImpl.scala rename to connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifier.scala index 5afbc008ca..a850585985 100644 --- a/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImpl.scala +++ b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifier.scala @@ -2,47 +2,47 @@ package io.iohk.atala.connect.core.service import io.iohk.atala.connect.core.model.ConnectionRecord import io.iohk.atala.connect.core.model.error.ConnectionServiceError -import io.iohk.atala.connect.core.repository.ConnectionRepository import io.iohk.atala.event.notification.{Event, EventNotificationService} import io.iohk.atala.mercury.model.DidId import io.iohk.atala.mercury.protocol.connection.{ConnectionRequest, ConnectionResponse} -import zio.{IO, Task, URLayer, ZIO, ZLayer} +import zio.{IO, URLayer, ZIO, ZLayer} import java.util.UUID -class ConnectionServiceWithEventNotificationImpl( - connectionRepository: ConnectionRepository[Task], +class ConnectionServiceNotifier( + svc: ConnectionService, eventNotificationService: EventNotificationService -) extends ConnectionServiceImpl(connectionRepository) { +) extends ConnectionService { + override def createConnectionInvitation( label: Option[String], pairwiseDID: DidId ): IO[ConnectionServiceError, ConnectionRecord] = - notifyOnSuccess(super.createConnectionInvitation(label, pairwiseDID)) + notifyOnSuccess(svc.createConnectionInvitation(label, pairwiseDID)) override def receiveConnectionInvitation(invitation: String): IO[ConnectionServiceError, ConnectionRecord] = - notifyOnSuccess(super.receiveConnectionInvitation(invitation)) + notifyOnSuccess(svc.receiveConnectionInvitation(invitation)) override def acceptConnectionInvitation( recordId: UUID, pairwiseDid: DidId ): IO[ConnectionServiceError, ConnectionRecord] = - notifyOnSuccess(super.acceptConnectionInvitation(recordId, pairwiseDid)) + notifyOnSuccess(svc.acceptConnectionInvitation(recordId, pairwiseDid)) override def markConnectionRequestSent(recordId: UUID): IO[ConnectionServiceError, ConnectionRecord] = - notifyOnSuccess(super.markConnectionRequestSent(recordId)) + notifyOnSuccess(svc.markConnectionRequestSent(recordId)) override def receiveConnectionRequest(request: ConnectionRequest): IO[ConnectionServiceError, ConnectionRecord] = - notifyOnSuccess(super.receiveConnectionRequest(request)) + notifyOnSuccess(svc.receiveConnectionRequest(request)) override def acceptConnectionRequest(recordId: UUID): IO[ConnectionServiceError, ConnectionRecord] = - notifyOnSuccess(super.acceptConnectionRequest(recordId)) + notifyOnSuccess(svc.acceptConnectionRequest(recordId)) override def markConnectionResponseSent(recordId: UUID): IO[ConnectionServiceError, ConnectionRecord] = - notifyOnSuccess(super.markConnectionResponseSent(recordId)) + notifyOnSuccess(svc.markConnectionResponseSent(recordId)) override def receiveConnectionResponse(response: ConnectionResponse): IO[ConnectionServiceError, ConnectionRecord] = - notifyOnSuccess(super.receiveConnectionResponse(response)) + notifyOnSuccess(svc.receiveConnectionResponse(response)) private[this] def notifyOnSuccess(effect: IO[ConnectionServiceError, ConnectionRecord]) = for { @@ -57,9 +57,24 @@ class ConnectionServiceWithEventNotificationImpl( } yield () result.catchAll(e => ZIO.logError(s"Notification service error: $e")) } + + override def getConnectionRecord(recordId: UUID): IO[ConnectionServiceError, Option[ConnectionRecord]] = ??? + + override def deleteConnectionRecord(recordId: UUID): IO[ConnectionServiceError, Int] = ??? + + override def reportProcessingFailure(recordId: UUID, failReason: Option[String]): IO[ConnectionServiceError, Unit] = + ??? + + override def getConnectionRecords(): IO[ConnectionServiceError, Seq[ConnectionRecord]] = ??? + + override def getConnectionRecordsByStates( + ignoreWithZeroRetries: Boolean, + limit: Int, + states: ConnectionRecord.ProtocolState* + ): IO[ConnectionServiceError, Seq[ConnectionRecord]] = ??? } -object ConnectionServiceWithEventNotificationImpl { - val layer: URLayer[ConnectionRepository[Task] with EventNotificationService, ConnectionService] = - ZLayer.fromFunction(ConnectionServiceWithEventNotificationImpl(_, _)) +object ConnectionServiceNotifier { + val layer: URLayer[ConnectionService & EventNotificationService, ConnectionService] = + ZLayer.fromFunction(ConnectionServiceNotifier(_, _)) } diff --git a/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/MockConnectionService.scala b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/MockConnectionService.scala new file mode 100644 index 0000000000..a8f3680b07 --- /dev/null +++ b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/MockConnectionService.scala @@ -0,0 +1,78 @@ +package io.iohk.atala.connect.core.service + +import io.iohk.atala.connect.core.model.ConnectionRecord +import io.iohk.atala.connect.core.model.error.ConnectionServiceError +import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.mercury.protocol.connection.{ConnectionRequest, ConnectionResponse} +import zio.mock.{Mock, Proxy} +import zio.{IO, URLayer, ZIO, ZLayer, mock} + +import java.util.UUID + +object MockConnectionService extends Mock[ConnectionService] { + + object CreateConnectionInvitation extends Effect[(Option[String], DidId), ConnectionServiceError, ConnectionRecord] + object ReceiveConnectionInvitation extends Effect[String, ConnectionServiceError, ConnectionRecord] + object AcceptConnectionInvitation extends Effect[(UUID, DidId), ConnectionServiceError, ConnectionRecord] + object MarkConnectionRequestSent extends Effect[UUID, ConnectionServiceError, ConnectionRecord] + object ReceiveConnectionRequest extends Effect[ConnectionRequest, ConnectionServiceError, ConnectionRecord] + object AcceptConnectionRequest extends Effect[UUID, ConnectionServiceError, ConnectionRecord] + object MarkConnectionResponseSent extends Effect[UUID, ConnectionServiceError, ConnectionRecord] + object ReceiveConnectionResponse extends Effect[ConnectionResponse, ConnectionServiceError, ConnectionRecord] + + override val compose: URLayer[mock.Proxy, ConnectionService] = ZLayer { + for { + proxy <- ZIO.service[Proxy] + } yield new ConnectionService { + override def createConnectionInvitation( + label: Option[String], + pairwiseDID: DidId + ): IO[ConnectionServiceError, ConnectionRecord] = + proxy(CreateConnectionInvitation, label, pairwiseDID) + + override def receiveConnectionInvitation(invitation: String): IO[ConnectionServiceError, ConnectionRecord] = + proxy(ReceiveConnectionInvitation, invitation) + + override def acceptConnectionInvitation( + recordId: UUID, + pairwiseDid: DidId + ): IO[ConnectionServiceError, ConnectionRecord] = + proxy(AcceptConnectionInvitation, recordId, pairwiseDid) + + override def markConnectionRequestSent(recordId: UUID): IO[ConnectionServiceError, ConnectionRecord] = + proxy(MarkConnectionRequestSent, recordId) + + override def receiveConnectionRequest(request: ConnectionRequest): IO[ConnectionServiceError, ConnectionRecord] = + proxy(ReceiveConnectionRequest, request) + + override def acceptConnectionRequest(recordId: UUID): IO[ConnectionServiceError, ConnectionRecord] = + proxy(AcceptConnectionRequest, recordId) + + override def markConnectionResponseSent(recordId: UUID): IO[ConnectionServiceError, ConnectionRecord] = + proxy(MarkConnectionResponseSent, recordId) + + override def receiveConnectionResponse( + response: ConnectionResponse + ): IO[ConnectionServiceError, ConnectionRecord] = + proxy(ReceiveConnectionResponse, response) + + override def getConnectionRecords(): IO[ConnectionServiceError, Seq[ConnectionRecord]] = ??? + + override def getConnectionRecordsByStates( + ignoreWithZeroRetries: Boolean, + limit: Int, + states: ConnectionRecord.ProtocolState* + ): IO[ConnectionServiceError, Seq[ConnectionRecord]] = ??? + + /** Get the ConnectionRecord by the record id. If the record is id is not found the value None will be return */ + override def getConnectionRecord(recordId: UUID): IO[ConnectionServiceError, Option[ConnectionRecord]] = ??? + + override def deleteConnectionRecord(recordId: UUID): IO[ConnectionServiceError, Int] = ??? + + override def reportProcessingFailure( + recordId: UUID, + failReason: Option[String] + ): IO[ConnectionServiceError, Unit] = ??? + } + } +} diff --git a/connect/lib/core/src/test/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImplSpec.scala b/connect/lib/core/src/test/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifierSpec.scala similarity index 60% rename from connect/lib/core/src/test/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImplSpec.scala rename to connect/lib/core/src/test/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifierSpec.scala index abf7e593e1..248f32b0f9 100644 --- a/connect/lib/core/src/test/scala/io/iohk/atala/connect/core/service/ConnectionServiceWithEventNotificationImplSpec.scala +++ b/connect/lib/core/src/test/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifierSpec.scala @@ -6,11 +6,62 @@ import io.iohk.atala.connect.core.repository.ConnectionRepositoryInMemory import io.iohk.atala.event.notification.* import io.iohk.atala.mercury.model.DidId import io.iohk.atala.mercury.protocol.connection.{ConnectionRequest, ConnectionResponse} +import io.iohk.atala.mercury.protocol.invitation.v2.Invitation import zio.* import zio.ZIO.* +import zio.mock.Expectation import zio.test.* -object ConnectionServiceWithEventNotificationImplSpec extends ZIOSpecDefault { +import java.time.Instant +import java.util.UUID + +object ConnectionServiceNotifierSpec extends ZIOSpecDefault { + + private val record = ConnectionRecord( + UUID.randomUUID(), + Instant.now, + None, + None, + None, + ConnectionRecord.Role.Inviter, + ProtocolState.InvitationGenerated, + Invitation(from = DidId("did:peer:INVITER"), Invitation.Body("", "", Nil)), + None, + None, + 5, + None, + None + ) + + private val inviterExpectations = + MockConnectionService.CreateConnectionInvitation( + assertion = Assertion.anything, + result = Expectation.value(record) + ) ++ MockConnectionService.ReceiveConnectionRequest( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.ConnectionRequestReceived)) + ) ++ MockConnectionService.AcceptConnectionRequest( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.ConnectionResponsePending)) + ) ++ MockConnectionService.MarkConnectionResponseSent( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.ConnectionResponseSent)) + ) + + private val inviteeExpectations = + MockConnectionService.ReceiveConnectionInvitation( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.InvitationReceived)) + ) ++ MockConnectionService.AcceptConnectionInvitation( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.ConnectionRequestPending)) + ) ++ MockConnectionService.MarkConnectionRequestSent( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.ConnectionRequestSent)) + ) ++ MockConnectionService.ReceiveConnectionResponse( + assertion = Assertion.anything, + result = Expectation.value(record.copy(protocolState = ProtocolState.ConnectionResponseReceived)) + ) override def spec: Spec[TestEnvironment with Scope, Any] = { suite("ConnectionServiceWithEventNotificationImpl")( @@ -40,7 +91,10 @@ object ConnectionServiceWithEventNotificationImplSpec extends ZIOSpecDefault { assertTrue(events(2).data.protocolState == ProtocolState.ConnectionResponsePending) && assertTrue(events(3).data.protocolState == ProtocolState.ConnectionResponseSent) } - }, + }.provide( + ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer, + inviterExpectations.toLayer >>> ConnectionServiceNotifier.layer + ), test("should send relevant events during flow execution on the invitee side") { for { inviterSvc <- ZIO @@ -75,11 +129,10 @@ object ConnectionServiceWithEventNotificationImplSpec extends ZIOSpecDefault { assertTrue(events(2).data.protocolState == ProtocolState.ConnectionRequestSent) && assertTrue(events(3).data.protocolState == ProtocolState.ConnectionResponseReceived) } - } - ).provide( - ConnectionRepositoryInMemory.layer, - ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer, - ConnectionServiceWithEventNotificationImpl.layer + }.provide( + ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer, + inviteeExpectations.toLayer >>> ConnectionServiceNotifier.layer + ) ) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 939adaf14f..96561f8dff 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -9,7 +9,7 @@ import io.iohk.atala.castor.controller.{DIDControllerImpl, DIDRegistrarControlle import io.iohk.atala.castor.core.service.DIDServiceImpl import io.iohk.atala.castor.core.util.DIDOperationValidator import io.iohk.atala.connect.controller.ConnectionControllerImpl -import io.iohk.atala.connect.core.service.ConnectionServiceWithEventNotificationImpl +import io.iohk.atala.connect.core.service.{ConnectionServiceImpl, ConnectionServiceNotifier} import io.iohk.atala.connect.sql.repository.{JdbcConnectionRepository, Migrations as ConnectMigrations} import io.iohk.atala.event.notification.EventNotificationServiceImpl import io.iohk.atala.issue.controller.IssueControllerImpl @@ -130,7 +130,7 @@ object MainApp extends ZIOAppDefault { DIDResolver.layer, HttpURIDereferencerImpl.layer, // service - ConnectionServiceWithEventNotificationImpl.layer, + ConnectionServiceImpl.layer >>> ConnectionServiceNotifier.layer, CredentialSchemaServiceImpl.layer, CredentialServiceWithEventNotificationImpl.layer, DIDServiceImpl.layer, From 4b434cd6ec27ff5919cb885359625547fbb5c883 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Wed, 5 Jul 2023 13:15:19 +0200 Subject: [PATCH 39/56] test(pollux): refactor credential service notifier unit tests to use ZIO mock for CredentialService --- .../service/ConnectionServiceNotifier.scala | 14 +- .../service/CredentialServiceNotifier.scala | 162 +++++++++++++++ ...tialServiceWithEventNotificationImpl.scala | 107 ---------- .../core/service/MockCredentialService.scala | 191 ++++++++++++++++++ .../CredentialServiceNotifierSpec.scala | 145 +++++++++++++ ...ServiceWithEventNotificationImplSpec.scala | 97 --------- .../io/iohk/atala/agent/server/Main.scala | 16 +- 7 files changed, 510 insertions(+), 222 deletions(-) create mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala delete mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala create mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockCredentialService.scala create mode 100644 pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifierSpec.scala delete mode 100644 pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImplSpec.scala diff --git a/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifier.scala b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifier.scala index a850585985..41302c07d1 100644 --- a/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifier.scala +++ b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifier.scala @@ -58,20 +58,24 @@ class ConnectionServiceNotifier( result.catchAll(e => ZIO.logError(s"Notification service error: $e")) } - override def getConnectionRecord(recordId: UUID): IO[ConnectionServiceError, Option[ConnectionRecord]] = ??? + override def getConnectionRecord(recordId: UUID): IO[ConnectionServiceError, Option[ConnectionRecord]] = + svc.getConnectionRecord(recordId) - override def deleteConnectionRecord(recordId: UUID): IO[ConnectionServiceError, Int] = ??? + override def deleteConnectionRecord(recordId: UUID): IO[ConnectionServiceError, Int] = + svc.deleteConnectionRecord(recordId) override def reportProcessingFailure(recordId: UUID, failReason: Option[String]): IO[ConnectionServiceError, Unit] = - ??? + svc.reportProcessingFailure(recordId, failReason) - override def getConnectionRecords(): IO[ConnectionServiceError, Seq[ConnectionRecord]] = ??? + override def getConnectionRecords(): IO[ConnectionServiceError, Seq[ConnectionRecord]] = + svc.getConnectionRecords() override def getConnectionRecordsByStates( ignoreWithZeroRetries: Boolean, limit: Int, states: ConnectionRecord.ProtocolState* - ): IO[ConnectionServiceError, Seq[ConnectionRecord]] = ??? + ): IO[ConnectionServiceError, Seq[ConnectionRecord]] = + svc.getConnectionRecordsByStates(ignoreWithZeroRetries, limit, states: _*) } object ConnectionServiceNotifier { diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala new file mode 100644 index 0000000000..833ef9ae0f --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala @@ -0,0 +1,162 @@ +package io.iohk.atala.pollux.core.service + +import io.circe.Json +import io.iohk.atala.castor.core.model.did.CanonicalPrismDID +import io.iohk.atala.event.notification.* +import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.mercury.protocol.issuecredential.{IssueCredential, OfferCredential, RequestCredential} +import io.iohk.atala.pollux.core.model.error.CredentialServiceError +import io.iohk.atala.pollux.core.model.{DidCommID, IssueCredentialRecord, PublishedBatchData} +import io.iohk.atala.pollux.vc.jwt.{Issuer, JWT, PresentationPayload, W3cCredentialPayload} +import io.iohk.atala.prism.crypto.MerkleInclusionProof +import zio.{IO, URLayer, ZIO, ZLayer} + +import java.time.Instant + +class CredentialServiceNotifier( + svc: CredentialService, + eventNotificationService: EventNotificationService +) extends CredentialService { + + override def createIssueCredentialRecord( + pairwiseIssuerDID: DidId, + pairwiseHolderDID: DidId, + thid: DidCommID, + maybeSchemaId: Option[_root_.java.lang.String], + claims: Json, + validityPeriod: Option[Double], + automaticIssuance: Option[Boolean], + awaitConfirmation: Option[Boolean], + issuingDID: Option[CanonicalPrismDID] + ): IO[CredentialServiceError, IssueCredentialRecord] = + notifyOnSuccess( + svc.createIssueCredentialRecord( + pairwiseIssuerDID, + pairwiseHolderDID, + thid, + maybeSchemaId, + claims, + validityPeriod, + automaticIssuance, + awaitConfirmation, + issuingDID + ) + ) + + override def markOfferSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + notifyOnSuccess(svc.markOfferSent(recordId)) + + override def receiveCredentialOffer(offer: OfferCredential): IO[CredentialServiceError, IssueCredentialRecord] = + notifyOnSuccess(svc.receiveCredentialOffer(offer)) + + override def acceptCredentialOffer( + recordId: DidCommID, + subjectId: String + ): IO[CredentialServiceError, IssueCredentialRecord] = + notifyOnSuccess(svc.acceptCredentialOffer(recordId, subjectId)) + + override def generateCredentialRequest( + recordId: DidCommID, + signedPresentation: JWT + ): IO[CredentialServiceError, IssueCredentialRecord] = + notifyOnSuccess(svc.generateCredentialRequest(recordId, signedPresentation)) + + override def markRequestSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + notifyOnSuccess(svc.markRequestSent(recordId)) + + override def receiveCredentialRequest(request: RequestCredential): IO[CredentialServiceError, IssueCredentialRecord] = + notifyOnSuccess(svc.receiveCredentialRequest(request)) + + override def acceptCredentialRequest(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + notifyOnSuccess(svc.acceptCredentialRequest(recordId)) + + override def markCredentialGenerated( + recordId: DidCommID, + issueCredential: IssueCredential + ): IO[CredentialServiceError, IssueCredentialRecord] = + notifyOnSuccess(svc.markCredentialGenerated(recordId, issueCredential)) + + override def markCredentialSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + notifyOnSuccess(svc.markCredentialSent(recordId)) + + override def receiveCredentialIssue(issue: IssueCredential): IO[CredentialServiceError, IssueCredentialRecord] = + notifyOnSuccess(svc.receiveCredentialIssue(issue)) + + private[this] def notifyOnSuccess(effect: IO[CredentialServiceError, IssueCredentialRecord]) = + for { + record <- effect + _ <- notify(record) + } yield record + + private[this] def notify(record: IssueCredentialRecord) = { + val result = for { + producer <- eventNotificationService.producer[IssueCredentialRecord]("Issue") + _ <- producer.send(Event(record)) + } yield () + result.catchAll(e => ZIO.logError(s"Notification service error: $e")) + } + + override def createPresentationPayload( + recordId: DidCommID, + subject: Issuer + ): IO[CredentialServiceError, PresentationPayload] = + svc.createPresentationPayload(recordId, subject) + + override def createCredentialPayloadFromRecord( + record: IssueCredentialRecord, + issuer: Issuer, + issuanceDate: Instant + ): IO[CredentialServiceError, W3cCredentialPayload] = + svc.createCredentialPayloadFromRecord(record, issuer, issuanceDate) + + override def publishCredentialBatch( + credentials: Seq[W3cCredentialPayload], + issuer: Issuer + ): IO[CredentialServiceError, PublishedBatchData] = + svc.publishCredentialBatch(credentials, issuer) + + override def markCredentialRecordsAsPublishQueued( + credentialsAndProofs: Seq[(W3cCredentialPayload, MerkleInclusionProof)] + ): IO[CredentialServiceError, Int] = + svc.markCredentialRecordsAsPublishQueued(credentialsAndProofs) + + override def markCredentialPublicationPending( + recordId: DidCommID + ): IO[CredentialServiceError, IssueCredentialRecord] = + svc.markCredentialPublicationPending(recordId) + + override def markCredentialPublicationQueued(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + svc.markCredentialPublicationQueued(recordId) + + override def markCredentialPublished(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + svc.markCredentialPublished(recordId) + + override def reportProcessingFailure( + recordId: DidCommID, + failReason: Option[_root_.java.lang.String] + ): IO[CredentialServiceError, Unit] = + svc.reportProcessingFailure(recordId, failReason) + + override def extractIdFromCredential(credential: W3cCredentialPayload): Option[DidCommID] = + svc.extractIdFromCredential(credential) + + override def getIssueCredentialRecord( + recordId: DidCommID + ): IO[CredentialServiceError, Option[IssueCredentialRecord]] = + svc.getIssueCredentialRecord(recordId) + + override def getIssueCredentialRecords: IO[CredentialServiceError, Seq[IssueCredentialRecord]] = + svc.getIssueCredentialRecords + + override def getIssueCredentialRecordsByStates( + ignoreWithZeroRetries: Boolean, + limit: Int, + states: IssueCredentialRecord.ProtocolState* + ): IO[CredentialServiceError, Seq[IssueCredentialRecord]] = + svc.getIssueCredentialRecordsByStates(ignoreWithZeroRetries, limit, states: _*) +} + +object CredentialServiceNotifier { + val layer: URLayer[CredentialService & EventNotificationService, CredentialServiceNotifier] = + ZLayer.fromFunction(CredentialServiceNotifier(_, _)) +} diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala deleted file mode 100644 index 1559f0db68..0000000000 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImpl.scala +++ /dev/null @@ -1,107 +0,0 @@ -package io.iohk.atala.pollux.core.service - -import io.circe.Json -import io.iohk.atala.castor.core.model.did.CanonicalPrismDID -import io.iohk.atala.event.notification.* -import io.iohk.atala.iris.proto.service.IrisServiceGrpc.IrisServiceStub -import io.iohk.atala.mercury.model.DidId -import io.iohk.atala.mercury.protocol.issuecredential.{IssueCredential, OfferCredential, RequestCredential} -import io.iohk.atala.pollux.core.model.error.CredentialServiceError -import io.iohk.atala.pollux.core.model.{DidCommID, IssueCredentialRecord} -import io.iohk.atala.pollux.core.repository.CredentialRepository -import io.iohk.atala.pollux.vc.jwt.{DidResolver, JWT} -import zio.{IO, Task, URLayer, ZIO, ZLayer} - -class CredentialServiceWithEventNotificationImpl( - irisClient: IrisServiceStub, - credentialRepository: CredentialRepository[Task], - didResolver: DidResolver, - uriDereferencer: URIDereferencer, - eventNotificationService: EventNotificationService -) extends CredentialServiceImpl(irisClient, credentialRepository, didResolver, uriDereferencer) { - - override def createIssueCredentialRecord( - pairwiseIssuerDID: DidId, - pairwiseHolderDID: DidId, - thid: DidCommID, - maybeSchemaId: Option[_root_.java.lang.String], - claims: Json, - validityPeriod: Option[Double], - automaticIssuance: Option[Boolean], - awaitConfirmation: Option[Boolean], - issuingDID: Option[CanonicalPrismDID] - ): IO[CredentialServiceError, IssueCredentialRecord] = - notifyOnSuccess( - super.createIssueCredentialRecord( - pairwiseIssuerDID, - pairwiseHolderDID, - thid, - maybeSchemaId, - claims, - validityPeriod, - automaticIssuance, - awaitConfirmation, - issuingDID - ) - ) - - override def markOfferSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = - notifyOnSuccess(super.markOfferSent(recordId)) - - override def receiveCredentialOffer(offer: OfferCredential): IO[CredentialServiceError, IssueCredentialRecord] = - notifyOnSuccess(super.receiveCredentialOffer(offer)) - - override def acceptCredentialOffer( - recordId: DidCommID, - subjectId: String - ): IO[CredentialServiceError, IssueCredentialRecord] = - notifyOnSuccess(super.acceptCredentialOffer(recordId, subjectId)) - - override def generateCredentialRequest( - recordId: DidCommID, - signedPresentation: JWT - ): IO[CredentialServiceError, IssueCredentialRecord] = - notifyOnSuccess(super.generateCredentialRequest(recordId, signedPresentation)) - - override def markRequestSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = - notifyOnSuccess(super.markRequestSent(recordId)) - - override def receiveCredentialRequest(request: RequestCredential): IO[CredentialServiceError, IssueCredentialRecord] = - notifyOnSuccess(super.receiveCredentialRequest(request)) - - override def acceptCredentialRequest(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = - notifyOnSuccess(super.acceptCredentialRequest(recordId)) - - override def markCredentialGenerated( - recordId: DidCommID, - issueCredential: IssueCredential - ): IO[CredentialServiceError, IssueCredentialRecord] = - notifyOnSuccess(super.markCredentialGenerated(recordId, issueCredential)) - - override def markCredentialSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = - notifyOnSuccess(super.markCredentialSent(recordId)) - - override def receiveCredentialIssue(issue: IssueCredential): IO[CredentialServiceError, IssueCredentialRecord] = - notifyOnSuccess(super.receiveCredentialIssue(issue)) - - private[this] def notifyOnSuccess(effect: IO[CredentialServiceError, IssueCredentialRecord]) = - for { - record <- effect - _ <- notify(record) - } yield record - - private[this] def notify(record: IssueCredentialRecord) = { - val result = for { - producer <- eventNotificationService.producer[IssueCredentialRecord]("Issue") - _ <- producer.send(Event(record)) - } yield () - result.catchAll(e => ZIO.logError(s"Notification service error: $e")) - } -} - -object CredentialServiceWithEventNotificationImpl { - val layer: URLayer[ - IrisServiceStub with CredentialRepository[Task] with DidResolver with URIDereferencer with EventNotificationService, - CredentialServiceWithEventNotificationImpl - ] = ZLayer.fromFunction(CredentialServiceWithEventNotificationImpl(_, _, _, _, _)) -} diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockCredentialService.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockCredentialService.scala new file mode 100644 index 0000000000..5bf22b4235 --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockCredentialService.scala @@ -0,0 +1,191 @@ +package io.iohk.atala.pollux.core.service + +import io.circe.Json +import io.iohk.atala.castor.core.model.did.CanonicalPrismDID +import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.mercury.protocol.issuecredential.{IssueCredential, OfferCredential, RequestCredential} +import io.iohk.atala.pollux.core.model.error.CredentialServiceError +import io.iohk.atala.pollux.core.model.{DidCommID, IssueCredentialRecord, PublishedBatchData} +import io.iohk.atala.pollux.vc.jwt.{Issuer, JWT, PresentationPayload, W3cCredentialPayload} +import io.iohk.atala.prism.crypto.MerkleInclusionProof +import zio.mock.{Mock, Proxy} +import zio.{IO, URLayer, ZIO, ZLayer, mock} + +import java.time.Instant + +object MockCredentialService extends Mock[CredentialService] { + + object CreateIssueCredentialRecord + extends Effect[ + ( + DidId, + DidId, + DidCommID, + Option[String], + Json, + Option[Double], + Option[Boolean], + Option[Boolean], + Option[CanonicalPrismDID] + ), + CredentialServiceError, + IssueCredentialRecord + ] + + object ReceiveCredentialOffer extends Effect[OfferCredential, CredentialServiceError, IssueCredentialRecord] + object AcceptCredentialOffer extends Effect[(DidCommID, String), CredentialServiceError, IssueCredentialRecord] + object CreatePresentationPayload extends Effect[(DidCommID, Issuer), CredentialServiceError, PresentationPayload] + object GenerateCredentialRequest extends Effect[(DidCommID, JWT), CredentialServiceError, IssueCredentialRecord] + object ReceiveCredentialRequest extends Effect[RequestCredential, CredentialServiceError, IssueCredentialRecord] + object AcceptCredentialRequest extends Effect[DidCommID, CredentialServiceError, IssueCredentialRecord] + object CreateCredentialPayloadFromRecord + extends Effect[(IssueCredentialRecord, Issuer, Instant), CredentialServiceError, W3cCredentialPayload] + object PublishCredentialBatch + extends Effect[(Seq[W3cCredentialPayload], Issuer), CredentialServiceError, PublishedBatchData] + object MarkCredentialRecordsAsPublishQueued + extends Effect[Seq[(W3cCredentialPayload, MerkleInclusionProof)], CredentialServiceError, Int] + object ReceiveCredentialIssue extends Effect[IssueCredential, CredentialServiceError, IssueCredentialRecord] + object MarkOfferSent extends Effect[DidCommID, CredentialServiceError, IssueCredentialRecord] + object MarkRequestSent extends Effect[DidCommID, CredentialServiceError, IssueCredentialRecord] + object MarkCredentialGenerated + extends Effect[(DidCommID, IssueCredential), CredentialServiceError, IssueCredentialRecord] + object MarkCredentialSent extends Effect[DidCommID, CredentialServiceError, IssueCredentialRecord] + object MarkCredentialPublicationPending extends Effect[DidCommID, CredentialServiceError, IssueCredentialRecord] + object MarkCredentialPublicationQueued extends Effect[DidCommID, CredentialServiceError, IssueCredentialRecord] + object MarkCredentialPublished extends Effect[DidCommID, CredentialServiceError, IssueCredentialRecord] + object ReportProcessingFailure extends Effect[(DidCommID, Option[String]), CredentialServiceError, Unit] + + override val compose: URLayer[mock.Proxy, CredentialService] = ZLayer { + for { + proxy <- ZIO.service[Proxy] + } yield new CredentialService { + + override def createIssueCredentialRecord( + pairwiseIssuerDID: DidId, + pairwiseHolderDID: DidId, + thid: DidCommID, + maybeSchemaId: Option[String], + claims: Json, + validityPeriod: Option[Double], + automaticIssuance: Option[Boolean], + awaitConfirmation: Option[Boolean], + issuingDID: Option[CanonicalPrismDID] + ): IO[CredentialServiceError, IssueCredentialRecord] = + proxy( + CreateIssueCredentialRecord, + pairwiseIssuerDID, + pairwiseHolderDID, + thid, + maybeSchemaId, + claims, + validityPeriod, + automaticIssuance, + awaitConfirmation, + issuingDID + ) + + override def receiveCredentialOffer(offer: OfferCredential): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(ReceiveCredentialOffer, offer) + + override def acceptCredentialOffer( + recordId: DidCommID, + subjectId: String + ): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(AcceptCredentialOffer, recordId, subjectId) + + override def createPresentationPayload( + recordId: DidCommID, + subject: Issuer + ): IO[CredentialServiceError, PresentationPayload] = + proxy(CreatePresentationPayload, recordId, subject) + + override def generateCredentialRequest( + recordId: DidCommID, + signedPresentation: JWT + ): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(GenerateCredentialRequest, recordId, signedPresentation) + + override def receiveCredentialRequest( + request: RequestCredential + ): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(ReceiveCredentialRequest, request) + + override def acceptCredentialRequest(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(AcceptCredentialRequest, recordId) + + override def createCredentialPayloadFromRecord( + record: IssueCredentialRecord, + issuer: Issuer, + issuanceDate: Instant + ): IO[CredentialServiceError, W3cCredentialPayload] = + proxy(CreateCredentialPayloadFromRecord, record, issuer, issuanceDate) + + override def publishCredentialBatch( + credentials: Seq[W3cCredentialPayload], + issuer: Issuer + ): IO[CredentialServiceError, PublishedBatchData] = + proxy(PublishCredentialBatch, credentials, issuer) + + override def markCredentialRecordsAsPublishQueued( + credentialsAndProofs: Seq[(W3cCredentialPayload, MerkleInclusionProof)] + ): IO[CredentialServiceError, Int] = + proxy(MarkCredentialRecordsAsPublishQueued, credentialsAndProofs) + + override def receiveCredentialIssue(issue: IssueCredential): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(ReceiveCredentialIssue, issue) + + override def markOfferSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(MarkOfferSent, recordId) + + override def markRequestSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(MarkRequestSent, recordId) + + override def markCredentialGenerated( + recordId: DidCommID, + issueCredential: IssueCredential + ): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(MarkCredentialGenerated, recordId, issueCredential) + + override def markCredentialSent(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(MarkCredentialSent, recordId) + + override def markCredentialPublicationPending( + recordId: DidCommID + ): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(MarkCredentialPublicationPending, recordId) + + override def markCredentialPublicationQueued( + recordId: DidCommID + ): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(MarkCredentialPublicationQueued, recordId) + + override def markCredentialPublished(recordId: DidCommID): IO[CredentialServiceError, IssueCredentialRecord] = + proxy(MarkCredentialPublished, recordId) + + override def reportProcessingFailure( + recordId: DidCommID, + failReason: Option[String] + ): IO[CredentialServiceError, Unit] = + proxy(ReportProcessingFailure, recordId, failReason) + + override def extractIdFromCredential(credential: W3cCredentialPayload): Option[DidCommID] = + ??? + + override def getIssueCredentialRecords: IO[CredentialServiceError, Seq[IssueCredentialRecord]] = + ??? + + override def getIssueCredentialRecordsByStates( + ignoreWithZeroRetries: Boolean, + limit: Int, + states: IssueCredentialRecord.ProtocolState* + ): IO[CredentialServiceError, Seq[IssueCredentialRecord]] = + ??? + + override def getIssueCredentialRecord( + recordId: DidCommID + ): IO[CredentialServiceError, Option[IssueCredentialRecord]] = + ??? + + } + } +} diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifierSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifierSpec.scala new file mode 100644 index 0000000000..482538290d --- /dev/null +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifierSpec.scala @@ -0,0 +1,145 @@ +package io.iohk.atala.pollux.core.service + +import io.iohk.atala.event.notification.{EventNotificationService, EventNotificationServiceImpl} +import io.iohk.atala.mercury.protocol.issuecredential.* +import io.iohk.atala.pollux.core.model.* +import io.iohk.atala.pollux.core.model.IssueCredentialRecord.ProtocolState +import io.iohk.atala.pollux.core.model.error.CredentialServiceError +import io.iohk.atala.pollux.vc.jwt.JWT +import zio.* +import zio.mock.Expectation +import zio.test.{Assertion, *} + +import java.time.Instant + +object CredentialServiceNotifierSpec extends ZIOSpecDefault with CredentialServiceSpecHelper { + + private val issueCredentialRecord = IssueCredentialRecord( + DidCommID(), + Instant.now, + None, + DidCommID(), + None, + IssueCredentialRecord.Role.Issuer, + None, + None, + None, + None, + ProtocolState.OfferPending, + None, + None, + None, + None, + None, + None, + 5, + None, + None + ) + + private val issuerExpectations = + MockCredentialService.CreateIssueCredentialRecord( + assertion = Assertion.anything, + result = Expectation.value(issueCredentialRecord) + ) ++ + MockCredentialService.MarkOfferSent( + assertion = Assertion.anything, + result = Expectation.value(issueCredentialRecord.copy(protocolState = ProtocolState.OfferSent)) + ) ++ + MockCredentialService.ReceiveCredentialRequest( + assertion = Assertion.anything, + result = Expectation.value(issueCredentialRecord.copy(protocolState = ProtocolState.RequestReceived)) + ) ++ + MockCredentialService.AcceptCredentialRequest( + assertion = Assertion.anything, + result = Expectation.value(issueCredentialRecord.copy(protocolState = ProtocolState.CredentialPending)) + ) ++ + MockCredentialService.MarkCredentialGenerated( + assertion = Assertion.anything, + result = Expectation.value(issueCredentialRecord.copy(protocolState = ProtocolState.CredentialGenerated)) + ) ++ + MockCredentialService.MarkCredentialSent( + assertion = Assertion.anything, + result = Expectation.value(issueCredentialRecord.copy(protocolState = ProtocolState.CredentialSent)) + ) + + private val holderExpectations = + MockCredentialService.ReceiveCredentialOffer( + assertion = Assertion.anything, + result = Expectation.value(issueCredentialRecord.copy(protocolState = ProtocolState.OfferReceived)) + ) ++ MockCredentialService.AcceptCredentialOffer( + assertion = Assertion.anything, + result = Expectation.value(issueCredentialRecord.copy(protocolState = ProtocolState.RequestPending)) + ) ++ + MockCredentialService.GenerateCredentialRequest( + assertion = Assertion.anything, + result = Expectation.value(issueCredentialRecord.copy(protocolState = ProtocolState.RequestGenerated)) + ) ++ + MockCredentialService.MarkRequestSent( + assertion = Assertion.anything, + result = Expectation.value(issueCredentialRecord.copy(protocolState = ProtocolState.RequestSent)) + ) ++ + MockCredentialService.ReceiveCredentialIssue( + assertion = Assertion.anything, + result = Expectation.value(issueCredentialRecord.copy(protocolState = ProtocolState.CredentialReceived)) + ) + + override def spec: Spec[TestEnvironment with Scope, Any] = { + suite("CredentialServiceWithEventNotificationImpl")( + test("Happy flow generates relevant events on issuer side") { + for { + svc <- ZIO.service[CredentialService] + ens <- ZIO.service[EventNotificationService] + + offerCreatedRecord <- svc.createRecord() + issuerRecordId = offerCreatedRecord.id + _ <- svc.markOfferSent(issuerRecordId) + _ <- svc.receiveCredentialRequest(requestCredential()) + _ <- svc.acceptCredentialRequest(issuerRecordId) + _ <- svc.markCredentialGenerated(issuerRecordId, issueCredential()) + _ <- svc.markCredentialSent(issuerRecordId) + consumer <- ens.consumer[IssueCredentialRecord]("Issue") + events <- consumer.poll(50) + } yield { + assertTrue(events.size == 6) && + assertTrue(events.head.data.protocolState == ProtocolState.OfferPending) && + assertTrue(events(1).data.protocolState == ProtocolState.OfferSent) && + assertTrue(events(2).data.protocolState == ProtocolState.RequestReceived) && + assertTrue(events(3).data.protocolState == ProtocolState.CredentialPending) && + assertTrue(events(4).data.protocolState == ProtocolState.CredentialGenerated) && + assertTrue(events(5).data.protocolState == ProtocolState.CredentialSent) + } + }.provide( + ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer, + issuerExpectations.toLayer >>> CredentialServiceNotifier.layer + ), + test("Happy flow generates relevant events on the holder side") { + for { + svc <- ZIO.service[CredentialService] + ens <- ZIO.service[EventNotificationService] + + offerReceivedRecord <- svc.receiveCredentialOffer(offerCredential()) + holderRecordId = offerReceivedRecord.id + subjectId = "did:prism:60821d6833158c93fde5bb6a40d69996a683bf1fa5cdf32c458395b2887597c3" + _ <- svc.acceptCredentialOffer(holderRecordId, subjectId) + _ <- svc.generateCredentialRequest(offerReceivedRecord.id, JWT("Fake JWT")) + _ <- svc.markRequestSent(holderRecordId) + _ <- svc.receiveCredentialIssue(issueCredential()) + consumer <- ens.consumer[IssueCredentialRecord]("Issue") + events <- consumer.poll(50) + } yield { + assertTrue(events.size == 5) && + assertTrue(events.head.data.protocolState == ProtocolState.OfferReceived) && + assertTrue(events(1).data.protocolState == ProtocolState.RequestPending) && + assertTrue(events(2).data.protocolState == ProtocolState.RequestGenerated) && + assertTrue(events(3).data.protocolState == ProtocolState.RequestSent) && + assertTrue(events(4).data.protocolState == ProtocolState.CredentialReceived) + } + }.provide( + ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer, + holderExpectations.toLayer >>> CredentialServiceNotifier.layer + ) + ) + } + +} diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImplSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImplSpec.scala deleted file mode 100644 index 7f5ed36ce2..0000000000 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceWithEventNotificationImplSpec.scala +++ /dev/null @@ -1,97 +0,0 @@ -package io.iohk.atala.pollux.core.service - -import io.circe.syntax.* -import io.iohk.atala.event.notification.{EventNotificationService, EventNotificationServiceImpl} -import io.iohk.atala.mercury.model.Message -import io.iohk.atala.mercury.protocol.issuecredential.* -import io.iohk.atala.pollux.core.model.* -import io.iohk.atala.pollux.core.model.IssueCredentialRecord.ProtocolState -import io.iohk.atala.pollux.core.model.error.CredentialServiceError -import io.iohk.atala.pollux.core.repository.CredentialRepositoryInMemory -import io.iohk.atala.pollux.vc.jwt.* -import zio.* -import zio.test.* - -object CredentialServiceWithEventNotificationImplSpec extends ZIOSpecDefault with CredentialServiceSpecHelper { - - private val eventNotificationService = ZLayer.succeed(50) >>> EventNotificationServiceImpl.layer - private val credentialServiceWithEventNotificationLayer = - irisStubLayer ++ CredentialRepositoryInMemory.layer ++ didResolverLayer ++ ResourceURIDereferencerImpl.layer - >>> CredentialServiceWithEventNotificationImpl.layer - - override def spec: Spec[TestEnvironment with Scope, Any] = { - suite("CredentialServiceWithEventNotificationImpl") { - test("Happy flow generates relevant events") { - for { - // Get issuer services - issuerServices <- (for { - issuerSvc <- ZIO.service[CredentialService] - issuerEns <- ZIO.service[EventNotificationService] - } yield (issuerSvc, issuerEns)) - .provide(eventNotificationService, credentialServiceWithEventNotificationLayer) - issuerSvc = issuerServices._1 - issuerEns = issuerServices._2 - - // Get Holder services - holderServices <- (for { - holderSvc <- ZIO.service[CredentialService] - holderEns <- ZIO.service[EventNotificationService] - } yield (holderSvc, holderEns)) - .provide(eventNotificationService, credentialServiceWithEventNotificationLayer) - holderSvc = holderServices._1 - holderEns = holderServices._2 - - // Issuer creates offer - offerCreatedRecord <- issuerSvc.createRecord() - issuerRecordId = offerCreatedRecord.id - // Issuer sends offer - _ <- issuerSvc.markOfferSent(issuerRecordId) - msg <- ZIO.fromEither(offerCreatedRecord.offerCredentialData.get.makeMessage.asJson.as[Message]) - // Holder receives offer - offerReceivedRecord <- holderSvc.receiveCredentialOffer(OfferCredential.readFromMessage(msg)) - holderRecordId = offerReceivedRecord.id - subjectId = "did:prism:60821d6833158c93fde5bb6a40d69996a683bf1fa5cdf32c458395b2887597c3" - // Holder accepts offer - _ <- holderSvc.acceptCredentialOffer(holderRecordId, subjectId) - // Holder generates proof - requestGeneratedRecord <- holderSvc.generateCredentialRequest(offerReceivedRecord.id, JWT("Fake JWT")) - // Holder sends offer - _ <- holderSvc.markRequestSent(holderRecordId) - msg <- ZIO.fromEither(requestGeneratedRecord.requestCredentialData.get.makeMessage.asJson.as[Message]) - // Issuer receives request - requestReceivedRecord <- issuerSvc.receiveCredentialRequest(RequestCredential.readFromMessage(msg)) - // Issuer accepts request - requestAcceptedRecord <- issuerSvc.acceptCredentialRequest(issuerRecordId) - // Issuer generates credential - issue = issueCredential(Some(requestAcceptedRecord.thid)) - credentialGenerateRecord <- issuerSvc.markCredentialGenerated(issuerRecordId, issue) - // Issuer sends credential - _ <- issuerSvc.markCredentialSent(issuerRecordId) - msg <- ZIO.fromEither(credentialGenerateRecord.issueCredentialData.get.makeMessage.asJson.as[Message]) - // Holder receives credential - _ <- holderSvc.receiveCredentialIssue(IssueCredential.readFromMessage(msg)) - // Get generated events - issuerConsumer <- issuerEns.consumer[IssueCredentialRecord]("Issue") - issuerEvents <- issuerConsumer.poll(50) - holderConsumer <- holderEns.consumer[IssueCredentialRecord]("Issue") - holderEvents <- holderConsumer.poll(50) - } yield { - assertTrue(issuerEvents.size == 6) && - assertTrue(issuerEvents.head.data.protocolState == ProtocolState.OfferPending) && - assertTrue(issuerEvents(1).data.protocolState == ProtocolState.OfferSent) && - assertTrue(issuerEvents(2).data.protocolState == ProtocolState.RequestReceived) && - assertTrue(issuerEvents(3).data.protocolState == ProtocolState.CredentialPending) && - assertTrue(issuerEvents(4).data.protocolState == ProtocolState.CredentialGenerated) && - assertTrue(issuerEvents(5).data.protocolState == ProtocolState.CredentialSent) && - assertTrue(holderEvents.size == 5) && - assertTrue(holderEvents.head.data.protocolState == ProtocolState.OfferReceived) && - assertTrue(holderEvents(1).data.protocolState == ProtocolState.RequestPending) && - assertTrue(holderEvents(2).data.protocolState == ProtocolState.RequestGenerated) && - assertTrue(holderEvents(3).data.protocolState == ProtocolState.RequestSent) && - assertTrue(holderEvents(4).data.protocolState == ProtocolState.CredentialReceived) - } - } - } - } - -} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 96561f8dff..b9d5221a47 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -15,18 +15,8 @@ import io.iohk.atala.event.notification.EventNotificationServiceImpl import io.iohk.atala.issue.controller.IssueControllerImpl import io.iohk.atala.mercury.* import io.iohk.atala.pollux.core.service.* -import io.iohk.atala.pollux.credentialschema.controller.{ - CredentialSchemaController, - CredentialSchemaControllerImpl, - VerificationPolicyControllerImpl -} -import io.iohk.atala.pollux.sql.repository.{ - JdbcCredentialRepository, - JdbcCredentialSchemaRepository, - JdbcPresentationRepository, - JdbcVerificationPolicyRepository, - Migrations as PolluxMigrations -} +import io.iohk.atala.pollux.credentialschema.controller.{CredentialSchemaController, CredentialSchemaControllerImpl, VerificationPolicyControllerImpl} +import io.iohk.atala.pollux.sql.repository.{JdbcCredentialRepository, JdbcCredentialSchemaRepository, JdbcPresentationRepository, JdbcVerificationPolicyRepository, Migrations as PolluxMigrations} import io.iohk.atala.presentproof.controller.PresentProofControllerImpl import io.iohk.atala.resolvers.DIDResolver import io.iohk.atala.system.controller.SystemControllerImpl @@ -132,7 +122,7 @@ object MainApp extends ZIOAppDefault { // service ConnectionServiceImpl.layer >>> ConnectionServiceNotifier.layer, CredentialSchemaServiceImpl.layer, - CredentialServiceWithEventNotificationImpl.layer, + CredentialServiceImpl.layer >>> CredentialServiceNotifier.layer, DIDServiceImpl.layer, ManagedDIDServiceWithEventNotificationImpl.layer, PresentationServiceImpl.layer >>> PresentationServiceNotifier.layer, From d1108a0687f20c4f5514f5716574b1a86729c895 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Wed, 5 Jul 2023 16:05:12 +0200 Subject: [PATCH 40/56] fix(pollux): fix presentation accepted/rejected event not being sent --- .../service/PresentationServiceNotifier.scala | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala index a9af976b6b..107c143177 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala @@ -6,7 +6,7 @@ import io.iohk.atala.pollux.core.model.error.PresentationError import io.iohk.atala.pollux.core.model.presentation.Options import io.iohk.atala.pollux.core.model.{DidCommID, PresentationRecord} import io.iohk.atala.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCredentialPayload} -import zio.{IO, ZIO, ZLayer, URLayer} +import zio.{IO, URLayer, ZIO, ZLayer} import java.time.Instant import java.util.UUID @@ -71,11 +71,11 @@ class PresentationServiceNotifier( ): IO[PresentationError, PresentationRecord] = notifyOnSuccess(svc.markPresentationVerificationFailed(recordId)) - override def markPresentationAccepted(recordId: DidCommID): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(svc.markPresentationAccepted(recordId)) + override def acceptPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.acceptPresentation(recordId)) - override def markPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = - notifyOnSuccess(svc.markPresentationRejected(recordId)) + override def rejectPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + notifyOnSuccess(svc.rejectPresentation(recordId)) private[this] def notifyOnSuccess(effect: IO[PresentationError, PresentationRecord]) = for { @@ -118,15 +118,15 @@ class PresentationServiceNotifier( override def acceptProposePresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = svc.acceptPresentation(recordId) - override def acceptPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = - svc.acceptPresentation(recordId) - - override def rejectPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = - svc.rejectPresentation(recordId) - override def markProposePresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = svc.markProposePresentationSent(recordId) + override def markPresentationAccepted(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + svc.markPresentationAccepted(recordId) + + override def markPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + svc.markPresentationRejected(recordId) + override def reportProcessingFailure( recordId: DidCommID, failReason: Option[_root_.java.lang.String] From eb14280681f5f3c0dd3bc732859de82e0ab03ac0 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Wed, 5 Jul 2023 16:13:24 +0200 Subject: [PATCH 41/56] chore(prism-agent): scalafmtAll --- .../scala/io/iohk/atala/agent/server/Main.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index b9d5221a47..11c405e46a 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -15,8 +15,18 @@ import io.iohk.atala.event.notification.EventNotificationServiceImpl import io.iohk.atala.issue.controller.IssueControllerImpl import io.iohk.atala.mercury.* import io.iohk.atala.pollux.core.service.* -import io.iohk.atala.pollux.credentialschema.controller.{CredentialSchemaController, CredentialSchemaControllerImpl, VerificationPolicyControllerImpl} -import io.iohk.atala.pollux.sql.repository.{JdbcCredentialRepository, JdbcCredentialSchemaRepository, JdbcPresentationRepository, JdbcVerificationPolicyRepository, Migrations as PolluxMigrations} +import io.iohk.atala.pollux.credentialschema.controller.{ + CredentialSchemaController, + CredentialSchemaControllerImpl, + VerificationPolicyControllerImpl +} +import io.iohk.atala.pollux.sql.repository.{ + JdbcCredentialRepository, + JdbcCredentialSchemaRepository, + JdbcPresentationRepository, + JdbcVerificationPolicyRepository, + Migrations as PolluxMigrations +} import io.iohk.atala.presentproof.controller.PresentProofControllerImpl import io.iohk.atala.resolvers.DIDResolver import io.iohk.atala.system.controller.SystemControllerImpl From 2d360151c2bd169c91023366f7d4999c7836598f Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Thu, 6 Jul 2023 11:59:26 +0200 Subject: [PATCH 42/56] test(pollux): fix unit tests --- .../core/service/MockPresentationService.scala | 14 ++++++++++---- .../service/PresentationServiceNotifierSpec.scala | 8 ++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala index 9be9d8489f..f3fe774783 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala @@ -41,6 +41,10 @@ object MockPresentationService extends Mock[PresentationService] { object MarkPresentationSent extends Effect[DidCommID, PresentationError, PresentationRecord] + object AcceptPresentation extends Effect[DidCommID, PresentationError, PresentationRecord] + + object RejectPresentation extends Effect[DidCommID, PresentationError, PresentationRecord] + object ReceiveRequestPresentation extends Effect[(Option[String], RequestPresentation), PresentationError, PresentationRecord] @@ -104,6 +108,12 @@ object MockPresentationService extends Mock[PresentationService] { ): IO[PresentationError, PresentationRecord] = proxy(ReceiveRequestPresentation, (connectionId, request)) + override def acceptPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + proxy(AcceptPresentation, recordId) + + override def rejectPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = + proxy(RejectPresentation, recordId) + override def extractIdFromCredential(credential: W3cCredentialPayload): Option[UUID] = ??? override def getPresentationRecords(): IO[PresentationError, Seq[PresentationRecord]] = ??? @@ -127,10 +137,6 @@ object MockPresentationService extends Mock[PresentationService] { override def acceptProposePresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = ??? - override def acceptPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = ??? - - override def rejectPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = ??? - override def markRequestPresentationRejected(recordId: DidCommID): IO[PresentationError, PresentationRecord] = ??? override def markProposePresentationSent(recordId: DidCommID): IO[PresentationError, PresentationRecord] = ??? diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala index 0bbd4ef3be..5725d8a04b 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala @@ -49,7 +49,7 @@ object PresentationServiceNotifierSpec extends ZIOSpecDefault with PresentationS assertion = Assertion.anything, result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationVerified)) ) ++ - MockPresentationService.MarkPresentationAccepted( + MockPresentationService.AcceptPresentation( assertion = Assertion.anything, result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationAccepted)) ) @@ -61,7 +61,7 @@ object PresentationServiceNotifierSpec extends ZIOSpecDefault with PresentationS ) private val verifierRejectPresentationExpectations = - MockPresentationService.MarkPresentationRejected( + MockPresentationService.RejectPresentation( assertion = Assertion.anything, result = Expectation.value(record.copy(protocolState = ProtocolState.PresentationRejected)) ) @@ -101,7 +101,7 @@ object PresentationServiceNotifierSpec extends ZIOSpecDefault with PresentationS _ <- svc.markRequestPresentationSent(record.id) _ <- svc.receivePresentation(presentation(record.thid.value)) _ <- svc.markPresentationVerified(record.id) - _ <- svc.markPresentationAccepted(record.id) + _ <- svc.acceptPresentation(record.id) consumer <- ens.consumer[PresentationRecord]("Presentation") events <- consumer.poll(50) @@ -139,7 +139,7 @@ object PresentationServiceNotifierSpec extends ZIOSpecDefault with PresentationS svc <- ZIO.service[PresentationService] ens <- ZIO.service[EventNotificationService] - _ <- svc.markPresentationRejected(DidCommID()) + _ <- svc.rejectPresentation(DidCommID()) consumer <- ens.consumer[PresentationRecord]("Presentation") events <- consumer.poll(50) From 6a3839d51999e4f65c0e1315720a0cdaa3a7d0c2 Mon Sep 17 00:00:00 2001 From: Yurii Shynbuiev Date: Thu, 6 Jul 2023 18:44:03 +0700 Subject: [PATCH 43/56] feat: add Json serialization for events #1 --- .../iohk/atala/event/notification/Event.scala | 13 ++++++- .../agent/notification/JsonEventDecoder.scala | 38 +++++++++++++++++++ .../agent/notification/WebhookPublisher.scala | 16 +++++--- .../connect/controller/http/Connection.scala | 2 + .../http/IssueCredentialRecord.scala | 2 + .../controller/http/PresentationStatus.scala | 2 + ...dDIDServiceWithEventNotificationImpl.scala | 30 ++------------- 7 files changed, 69 insertions(+), 34 deletions(-) create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/JsonEventDecoder.scala diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala index 40d49d2538..f152b15d50 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala @@ -1,3 +1,14 @@ package io.iohk.atala.event.notification +import java.time.Instant +import java.util.UUID +import zio.IO -case class Event[A](data: A) +case class Event[A](id: UUID, ts: Instant, data: A){ + def map[B](f: A => B): Event[B] = copy(data = f(data)) + def mapZIO[E, B](f: A => IO[E, B]): IO[E, Event[B]] = f(data).map(d => copy(data = d)) + def convert[B](implicit conversion: Conversion[A, B]): Event[B] = map(conversion.convert) +} + +object Event { + def apply[A](data: A): Event[A] = Event(UUID.randomUUID(), Instant.now(), data) +} \ No newline at end of file diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/JsonEventDecoder.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/JsonEventDecoder.scala new file mode 100644 index 0000000000..01f962f480 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/JsonEventDecoder.scala @@ -0,0 +1,38 @@ +package io.iohk.atala.agent.notification + +import io.iohk.atala.agent.walletapi.model.ManagedDIDDetail +import io.iohk.atala.castor.controller.http.ManagedDID +import io.iohk.atala.castor.controller.http.ManagedDID.* +import io.iohk.atala.connect.controller.http.Connection +import io.iohk.atala.connect.controller.http.Connection.* +import io.iohk.atala.connect.core.model.ConnectionRecord +import io.iohk.atala.event.notification.{Event, EventNotificationServiceError} +import io.iohk.atala.issue.controller.http.IssueCredentialRecord +import io.iohk.atala.issue.controller.http.IssueCredentialRecord.* +import io.iohk.atala.pollux.core.model.{ + IssueCredentialRecord as PolluxIssueCredentialRecord, + PresentationRecord as PolluxPresentationRecord +} +import io.iohk.atala.presentproof.controller.http.PresentationStatus +import zio.* +import zio.json.* + +object JsonEventEncoders { + + implicit val connectionRecordEncoder: JsonEncoder[ConnectionRecord] = + Connection.encoder.contramap(implicitly[Conversion[ConnectionRecord, Connection]].convert) + + implicit val issueCredentialRecordEncoder: JsonEncoder[PolluxIssueCredentialRecord] = + IssueCredentialRecord.encoder.contramap( + implicitly[Conversion[PolluxIssueCredentialRecord, IssueCredentialRecord]].convert + ) + + implicit val presentationRecordEncoder: JsonEncoder[PolluxPresentationRecord] = + PresentationStatus.encoder.contramap(implicitly[Conversion[PolluxPresentationRecord, PresentationStatus]].convert) + + implicit val managedDIDDetailEncoder: JsonEncoder[ManagedDIDDetail] = + ManagedDID.encoder.contramap(implicitly[Conversion[ManagedDIDDetail, ManagedDID]].convert) + + implicit def eventEncoder[T](implicit jsonEncoder: JsonEncoder[T]): JsonEncoder[Event[T]] = + DeriveJsonEncoder.gen[Event[T]] +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index ebc16d99c4..e1362dc7cc 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -2,7 +2,7 @@ package io.iohk.atala.agent.notification import io.iohk.atala.agent.notification.WebhookPublisher.given import io.iohk.atala.agent.notification.WebhookPublisherError.{InvalidWebhookURL, UnexpectedError} import io.iohk.atala.agent.server.config.{AppConfig, WebhookPublisherConfig} -import io.iohk.atala.agent.walletapi.model.ManagedDIDState +import io.iohk.atala.agent.walletapi.model.ManagedDIDDetail import io.iohk.atala.connect.core.model.ConnectionRecord import io.iohk.atala.event.notification.{Event, EventConsumer, EventNotificationService} import io.iohk.atala.pollux.core.model.{IssueCredentialRecord, PresentationRecord} @@ -10,6 +10,9 @@ import zio.* import zio.http.* import zio.http.ZClient.ClientLive import zio.http.model.{Header, Headers, Method} +import zio.json.JsonEncoder +import zio.json._ +import io.iohk.atala.agent.notification.JsonEventEncoders._ import java.net.{URI, URL} @@ -39,7 +42,7 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat .consumer[PresentationRecord]("Presentation") .mapError(e => UnexpectedError(e.toString)) didStateConsumer <- notificationService - .consumer[ManagedDIDState]("DIDState") + .consumer[ManagedDIDDetail]("DIDDetail") .mapError(e => UnexpectedError(e.toString)) _ <- pollAndNotify(connectConsumer, url).forever.debug.forkDaemon _ <- pollAndNotify(issueConsumer, url).forever.debug.forkDaemon @@ -49,7 +52,7 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat case None => ZIO.unit } - private[this] def pollAndNotify[A](consumer: EventConsumer[A], url: URL) = { + private[this] def pollAndNotify[A](consumer: EventConsumer[A], url: URL)(implicit encoder: JsonEncoder[A]) = { for { _ <- ZIO.log(s"Polling $parallelism event(s)") events <- consumer.poll(parallelism).mapError(e => UnexpectedError(e.toString)) @@ -62,7 +65,9 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat } yield () } - private[this] def notifyWebhook[A](event: Event[A], url: URL): ZIO[Client, UnexpectedError, Unit] = { + private[this] def notifyWebhook[A](event: Event[A], url: URL)(implicit + encoder: JsonEncoder[A] + ): ZIO[Client, UnexpectedError, Unit] = { for { _ <- ZIO.log(s"Sending event: $event to HTTP webhook URL: $url with API key ${config.apiKey}") response <- Client @@ -70,8 +75,7 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat url = url.toString, method = Method.POST, headers = baseHeaders, - // TODO serialize event to JSON here - content = Body.fromString(event.data.toString) + content = Body.fromString(event.toJson) ) .mapError(t => UnexpectedError(s"Webhook request error: $t")) resp <- response match diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/http/Connection.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/http/Connection.scala index ace4775662..12df776075 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/http/Connection.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/http/Connection.scala @@ -80,6 +80,8 @@ object Connection { kind = "Connection", ) + given Conversion[model.ConnectionRecord, Connection] = fromDomain + object annotations { object connectionId extends Annotation[UUID]( diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/http/IssueCredentialRecord.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/http/IssueCredentialRecord.scala index 8911b94a5d..5cc22f1ca2 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/http/IssueCredentialRecord.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/http/IssueCredentialRecord.scala @@ -111,6 +111,8 @@ object IssueCredentialRecord { }) ) + given Conversion[PolluxIssueCredentialRecord, IssueCredentialRecord] = fromDomain + object annotations { object subjectId diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/PresentationStatus.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/PresentationStatus.scala index 94a80c6b0c..8c6a5f6d24 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/PresentationStatus.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/PresentationStatus.scala @@ -48,6 +48,8 @@ object PresentationStatus { ) } + given Conversion[PresentationRecord, PresentationStatus] = fromDomain + object annotations { object presentationId extends Annotation[String]( diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala index 8b807e7533..aedc2252e4 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala @@ -1,7 +1,7 @@ package io.iohk.atala.agent.walletapi.service import io.iohk.atala.agent.walletapi.crypto.Apollo -import io.iohk.atala.agent.walletapi.model.ManagedDIDState +import io.iohk.atala.agent.walletapi.model.{ManagedDIDState, ManagedDIDDetail} import io.iohk.atala.agent.walletapi.model.error.CommonWalletStorageError import io.iohk.atala.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage} import io.iohk.atala.agent.walletapi.util.SeedResolver @@ -32,29 +32,6 @@ class ManagedDIDServiceWithEventNotificationImpl( createDIDSem ) { -// override protected def computeNewDIDLineageStatusAndPersist[E]( -// updateLineage: DIDUpdateLineage -// )(using -// c1: Conversion[DIDOperationError, E], -// c2: Conversion[CommonWalletStorageError, E] -// ): IO[E, Boolean] = { -// for { -// updated <- super.computeNewDIDLineageStatusAndPersist(updateLineage) -// maybeOperationDetail <- didService -// .getScheduledDIDOperationDetail(updateLineage.operationId.toArray) -// .mapError[E](e => e) -// _ <- ZIO.when(updated) { -// val result = for { -// maybeUpdatedDID <- nonSecretStorage.getManagedDIDState(updateLineage.???) -// updatedDID <- ZIO.fromOption(maybeUpdatedDID) -// producer <- eventNotificationService.producer[ManagedDIDState]("DIDState") -// _ <- producer.send(Event(updatedDID)) -// } yield () -// result.catchAll(e => ZIO.logError(s"Notification service error: $e")) -// } -// } yield updated -// } - override protected def computeNewDIDStateFromDLTAndPersist[E]( did: CanonicalPrismDID )(using @@ -67,14 +44,13 @@ class ManagedDIDServiceWithEventNotificationImpl( val result = for { maybeUpdatedDID <- nonSecretStorage.getManagedDIDState(did) updatedDID <- ZIO.fromOption(maybeUpdatedDID) - producer <- eventNotificationService.producer[ManagedDIDState]("DIDState") - _ <- producer.send(Event(updatedDID)) + producer <- eventNotificationService.producer[ManagedDIDDetail]("DIDDetail") + _ <- producer.send(Event(ManagedDIDDetail(did, updatedDID))) } yield () result.catchAll(e => ZIO.logError(s"Notification service error: $e")) } } yield updated } - } object ManagedDIDServiceWithEventNotificationImpl { From c954544953975259b990f36e3679322a5685359a Mon Sep 17 00:00:00 2001 From: Yurii Shynbuiev Date: Thu, 6 Jul 2023 20:31:37 +0700 Subject: [PATCH 44/56] feat: add application/json header --- .../agent/notification/WebhookPublisher.scala | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index e1362dc7cc..4ebc8f2c60 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -1,25 +1,24 @@ package io.iohk.atala.agent.notification -import io.iohk.atala.agent.notification.WebhookPublisher.given +import io.iohk.atala.agent.notification.JsonEventEncoders.* import io.iohk.atala.agent.notification.WebhookPublisherError.{InvalidWebhookURL, UnexpectedError} -import io.iohk.atala.agent.server.config.{AppConfig, WebhookPublisherConfig} +import io.iohk.atala.agent.server.config.AppConfig import io.iohk.atala.agent.walletapi.model.ManagedDIDDetail import io.iohk.atala.connect.core.model.ConnectionRecord import io.iohk.atala.event.notification.{Event, EventConsumer, EventNotificationService} import io.iohk.atala.pollux.core.model.{IssueCredentialRecord, PresentationRecord} import zio.* import zio.http.* -import zio.http.ZClient.ClientLive -import zio.http.model.{Header, Headers, Method} -import zio.json.JsonEncoder -import zio.json._ -import io.iohk.atala.agent.notification.JsonEventEncoders._ +import zio.http.model.* +import zio.json.* -import java.net.{URI, URL} +import java.net.URL class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificationService) { private val config = appConfig.agent.webhookPublisher - private val baseHeaders = config.apiKey.map(key => Headers.authorization(key)).getOrElse(Headers.empty) + private val baseHeaders = + config.apiKey.map(key => Headers.authorization(key)).getOrElse(Headers.empty) ++ + Headers.contentType(HeaderValues.applicationJson) private val parallelism = config.parallelism match { case Some(p) if p < 1 => 1 From 678f8cc394c71ca4c111a8e7b14fc1d5c3daa991 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 7 Jul 2023 14:32:20 +0200 Subject: [PATCH 45/56] doc(prism-agent): update protocol state enums in OAS --- .../api/http/prism-agent-openapi-spec.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/prism-agent/service/api/http/prism-agent-openapi-spec.yaml b/prism-agent/service/api/http/prism-agent-openapi-spec.yaml index 2820aef82d..26fe57a603 100644 --- a/prism-agent/service/api/http/prism-agent-openapi-spec.yaml +++ b/prism-agent/service/api/http/prism-agent-openapi-spec.yaml @@ -1992,6 +1992,21 @@ components: type: string description: The current state of the issue credential protocol execution. example: OfferPending + enum: + - OfferPending + - OfferSent + - OfferReceived + - RequestPending + - RequestGenerated + - RequestSent + - RequestReceived + - CredentialPending + - CredentialGenerated + - CredentialSent + - CredentialReceived + - ProblemReportPending + - ProblemReportSent + - ProblemReportReceived jwtCredential: type: string description: The base64-encoded JWT verifiable credential that has been @@ -2140,6 +2155,7 @@ components: - PresentationSent - PresentationReceived - PresentationVerified + - PresentationVerificationFailed - PresentationAccepted - PresentationRejected - ProblemReportPending From b71a0f0813585854bae6024a9b12d3cb051df24f Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 7 Jul 2023 14:45:58 +0200 Subject: [PATCH 46/56] doc(prism-agent): add tutorial for simple event mechanism webhooks --- docs/docusaurus/sidebars.js | 3 +- docs/docusaurus/webhooks/webhook.md | 161 ++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 docs/docusaurus/webhooks/webhook.md diff --git a/docs/docusaurus/sidebars.js b/docs/docusaurus/sidebars.js index e84b07b37d..ed52913979 100644 --- a/docs/docusaurus/sidebars.js +++ b/docs/docusaurus/sidebars.js @@ -65,7 +65,8 @@ const sidebars = { 'secrets/operation', 'secrets/seed-generation' ] - } + }, + 'webhooks/webhook' ] } diff --git a/docs/docusaurus/webhooks/webhook.md b/docs/docusaurus/webhooks/webhook.md new file mode 100644 index 0000000000..2ce0118de9 --- /dev/null +++ b/docs/docusaurus/webhooks/webhook.md @@ -0,0 +1,161 @@ +# Getting Started with Webhook Notifications in PRISM Agent + +## Introduction + +Welcome to the tutorial on webhook notifications in PRISM Agent. In this tutorial, we will explore how webhook +notifications can enhance your experience with PRISM Agent by providing real-time updates on events. By leveraging +webhook notifications, you can stay informed about important changes happening within the agent. + +## Section 1: Understanding Webhook Notifications + +### 1.1 What are Webhooks? + +Webhooks enable real-time communication between applications by sending HTTP requests containing event data to specified +endpoints (webhook URLs) when events occur. They establish a direct communication channel, allowing applications to +receive instant updates and respond in a timely manner, promoting efficient integration between event-driven +systems. + +### 1.2 Purpose of Webhook Notifications in PRISM Agent + +Webhook notifications in PRISM Agent serve as a vital feature, enabling you to receive timely updates on various events +occurring within the agent. Webhooks allow you to receive HTTP requests containing event details at a specified +endpoint (webhook URL). These events are specifically related to the execution of +the [Connect](../connections/connection.md), [Issue](../credentials/issue.md), +and [Presentation](../credentials/present-proof.md) flows. Webhook notifications will be sent each time there is a state +change during the execution of these protocols. + +By leveraging webhooks, you can integrate PRISM Agent seamlessly into your applications and systems. You can track and +monitor the progress of the main flows, receiving timely updates about changes and events. + +## Section 2: Configuring the Webhook Feature + +### 2.1 Enabling the Webhook Feature + +PRISM Agent uses the following environment variables to manage webhook notifications: + +| Name | Description | Default | +|-------------------|--------------------------------------------------------------------------|---------| +| `WEBHOOK_URL` | The webhook endpoint URL where the notifications will be sent | null | +| `WEBHOOK_API_KEY` | The optional API key (bearer token) to use as the `Authorization` header | `vault` | + +### 2.2 Securing the Webhook Endpoint + +It is essential to secure the webhook endpoint to protect the integrity and confidentiality of the event data. Consider +the following best practices when securing your webhook endpoint: + +- Use HTTPS to encrypt communication between PRISM Agent and the webhook endpoint. +- Implement authentication mechanisms (e.g., API keys, tokens) to verify the authenticity of incoming requests. +- Validate and sanitize incoming webhook requests to mitigate potential security risks. + +The current supported authorization mechanism for PRISM Agent's webhook notifications is the bearer token. If +configured, the token will be included in the `Authorization` header of the HTTP request sent by the agent to the +webhook endpoint. You can configure this bearer token by setting the value of the `WEBHOOK_API_KEY` environment +variable. + +## Section 3: Event Format and Types + +### 3.1 Event Format + +Webhook notifications from PRISM Agent are sent as JSON payloads in the HTTP requests. + +The event format is consistent across all events. Each event follows a common structure, while the 'data' field +within the event payload contains information specific to the type of event. Here is an example of the JSON payload +format: + +The event payload typically includes relevant details about the specific event that occurred within the agent. Below is +an example of the JSON payload format: + +```json +{ + "id": "cb8d4e96-30f0-4892-863f-44d49d634211", + "ts": "2023-07-06T12:01:19.769427Z", + "eventType": "xxxx", + "data": { + // Event-specific data goes here + } +} +``` + +This event format ensures consistency and allows you to handle webhook notifications uniformly while easily extracting +the relevant data specific to each event type from the `data` field. + +Here is an example of a webhook notification event related to a connection flow state change (invitation generated): + +```json +{ + "id": "cb8d4e96-30f0-4892-863f-44d49d634211", + "ts": "2023-07-06T12:01:19.769427Z", + "eventType": "Connection", + "data": { + "connectionId": "c10787cf-99bb-47f4-99bb-1fdcca32b673", + "label": "Connect with Alice", + "role": "Inviter", + "state": "InvitationGenerated", + "invitation": { + "id": "c10787cf-99bb-47f4-99bb-1fdcca32b673", + "type": "https://didcomm.org/out-of-band/2.0/invitation", + "from": "did:peer:2.Ez6LS...jIiXX0", + "invitationUrl": "https://my.domain.com/path?_oob=eyJpZCI6...bXX19" + }, + "createdAt": "2023-07-06T12:01:19.760126Z", + "self": "c10787cf-99bb-47f4-99bb-1fdcca32b673", + "kind": "Connection" + } +} +``` + +### 3.2 Common Event Types + +PRISM Agent sends webhook notifications for events related to protocol state changes in +the [Connect](../connections/connection.md), [Issue](../credentials/issue.md), +and [Presentation](../credentials/present-proof.md) flows. These events allow you to track the progress and updates +within these flows in real-time. Some common event types that you can expect to receive through webhook notifications +include: + +- Connection State Change: Notifies about state changes in the connection flow, such as `InvitationGenerated`, + `ConnectionRequestSent`, `ConnectionResponseReceived`, etc. Please refer to the `state` field of + the [connection resource](https://docs.atalaprism.io/agent-api/#tag/Connections-Management/operation/getConnection) + for an exhaustive list of states. +- Credential State Change: Indicates changes in the credential issuance flow, such as `OfferSent`, `RequestReceived`, + `CredentialSent`, etc. Please refer to the `protocolState` field of + the [credential resource](https://docs.atalaprism.io/agent-api/#tag/Issue-Credentials-Protocol/operation/getCredentialRecord) + for an exhaustive list of states. +- Presentation State Change: Notifies about changes in the presentation flow, such as `RequestReceived`, + `PresentationGenerated`, `PresentationVerified`, etc. Please refer to the `status` field of + the [presentation resource](https://docs.atalaprism.io/agent-api/#tag/Present-Proof/operation/getPresentation) for an + exhaustive list of states. + +## Section 4: Processing Webhook Notifications + +### 4.1 Handling Incoming Webhook Requests + +To handle incoming webhook notifications from PRISM Agent in your application, follow these general steps: + +1. Receive the HTTP request at your specified webhook endpoint. +2. Parse the JSON payload of the request to extract the event details. +3. Process the event data according to your application's requirements. +4. Send a response back to acknowledge the successful receipt of the webhook notification. For a successful reception, + the response status code should be `>= 200` and `< 300`. Any other response status code will lead to a new attempt + from the PRISM Agent. + +### 4.2 Error Handling and Retry Mechanisms + +When working with webhook notifications in PRISM Agent, it is important to consider error handling and retry mechanisms. +In case of failed webhook notifications or errors, PRISM Agent employs an automatic retry mechanism to ensure delivery. +The agent will attempt to send the webhook notification up to three times, with a five-second interval between each +attempt. Please note that the number of retries and the interval duration are currently not configurable in PRISM Agent. + +By default, this retry mechanism provides a reasonable level of reliability for delivering webhook notifications, +allowing for temporary network issues or intermittent failures. + +## Conclusion + +Congratulations! You've learned about webhook notifications in PRISM Agent. By leveraging this feature, you can receive +real-time updates on events happening within the agent, enabling you to integrate PRISM Agent seamlessly into your +applications. Remember to secure your webhook endpoint and handle webhook notifications effectively to maximize the +benefits of this feature. + +Start integrating webhook notifications into your PRISM Agent workflow and unlock the power of real-time event updates! + +If you have any further questions or need assistance, don't hesitate to reach out to the PRISM Agent support team or +refer to the official documentation for more details. \ No newline at end of file From e47aaf45e9ff2e682ec9ac0baae6647c1276886a Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 7 Jul 2023 15:25:10 +0200 Subject: [PATCH 47/56] doc(prism-agent): update webhook tutorial --- docs/docusaurus/webhooks/webhook.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/docusaurus/webhooks/webhook.md b/docs/docusaurus/webhooks/webhook.md index 2ce0118de9..9eaf0b587d 100644 --- a/docs/docusaurus/webhooks/webhook.md +++ b/docs/docusaurus/webhooks/webhook.md @@ -1,4 +1,4 @@ -# Getting Started with Webhook Notifications in PRISM Agent +# Webhook Notifications in PRISM Agent ## Introduction @@ -6,16 +6,16 @@ Welcome to the tutorial on webhook notifications in PRISM Agent. In this tutoria notifications can enhance your experience with PRISM Agent by providing real-time updates on events. By leveraging webhook notifications, you can stay informed about important changes happening within the agent. -## Section 1: Understanding Webhook Notifications +## Understanding Webhook Notifications -### 1.1 What are Webhooks? +### What are Webhooks? Webhooks enable real-time communication between applications by sending HTTP requests containing event data to specified endpoints (webhook URLs) when events occur. They establish a direct communication channel, allowing applications to receive instant updates and respond in a timely manner, promoting efficient integration between event-driven systems. -### 1.2 Purpose of Webhook Notifications in PRISM Agent +### Purpose of Webhook Notifications in PRISM Agent Webhook notifications in PRISM Agent serve as a vital feature, enabling you to receive timely updates on various events occurring within the agent. Webhooks allow you to receive HTTP requests containing event details at a specified @@ -27,9 +27,9 @@ change during the execution of these protocols. By leveraging webhooks, you can integrate PRISM Agent seamlessly into your applications and systems. You can track and monitor the progress of the main flows, receiving timely updates about changes and events. -## Section 2: Configuring the Webhook Feature +## Configuring the Webhook Feature -### 2.1 Enabling the Webhook Feature +### Enabling the Webhook Feature PRISM Agent uses the following environment variables to manage webhook notifications: @@ -38,7 +38,7 @@ PRISM Agent uses the following environment variables to manage webhook notificat | `WEBHOOK_URL` | The webhook endpoint URL where the notifications will be sent | null | | `WEBHOOK_API_KEY` | The optional API key (bearer token) to use as the `Authorization` header | `vault` | -### 2.2 Securing the Webhook Endpoint +### Securing the Webhook Endpoint It is essential to secure the webhook endpoint to protect the integrity and confidentiality of the event data. Consider the following best practices when securing your webhook endpoint: @@ -52,9 +52,9 @@ configured, the token will be included in the `Authorization` header of the HTTP webhook endpoint. You can configure this bearer token by setting the value of the `WEBHOOK_API_KEY` environment variable. -## Section 3: Event Format and Types +## Event Format and Types -### 3.1 Event Format +### Event Format Webhook notifications from PRISM Agent are sent as JSON payloads in the HTTP requests. @@ -104,7 +104,7 @@ Here is an example of a webhook notification event related to a connection flow } ``` -### 3.2 Common Event Types +### Common Event Types PRISM Agent sends webhook notifications for events related to protocol state changes in the [Connect](../connections/connection.md), [Issue](../credentials/issue.md), @@ -125,9 +125,9 @@ include: the [presentation resource](https://docs.atalaprism.io/agent-api/#tag/Present-Proof/operation/getPresentation) for an exhaustive list of states. -## Section 4: Processing Webhook Notifications +## Processing Webhook Notifications -### 4.1 Handling Incoming Webhook Requests +### Handling Incoming Webhook Requests To handle incoming webhook notifications from PRISM Agent in your application, follow these general steps: @@ -138,7 +138,7 @@ To handle incoming webhook notifications from PRISM Agent in your application, f the response status code should be `>= 200` and `< 300`. Any other response status code will lead to a new attempt from the PRISM Agent. -### 4.2 Error Handling and Retry Mechanisms +### Error Handling and Retry Mechanisms When working with webhook notifications in PRISM Agent, it is important to consider error handling and retry mechanisms. In case of failed webhook notifications or errors, PRISM Agent employs an automatic retry mechanism to ensure delivery. From f97431fbb5024ff0cc5b0ecb8e3011c884b88aac Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Fri, 7 Jul 2023 15:34:20 +0200 Subject: [PATCH 48/56] doc(prism-agent): update Docusaurus sidebar for webhook --- docs/docusaurus/sidebars.js | 8 +++++++- docs/docusaurus/webhooks/webhook.md | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/docusaurus/sidebars.js b/docs/docusaurus/sidebars.js index ed52913979..8d9c7e5eef 100644 --- a/docs/docusaurus/sidebars.js +++ b/docs/docusaurus/sidebars.js @@ -66,7 +66,13 @@ const sidebars = { 'secrets/seed-generation' ] }, - 'webhooks/webhook' + { + type: 'category', + label: 'Webhooks', + items: [ + 'webhooks/webhook', + ] + } ] } diff --git a/docs/docusaurus/webhooks/webhook.md b/docs/docusaurus/webhooks/webhook.md index 9eaf0b587d..32872f9e8c 100644 --- a/docs/docusaurus/webhooks/webhook.md +++ b/docs/docusaurus/webhooks/webhook.md @@ -1,4 +1,4 @@ -# Webhook Notifications in PRISM Agent +# Webhook Notifications ## Introduction From 2181bfa7d7a6655eb675f3b66df04c11638c6b74 Mon Sep 17 00:00:00 2001 From: Yurii Shynbuiev Date: Mon, 10 Jul 2023 14:08:27 +0700 Subject: [PATCH 49/56] feat: add event type field to all events --- .../connect/core/service/ConnectionServiceNotifier.scala | 4 +++- .../scala/io/iohk/atala/event/notification/Event.scala | 8 ++------ .../pollux/core/service/CredentialServiceNotifier.scala | 4 +++- .../pollux/core/service/PresentationServiceNotifier.scala | 4 +++- .../{JsonEventDecoder.scala => JsonEventEncoders.scala} | 0 .../ManagedDIDServiceWithEventNotificationImpl.scala | 4 +++- 6 files changed, 14 insertions(+), 10 deletions(-) rename prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/{JsonEventDecoder.scala => JsonEventEncoders.scala} (100%) diff --git a/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifier.scala b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifier.scala index a2d05194e2..d6676c7e5f 100644 --- a/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifier.scala +++ b/connect/lib/core/src/main/scala/io/iohk/atala/connect/core/service/ConnectionServiceNotifier.scala @@ -14,6 +14,8 @@ class ConnectionServiceNotifier( eventNotificationService: EventNotificationService ) extends ConnectionService { + private val connectionUpdatedEvent = "ConnectionUpdated" + override def createConnectionInvitation( label: Option[String], pairwiseDID: DidId @@ -53,7 +55,7 @@ class ConnectionServiceNotifier( private[this] def notify(record: ConnectionRecord) = { val result = for { producer <- eventNotificationService.producer[ConnectionRecord]("Connect") - _ <- producer.send(Event(record)) + _ <- producer.send(Event(connectionUpdatedEvent, record)) } yield () result.catchAll(e => ZIO.logError(s"Notification service error: $e")) } diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala index f152b15d50..0474f769e7 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala @@ -3,12 +3,8 @@ import java.time.Instant import java.util.UUID import zio.IO -case class Event[A](id: UUID, ts: Instant, data: A){ - def map[B](f: A => B): Event[B] = copy(data = f(data)) - def mapZIO[E, B](f: A => IO[E, B]): IO[E, Event[B]] = f(data).map(d => copy(data = d)) - def convert[B](implicit conversion: Conversion[A, B]): Event[B] = map(conversion.convert) -} +case class Event[A](`type`: String, id: UUID, ts: Instant, data: A) object Event { - def apply[A](data: A): Event[A] = Event(UUID.randomUUID(), Instant.now(), data) + def apply[A](`type`: String, data: A): Event[A] = Event(`type`, UUID.randomUUID(), Instant.now(), data) } \ No newline at end of file diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala index b77ad7aa8b..f613474ffa 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala @@ -18,6 +18,8 @@ class CredentialServiceNotifier( eventNotificationService: EventNotificationService ) extends CredentialService { + private val issueCredentialRecordUpdatedEvent = "IssueCredentialRecordUpdated" + override def createIssueCredentialRecord( pairwiseIssuerDID: DidId, pairwiseHolderDID: DidId, @@ -91,7 +93,7 @@ class CredentialServiceNotifier( private[this] def notify(record: IssueCredentialRecord) = { val result = for { producer <- eventNotificationService.producer[IssueCredentialRecord]("Issue") - _ <- producer.send(Event(record)) + _ <- producer.send(Event(issueCredentialRecordUpdatedEvent, record)) } yield () result.catchAll(e => ZIO.logError(s"Notification service error: $e")) } diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala index 00fd474a1f..535418c03d 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala @@ -16,6 +16,8 @@ class PresentationServiceNotifier( eventNotificationService: EventNotificationService ) extends PresentationService { + private val presentationUpdatedEvent = "PresentationUpdated" + override def createPresentationRecord( pairwiseVerifierDID: DidId, pairwiseProverDID: DidId, @@ -86,7 +88,7 @@ class PresentationServiceNotifier( private[this] def notify(record: PresentationRecord) = { val result = for { producer <- eventNotificationService.producer[PresentationRecord]("Presentation") - _ <- producer.send(Event(record)) + _ <- producer.send(Event(presentationUpdatedEvent, record)) } yield () result.catchAll(e => ZIO.logError(s"Notification service error: $e")) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/JsonEventDecoder.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/JsonEventEncoders.scala similarity index 100% rename from prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/JsonEventDecoder.scala rename to prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/JsonEventEncoders.scala diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala index aedc2252e4..61409adace 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala @@ -32,6 +32,8 @@ class ManagedDIDServiceWithEventNotificationImpl( createDIDSem ) { + private val didStatusUpdatedEventName = "DIDStatusUpdated" + override protected def computeNewDIDStateFromDLTAndPersist[E]( did: CanonicalPrismDID )(using @@ -45,7 +47,7 @@ class ManagedDIDServiceWithEventNotificationImpl( maybeUpdatedDID <- nonSecretStorage.getManagedDIDState(did) updatedDID <- ZIO.fromOption(maybeUpdatedDID) producer <- eventNotificationService.producer[ManagedDIDDetail]("DIDDetail") - _ <- producer.send(Event(ManagedDIDDetail(did, updatedDID))) + _ <- producer.send(Event(didStatusUpdatedEventName, ManagedDIDDetail(did, updatedDID))) } yield () result.catchAll(e => ZIO.logError(s"Notification service error: $e")) } From 8aff7ccab3eb0954a2294a23c8dd4c1c89a00480 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Mon, 10 Jul 2023 12:56:41 +0200 Subject: [PATCH 50/56] chore(prism-agent): use ZIO ConcurrentMap to store notification queues --- build.sbt | 4 ++- .../iohk/atala/event/notification/Event.scala | 2 +- .../EventNotificationServiceImpl.scala | 18 ++++++---- .../EventNotificationServiceImplSpec.scala | 36 +++++++++---------- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/build.sbt b/build.sbt index f8df7dc2d4..1591355ae9 100644 --- a/build.sbt +++ b/build.sbt @@ -283,11 +283,12 @@ lazy val D_Pollux_VC_JWT = new { lazy val D_EventNotification = new { val zio = "dev.zio" %% "zio" % V.zio + val zioConcurrent = "dev.zio" %% "zio-concurrent" % V.zio val zioTest = "dev.zio" %% "zio-test" % V.zio % Test val zioTestSbt = "dev.zio" %% "zio-test-sbt" % V.zio % Test val zioTestMagnolia = "dev.zio" %% "zio-test-magnolia" % V.zio % Test - val zioDependencies: Seq[ModuleID] = Seq(zio, zioTest, zioTestSbt, zioTestMagnolia) + val zioDependencies: Seq[ModuleID] = Seq(zio, zioConcurrent, zioTest, zioTestSbt, zioTestMagnolia) val baseDependencies: Seq[ModuleID] = zioDependencies } @@ -848,6 +849,7 @@ lazy val aggregatedProjects: Seq[ProjectReference] = Seq( prismAgentWalletAPI, prismAgentServer, mediator, + eventNotification, ) lazy val root = project diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala index 0474f769e7..2ed89e8999 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/Event.scala @@ -7,4 +7,4 @@ case class Event[A](`type`: String, id: UUID, ts: Instant, data: A) object Event { def apply[A](`type`: String, data: A): Event[A] = Event(`type`, UUID.randomUUID(), Instant.now(), data) -} \ No newline at end of file +} diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala index 0ef709bded..26db2fed2b 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala @@ -1,20 +1,19 @@ package io.iohk.atala.event.notification import io.iohk.atala.event.notification.EventNotificationServiceError.EventSendingFailed +import zio.concurrent.ConcurrentMap import zio.{IO, Queue, URLayer, ZIO, ZLayer} -import scala.collection.mutable - -class EventNotificationServiceImpl(queueCapacity: Int) extends EventNotificationService: - private[this] val queueMap = mutable.Map.empty[String, Queue[Event[_]]] +class EventNotificationServiceImpl(queueMap: ConcurrentMap[String, Queue[Event[_]]], queueCapacity: Int) + extends EventNotificationService: private[this] def getOrCreateQueue(topic: String): IO[EventNotificationServiceError, Queue[Event[_]]] = { for { - maybeQueue <- ZIO.succeed(queueMap.get(topic)) + maybeQueue <- queueMap.get(topic) queue <- maybeQueue match case Some(value) => ZIO.succeed(value) case None => Queue.bounded(queueCapacity) - _ <- ZIO.succeed(queueMap.put(topic, queue)) + _ <- queueMap.put(topic, queue) } yield queue } @@ -42,5 +41,10 @@ class EventNotificationServiceImpl(queueCapacity: Int) extends EventNotification object EventNotificationServiceImpl { val layer: URLayer[Int, EventNotificationServiceImpl] = - ZLayer.fromFunction(new EventNotificationServiceImpl(_)) + ZLayer.fromZIO( + for { + map <- ConcurrentMap.make[String, Queue[Event[_]]]() + capacity <- ZIO.service[Int] + } yield new EventNotificationServiceImpl(map, capacity) + ) } diff --git a/event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala b/event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala index 1752c9de2c..6c07025ad0 100644 --- a/event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala +++ b/event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala @@ -14,10 +14,10 @@ object EventNotificationServiceImplSpec extends ZIOSpecDefault { svc <- ZIO.service[EventNotificationService] producer <- svc.producer[String]("TopicA") consumer <- svc.consumer[String]("TopicA") - _ <- producer.send(Event("event #1")) - _ <- producer.send(Event("event #2")) + _ <- producer.send(Event("Foo", "event #1")) + _ <- producer.send(Event("Foo", "event #2")) events <- consumer.poll(2) - } yield assertTrue(events == Seq(Event("event #1"), Event("event #2"))) + } yield assertTrue(events.map(_.data) == Seq("event #1", "event #2")) }, test("should not mix-up events from different topics") { for { @@ -26,16 +26,16 @@ object EventNotificationServiceImplSpec extends ZIOSpecDefault { consumerA <- svc.consumer[String]("TopicA") producerB <- svc.producer[String]("TopicB") consumerB <- svc.consumer[String]("TopicB") - _ <- producerA.send(Event("event #1")) - _ <- producerA.send(Event("event #2")) - _ <- producerB.send(Event("event #3")) + _ <- producerA.send(Event("Foo", "event #1")) + _ <- producerA.send(Event("Foo", "event #2")) + _ <- producerB.send(Event("Foo", "event #3")) eventsA <- consumerA.poll(5) eventsB <- consumerB.poll(5) } yield { assertTrue(eventsA.size == 2) && assertTrue(eventsB.size == 1) && - assertTrue(eventsA == Seq(Event("event #1"), Event("event #2"))) && - assertTrue(eventsB == Seq(Event("event #3"))) + assertTrue(eventsA.map(_.data) == Seq("event #1", "event #2")) && + assertTrue(eventsB.map(_.data) == Seq("event #3")) } }, test("should only deliver the requested messages number to a consumer") { @@ -43,21 +43,21 @@ object EventNotificationServiceImplSpec extends ZIOSpecDefault { svc <- ZIO.service[EventNotificationService] producer <- svc.producer[String]("TopicA") consumer <- svc.consumer[String]("TopicA") - _ <- producer.send(Event("event #1")) - _ <- producer.send(Event("event #2")) + _ <- producer.send(Event("Foo", "event #1")) + _ <- producer.send(Event("Foo", "event #2")) events <- consumer.poll(1) - } yield assertTrue(events == Seq(Event("event #1"))) + } yield assertTrue(events.map(_.data) == Seq("event #1")) }, test("should remove consumed messages from the queue") { for { svc <- ZIO.service[EventNotificationService] producer <- svc.producer[String]("TopicA") consumer <- svc.consumer[String]("TopicA") - _ <- producer.send(Event("event #1")) - _ <- producer.send(Event("event #2")) + _ <- producer.send(Event("Foo", "event #1")) + _ <- producer.send(Event("Foo", "event #2")) _ <- consumer.poll(1) events <- consumer.poll(1) - } yield assertTrue(events == Seq(Event("event #2"))) + } yield assertTrue(events.map(_.data) == Seq("event #2")) }, test("should send event even when consumer is created and polling first") { for { @@ -67,18 +67,18 @@ object EventNotificationServiceImplSpec extends ZIOSpecDefault { consumerFiber <- consumer.poll(1).fork // Producing in another fiber, after 3 seconds producer <- svc.producer[String]("TopicA") - producerFiber <- producer.send(Event("event #1")).delay(3.seconds).fork + producerFiber <- producer.send(Event("Foo", "event #1")).delay(3.seconds).fork _ <- TestClock.adjust(3.seconds) events <- consumerFiber.join _ <- producerFiber.join - } yield assertTrue(events == Seq(Event("event #1"))) + } yield assertTrue(events.map(_.data) == Seq("event #1")) }, test("should block on sending new messages when queue is full") { for { svc <- ZIO.service[EventNotificationService] producer <- svc.producer[String]("TopicA") - _ <- ZIO.collectAll((1 to 10).map(i => producer.send(Event(s"event #$i")))) - fiber <- producer.send(Event("One more event")).timeout(5.seconds).fork + _ <- ZIO.collectAll((1 to 10).map(i => producer.send(Event("Foo", s"event #$i")))) + fiber <- producer.send(Event("Foo", "One more event")).timeout(5.seconds).fork _ <- TestClock.adjust(5.seconds) res <- fiber.join } yield assertTrue(res.isEmpty) From 45348b28dcba1c1cbf1cf1627d0180afadafe3d1 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Mon, 10 Jul 2023 13:26:10 +0200 Subject: [PATCH 51/56] chore(prism-agent): fixing last PR comments --- docs/docusaurus/webhooks/webhook.md | 6 +++--- .../io/iohk/atala/agent/notification/WebhookPublisher.scala | 2 +- .../src/main/scala/io/iohk/atala/agent/server/Main.scala | 3 ++- .../scala/io/iohk/atala/agent/server/PrismAgentApp.scala | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/docusaurus/webhooks/webhook.md b/docs/docusaurus/webhooks/webhook.md index 32872f9e8c..ef638b4ad3 100644 --- a/docs/docusaurus/webhooks/webhook.md +++ b/docs/docusaurus/webhooks/webhook.md @@ -34,9 +34,9 @@ monitor the progress of the main flows, receiving timely updates about changes a PRISM Agent uses the following environment variables to manage webhook notifications: | Name | Description | Default | -|-------------------|--------------------------------------------------------------------------|---------| -| `WEBHOOK_URL` | The webhook endpoint URL where the notifications will be sent | null | -| `WEBHOOK_API_KEY` | The optional API key (bearer token) to use as the `Authorization` header | `vault` | +|-------------------|--------------------------------------------------------------------------|--------| +| `WEBHOOK_URL` | The webhook endpoint URL where the notifications will be sent | null | +| `WEBHOOK_API_KEY` | The optional API key (bearer token) to use as the `Authorization` header | null | ### Securing the Webhook Endpoint diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index 4ebc8f2c60..4c2cea8bc8 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -68,7 +68,7 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat encoder: JsonEncoder[A] ): ZIO[Client, UnexpectedError, Unit] = { for { - _ <- ZIO.log(s"Sending event: $event to HTTP webhook URL: $url with API key ${config.apiKey}") + _ <- ZIO.log(s"Sending event: $event to HTTP webhook URL: $url.") response <- Client .request( url = url.toString, diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index e4510227b9..56c4e0cd14 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -150,7 +150,8 @@ object MainApp extends ZIOAppDefault { // event notification service ZLayer.succeed(500) >>> EventNotificationServiceImpl.layer, // HTTP client - Scope.default >>> Client.default, + Client.default, + Scope.default ) } yield app diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala index 4c004946fa..dd195afe5a 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala @@ -31,7 +31,7 @@ object PrismAgentApp { _ <- syncDIDPublicationStateFromDltJob.fork _ <- AgentHttpServer.run.fork fiber <- DidCommHttpServer.run(didCommServicePort).fork - _ <- ZIO.scoped(WebhookPublisher.layer.build.map(_.get[WebhookPublisher])).flatMap(_.run.debug.fork) + _ <- WebhookPublisher.layer.build.map(_.get[WebhookPublisher]).flatMap(_.run.debug.fork) _ <- fiber.join *> ZIO.log(s"Server End") _ <- ZIO.never } yield () From 93cf3bd2d7608c8dc55e4bb420a6340983861be5 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Mon, 10 Jul 2023 14:02:29 +0200 Subject: [PATCH 52/56] doc(prism-agent): document DID-related events in webhook.md --- docs/docusaurus/webhooks/webhook.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/docusaurus/webhooks/webhook.md b/docs/docusaurus/webhooks/webhook.md index ef638b4ad3..30a906c1a3 100644 --- a/docs/docusaurus/webhooks/webhook.md +++ b/docs/docusaurus/webhooks/webhook.md @@ -20,8 +20,9 @@ systems. Webhook notifications in PRISM Agent serve as a vital feature, enabling you to receive timely updates on various events occurring within the agent. Webhooks allow you to receive HTTP requests containing event details at a specified endpoint (webhook URL). These events are specifically related to the execution of -the [Connect](../connections/connection.md), [Issue](../credentials/issue.md), -and [Presentation](../credentials/present-proof.md) flows. Webhook notifications will be sent each time there is a state +the [Connect](/tutorials/connections/connection), [Issue](/tutorials/credentials/issue), +and [Presentation](/tutorials/credentials/present-proof) flows. Webhook notifications will be sent each time there is a +state change during the execution of these protocols. By leveraging webhooks, you can integrate PRISM Agent seamlessly into your applications and systems. You can track and @@ -34,9 +35,9 @@ monitor the progress of the main flows, receiving timely updates about changes a PRISM Agent uses the following environment variables to manage webhook notifications: | Name | Description | Default | -|-------------------|--------------------------------------------------------------------------|--------| -| `WEBHOOK_URL` | The webhook endpoint URL where the notifications will be sent | null | -| `WEBHOOK_API_KEY` | The optional API key (bearer token) to use as the `Authorization` header | null | +|-------------------|--------------------------------------------------------------------------|---------| +| `WEBHOOK_URL` | The webhook endpoint URL where the notifications will be sent | null | +| `WEBHOOK_API_KEY` | The optional API key (bearer token) to use as the `Authorization` header | null | ### Securing the Webhook Endpoint @@ -107,23 +108,25 @@ Here is an example of a webhook notification event related to a connection flow ### Common Event Types PRISM Agent sends webhook notifications for events related to protocol state changes in -the [Connect](../connections/connection.md), [Issue](../credentials/issue.md), -and [Presentation](../credentials/present-proof.md) flows. These events allow you to track the progress and updates +the [Connect](/tutorials/connections/connection), [Issue](/tutorials/credentials/issue), +and [Presentation](/tutorials/credentials/present-proof) flows. These events allow you to track the progress and updates within these flows in real-time. Some common event types that you can expect to receive through webhook notifications include: - Connection State Change: Notifies about state changes in the connection flow, such as `InvitationGenerated`, `ConnectionRequestSent`, `ConnectionResponseReceived`, etc. Please refer to the `state` field of - the [connection resource](https://docs.atalaprism.io/agent-api/#tag/Connections-Management/operation/getConnection) + the [connection resource](agent-api/#tag/Connections-Management/operation/getConnection) for an exhaustive list of states. - Credential State Change: Indicates changes in the credential issuance flow, such as `OfferSent`, `RequestReceived`, `CredentialSent`, etc. Please refer to the `protocolState` field of - the [credential resource](https://docs.atalaprism.io/agent-api/#tag/Issue-Credentials-Protocol/operation/getCredentialRecord) + the [credential resource](agent-api/#tag/Issue-Credentials-Protocol/operation/getCredentialRecord) for an exhaustive list of states. - Presentation State Change: Notifies about changes in the presentation flow, such as `RequestReceived`, `PresentationGenerated`, `PresentationVerified`, etc. Please refer to the `status` field of - the [presentation resource](https://docs.atalaprism.io/agent-api/#tag/Present-Proof/operation/getPresentation) for an + the [presentation resource](agent-api/#tag/Present-Proof/operation/getPresentation) for an exhaustive list of states. +- DID State Change: Notifies about DID-related state changes. Currently, only the `Published` DID publication state + event will be notified. ## Processing Webhook Notifications From 42bfd82be91536a05991e71d43e86016834cf752 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Mon, 10 Jul 2023 14:40:06 +0200 Subject: [PATCH 53/56] fix(prism-agent): use ZIO sliding queue (discarding old messages) instead of a bounded one --- .../atala/event/notification/EventNotificationServiceImpl.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala index 26db2fed2b..55a138c8df 100644 --- a/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala +++ b/event-notification/src/main/scala/io/iohk/atala/event/notification/EventNotificationServiceImpl.scala @@ -12,7 +12,7 @@ class EventNotificationServiceImpl(queueMap: ConcurrentMap[String, Queue[Event[_ maybeQueue <- queueMap.get(topic) queue <- maybeQueue match case Some(value) => ZIO.succeed(value) - case None => Queue.bounded(queueCapacity) + case None => Queue.sliding(queueCapacity) _ <- queueMap.put(topic, queue) } yield queue } From 94c273d9c6bdf822098afa7271bd7e367ce586db Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Mon, 10 Jul 2023 14:45:52 +0200 Subject: [PATCH 54/56] feat(prism-agent): use a 5 seconds request timeout in Webhook publisher --- .../io/iohk/atala/agent/notification/WebhookPublisher.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala index 4c2cea8bc8..81fc65d7e3 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/notification/WebhookPublisher.scala @@ -76,6 +76,7 @@ class WebhookPublisher(appConfig: AppConfig, notificationService: EventNotificat headers = baseHeaders, content = Body.fromString(event.toJson) ) + .timeoutFail(new RuntimeException("Client request timed out"))(5.seconds) .mapError(t => UnexpectedError(s"Webhook request error: $t")) resp <- response match case Response(status, _, _, _, _) if status.isSuccess => From 1d8b2f2745bcc8af9cdf7742e51431ab98f05261 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Mon, 10 Jul 2023 14:54:48 +0200 Subject: [PATCH 55/56] test(prism-agent): fix unit test for sliding queue --- .../EventNotificationServiceImplSpec.scala | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala b/event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala index 6c07025ad0..9e5dbf63f5 100644 --- a/event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala +++ b/event-notification/src/test/scala/io/iohk/atala/event/notification/EventNotificationServiceImplSpec.scala @@ -73,15 +73,19 @@ object EventNotificationServiceImplSpec extends ZIOSpecDefault { _ <- producerFiber.join } yield assertTrue(events.map(_.data) == Seq("event #1")) }, - test("should block on sending new messages when queue is full") { + test("should drop old items when sending new messages and queue is full") { for { svc <- ZIO.service[EventNotificationService] producer <- svc.producer[String]("TopicA") + consumer <- svc.consumer[String]("TopicA") _ <- ZIO.collectAll((1 to 10).map(i => producer.send(Event("Foo", s"event #$i")))) - fiber <- producer.send(Event("Foo", "One more event")).timeout(5.seconds).fork - _ <- TestClock.adjust(5.seconds) - res <- fiber.join - } yield assertTrue(res.isEmpty) + _ <- producer.send(Event("Foo", "One more event")) + events <- consumer.poll(10) + } yield { + assertTrue(events.size == 10) && + assertTrue(events.head.data == "event #2") && + assertTrue(events(9).data == "One more event") + } }, test("should block on reading new messages when queue is empty") { for { From df46486ed3a0b134a0230c8da42b3cf509cf56f2 Mon Sep 17 00:00:00 2001 From: Benjamin Voiturier Date: Mon, 10 Jul 2023 16:01:54 +0200 Subject: [PATCH 56/56] chore(prism-agent): fix URLs in webhook.md --- docs/docusaurus/webhooks/webhook.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docusaurus/webhooks/webhook.md b/docs/docusaurus/webhooks/webhook.md index 30a906c1a3..123acb9df4 100644 --- a/docs/docusaurus/webhooks/webhook.md +++ b/docs/docusaurus/webhooks/webhook.md @@ -115,15 +115,15 @@ include: - Connection State Change: Notifies about state changes in the connection flow, such as `InvitationGenerated`, `ConnectionRequestSent`, `ConnectionResponseReceived`, etc. Please refer to the `state` field of - the [connection resource](agent-api/#tag/Connections-Management/operation/getConnection) + the [connection resource](/agent-api/#tag/Connections-Management/operation/getConnection) for an exhaustive list of states. - Credential State Change: Indicates changes in the credential issuance flow, such as `OfferSent`, `RequestReceived`, `CredentialSent`, etc. Please refer to the `protocolState` field of - the [credential resource](agent-api/#tag/Issue-Credentials-Protocol/operation/getCredentialRecord) + the [credential resource](/agent-api/#tag/Issue-Credentials-Protocol/operation/getCredentialRecord) for an exhaustive list of states. - Presentation State Change: Notifies about changes in the presentation flow, such as `RequestReceived`, `PresentationGenerated`, `PresentationVerified`, etc. Please refer to the `status` field of - the [presentation resource](agent-api/#tag/Present-Proof/operation/getPresentation) for an + the [presentation resource](/agent-api/#tag/Present-Proof/operation/getPresentation) for an exhaustive list of states. - DID State Change: Notifies about DID-related state changes. Currently, only the `Published` DID publication state event will be notified.