• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / atala-prism-building-blocks / 4831639238

28 Apr 2023 02:26PM UTC coverage: 23.321% (-0.6%) from 23.964%
4831639238

Pull #516

David Poltorak
feat: migrate issue endpoint to tapir
Pull Request #516: feat: migrate issue endpoint to tapir

280 of 280 new or added lines in 10 files covered. (100.0%)

2042 of 8756 relevant lines covered (23.32%)

0.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueControllerImpl.scala
1
package io.iohk.atala.issue.controller
2

3
import io.iohk.atala.agent.server.config.AppConfig
4
import io.iohk.atala.agent.server.http.model.HttpServiceError
5
import io.iohk.atala.agent.server.http.model.HttpServiceError.InvalidPayload
6
import io.iohk.atala.agent.walletapi.service.ManagedDIDService
7
import io.iohk.atala.api.http.model.{Pagination, PaginationInput}
8
import io.iohk.atala.api.http.{ErrorResponse, RequestContext}
9
import io.iohk.atala.castor.core.model.did.PrismDID
10
import io.iohk.atala.connect.controller.ConnectionController
11
import io.iohk.atala.connect.core.model.ConnectionRecord
12
import io.iohk.atala.connect.core.model.ConnectionRecord.{ProtocolState, Role}
13
import io.iohk.atala.connect.core.model.error.ConnectionServiceError
14
import io.iohk.atala.connect.core.service.ConnectionService
15
import io.iohk.atala.issue.controller.IssueController.toHttpError
16
import io.iohk.atala.issue.controller.http.{
17
  AcceptCredentialOfferRequest,
18
  CreateIssueCredentialRecordRequest,
19
  IssueCredentialRecord,
20
  IssueCredentialRecordPage
21
}
22
import io.iohk.atala.mercury.model.DidId
23
import io.iohk.atala.pollux.core.service.CredentialService
24
import io.iohk.atala.pollux.core.model.DidCommID
25
import io.iohk.atala.pollux.core.model.error.CredentialServiceError
26
import zio.{IO, URLayer, ZIO, ZLayer}
27

28
import java.util.UUID
29
import scala.util.Try
30

31
class IssueControllerImpl(
32
    credentialService: CredentialService,
33
    connectionService: ConnectionService,
34
    appConfig: AppConfig
35
) extends IssueController {
×
36
  override def createCredentialOffer(
37
      request: CreateIssueCredentialRecordRequest
38
  )(implicit rc: RequestContext): IO[ErrorResponse, IssueCredentialRecord] = {
×
39
    val result: IO[ConnectionServiceError | CredentialServiceError | InvalidPayload, IssueCredentialRecord] = for {
×
40
      didIdPair <- getPairwiseDIDs(request.connectionId)
×
41
      issuingDID <- extractPrismDIDFromString(request.issuingDID)
×
42
      outcome <- credentialService
×
43
        .createIssueCredentialRecord(
44
          pairwiseIssuerDID = didIdPair.myDID,
45
          pairwiseHolderDID = didIdPair.theirDid,
×
46
          thid = DidCommID(),
47
          schemaId = None,
48
          claims = request.claims,
49
          validityPeriod = request.validityPeriod,
×
50
          automaticIssuance = request.automaticIssuance.orElse(Some(true)),
51
          awaitConfirmation = Some(false),
×
52
          issuingDID = Some(issuingDID.asCanonical)
53
        )
×
54
    } yield IssueCredentialRecord.fromDomain(outcome)
×
55
    mapIssueErrors(result)
56
  }
57

58
  // TODO - Tech Debt - Do not filter this in memory - need to filter at the database level
59
  // TODO - Tech Debt - Implement pagination
×
60
  override def getCredentialRecords(paginationInput: PaginationInput, thid: Option[String])(implicit
61
      rc: RequestContext
62
  ): IO[ErrorResponse, IssueCredentialRecordPage] = {
×
63
    val result = for {
×
64
      records <- credentialService.getIssueCredentialRecords()
65
      outcome = thid match
×
66
        case None        => records
×
67
        case Some(value) => records.filter(_.thid.value == value) // this logic should be moved to the DB
68
    } yield IssueCredentialRecordPage(
69
      self = "/issue-credentials/records",
70
      kind = "Collection",
71
      pageOf = "1",
72
      next = None,
73
      previous = None,
×
74
      contents = (outcome map IssueCredentialRecord.fromDomain) // TODO - Tech Debt - Optimise this transformation - each time we get a list of things we iterate it once here
75
    )
×
76
    mapIssueErrors(result)
77
  }
78

×
79
  override def getCredentialRecord(
80
      recordId: String
81
  )(implicit rc: RequestContext): IO[ErrorResponse, IssueCredentialRecord] = {
×
82
    val result: IO[CredentialServiceError | InvalidPayload, Option[IssueCredentialRecord]] = for {
×
83
      id <- extractDidCommIdFromString(recordId)
×
84
      outcome <- credentialService.getIssueCredentialRecord(id)
×
85
    } yield (outcome map IssueCredentialRecord.fromDomain)
×
86
    mapIssueErrors(result) someOrFail toHttpError(
×
87
      CredentialServiceError.RecordIdNotFound(DidCommID(recordId))
88
    ) // TODO - Tech Debt - Review if this is safe. Currently is because DidCommID is opaque type => string with no validation
89
  }
90

×
91
  override def acceptCredentialOffer(recordId: String, request: AcceptCredentialOfferRequest)(implicit
92
      rc: RequestContext
93
  ): IO[ErrorResponse, IssueCredentialRecord] = {
×
94
    val result: IO[CredentialServiceError | InvalidPayload, IssueCredentialRecord] = for {
×
95
      id <- extractDidCommIdFromString(recordId)
×
96
      outcome <- credentialService.acceptCredentialOffer(id, request.subjectId)
×
97
    } yield IssueCredentialRecord.fromDomain(outcome)
×
98
    mapIssueErrors(result)
99
  }
100

×
101
  override def issueCredential(
102
      recordId: String
103
  )(implicit rc: RequestContext): IO[ErrorResponse, IssueCredentialRecord] = {
×
104
    val result: IO[InvalidPayload | CredentialServiceError, IssueCredentialRecord] = for {
×
105
      id <- extractDidCommIdFromString(recordId)
×
106
      outcome <- credentialService.acceptCredentialRequest(id)
×
107
    } yield IssueCredentialRecord.fromDomain(outcome)
×
108
    mapIssueErrors(result)
109
  }
110

×
111
  private def mapIssueErrors[T](
112
      result: IO[CredentialServiceError | ConnectionServiceError | InvalidPayload, T]
113
  ): IO[ErrorResponse, T] = {
×
114
    result mapError {
×
115
      case invalidPayload: InvalidPayload =>
116
        ErrorResponse(
117
          status = 422,
118
          `type` = "InvalidPayload",
119
          title = "error-title",
120
          detail = Some(invalidPayload.msg),
121
          instance = "error-instance"
122
        )
×
123
      case connError: ConnectionServiceError =>
×
124
        ConnectionController.toHttpError(connError)
×
125
      case credError: CredentialServiceError =>
×
126
        toHttpError(credError)
127
    }
128
  }
129

130
  private[this] case class DidIdPair(myDID: DidId, theirDid: DidId)
131

×
132
  private[this] def extractDidCommIdFromString(
133
      maybeDidCommId: String
134
  ): IO[InvalidPayload, io.iohk.atala.pollux.core.model.DidCommID] = {
×
135
    ZIO
×
136
      .fromTry(Try(io.iohk.atala.pollux.core.model.DidCommID(maybeDidCommId)))
×
137
      .mapError(e => HttpServiceError.InvalidPayload(s"Error parsing string as DidCommID: ${e.getMessage}"))
138
  }
139

×
140
  private[this] def extractPrismDIDFromString(maybeDid: String): IO[InvalidPayload, PrismDID] = {
×
141
    ZIO
×
142
      .fromEither(PrismDID.fromString(maybeDid))
×
143
      .mapError(e => HttpServiceError.InvalidPayload(s"Error parsing string as PrismDID: ${e}"))
144
  }
145

×
146
  private[this] def extractDidIdPairFromValidConnection(connRecord: ConnectionRecord): Option[DidIdPair] = {
147
    (connRecord.protocolState, connRecord.connectionResponse, connRecord.role) match {
×
148
      case (ProtocolState.ConnectionResponseReceived, Some(resp), Role.Invitee) =>
149
        // If Invitee, myDid is the target
150
        Some(DidIdPair(resp.to, resp.from))
×
151
      case (ProtocolState.ConnectionResponseSent, Some(resp), Role.Inviter) =>
152
        // If Inviter, myDid is the source
153
        Some(DidIdPair(resp.from, resp.to))
×
154
      case _ => None
155
    }
156
  }
157

×
158
  private[this] def getPairwiseDIDs(connectionId: String): IO[ConnectionServiceError, DidIdPair] = {
×
159
    val lookupId = UUID.fromString(connectionId)
×
160
    for {
×
161
      maybeConnection <- connectionService.getConnectionRecord(lookupId)
×
162
      didIdPair <- maybeConnection match
×
163
        case Some(connRecord: ConnectionRecord) =>
×
164
          extractDidIdPairFromValidConnection(connRecord) match {
×
165
            case Some(didIdPair: DidIdPair) => ZIO.succeed(didIdPair)
×
166
            case None =>
×
167
              ZIO.fail(ConnectionServiceError.UnexpectedError("Invalid connection record state for operation"))
168
          }
×
169
        case _ => ZIO.fail(ConnectionServiceError.RecordIdNotFound(lookupId))
170
    } yield didIdPair
171
  }
172

173
}
174

175
object IssueControllerImpl {
176
  val layer: URLayer[CredentialService & ConnectionService & AppConfig, IssueController] =
×
177
    ZLayer.fromFunction(IssueControllerImpl(_, _, _))
178
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc