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

hyperledger / identus-cloud-agent / 11407164708

18 Oct 2024 04:02PM UTC coverage: 48.619% (-0.1%) from 48.741%
11407164708

Pull #1400

FabioPinheiro
build: add job Update Dependency Graph

Signed-off-by: FabioPinheiro <fabiomgpinheiro@gmail.com>
Pull Request #1400: build: add job Update Dependency Graph

7867 of 16181 relevant lines covered (48.62%)

0.49 hits per line

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

71.62
/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala
1
package org.hyperledger.identus.pollux.core.service
2

3
import cats.implicits.*
4
import io.circe.*
5
import io.circe.parser.*
6
import io.circe.syntax.*
7
import org.hyperledger.identus.agent.walletapi.model.{ManagedDIDState, PublicationState}
8
import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService
9
import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage
10
import org.hyperledger.identus.castor.core.model.did.*
11
import org.hyperledger.identus.castor.core.service.DIDService
12
import org.hyperledger.identus.mercury.model.*
13
import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation
14
import org.hyperledger.identus.mercury.protocol.issuecredential.*
15
import org.hyperledger.identus.pollux.*
16
import org.hyperledger.identus.pollux.anoncreds.*
17
import org.hyperledger.identus.pollux.core.model.*
18
import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError
19
import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError.*
20
import org.hyperledger.identus.pollux.core.model.presentation.*
21
import org.hyperledger.identus.pollux.core.model.schema.{CredentialDefinition, CredentialSchema}
22
import org.hyperledger.identus.pollux.core.model.secret.CredentialDefinitionSecret
23
import org.hyperledger.identus.pollux.core.model.CredentialFormat.AnonCreds
24
import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.ProtocolState.OfferReceived
25
import org.hyperledger.identus.pollux.core.repository.{CredentialRepository, CredentialStatusListRepository}
26
import org.hyperledger.identus.pollux.prex.{ClaimFormat, Jwt, PresentationDefinition}
27
import org.hyperledger.identus.pollux.sdjwt.*
28
import org.hyperledger.identus.pollux.vc.jwt.{Issuer as JwtIssuer, *}
29
import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Secp256k1KeyPair}
30
import org.hyperledger.identus.shared.http.UriResolver
31
import org.hyperledger.identus.shared.models.*
32
import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect
33
import org.hyperledger.identus.shared.utils.Base64Utils
34
import zio.*
35
import zio.json.*
36
import zio.prelude.ZValidation
37

38
import java.time.{Instant, ZoneId}
39
import java.util.UUID
40
import scala.language.implicitConversions
41

42
object CredentialServiceImpl {
43
  val layer: URLayer[
44
    CredentialRepository & CredentialStatusListRepository & DidResolver & UriResolver & GenericSecretStorage &
45
      CredentialDefinitionService & LinkSecretService & DIDService & ManagedDIDService,
46
    CredentialService
47
  ] = {
48
    ZLayer.fromZIO {
1✔
49
      for {
1✔
50
        credentialRepo <- ZIO.service[CredentialRepository]
1✔
51
        credentialStatusListRepo <- ZIO.service[CredentialStatusListRepository]
1✔
52
        didResolver <- ZIO.service[DidResolver]
1✔
53
        uriResolver <- ZIO.service[UriResolver]
1✔
54
        genericSecretStorage <- ZIO.service[GenericSecretStorage]
1✔
55
        credDefenitionService <- ZIO.service[CredentialDefinitionService]
1✔
56
        linkSecretService <- ZIO.service[LinkSecretService]
1✔
57
        didService <- ZIO.service[DIDService]
1✔
58
        manageDidService <- ZIO.service[ManagedDIDService]
1✔
59
        issueCredentialSem <- Semaphore.make(1)
1✔
60
      } yield CredentialServiceImpl(
1✔
61
        credentialRepo,
62
        credentialStatusListRepo,
63
        didResolver,
64
        uriResolver,
65
        genericSecretStorage,
66
        credDefenitionService,
67
        linkSecretService,
68
        didService,
69
        manageDidService,
70
        5,
71
        issueCredentialSem
72
      )
73
    }
74
  }
75

76
  //  private val VC_JSON_SCHEMA_URI = "https://w3c-ccg.github.io/vc-json-schemas/schema/2.0/schema.json"
77
  private val VC_JSON_SCHEMA_TYPE = "CredentialSchema2022"
78
}
79

80
class CredentialServiceImpl(
81
    credentialRepository: CredentialRepository,
82
    credentialStatusListRepository: CredentialStatusListRepository,
83
    didResolver: DidResolver,
84
    uriResolver: UriResolver,
85
    genericSecretStorage: GenericSecretStorage,
86
    credentialDefinitionService: CredentialDefinitionService,
87
    linkSecretService: LinkSecretService,
88
    didService: DIDService,
89
    managedDIDService: ManagedDIDService,
90
    maxRetries: Int = 5, // TODO move to config
×
91
    issueCredentialSem: Semaphore
92
) extends CredentialService {
93

94
  import CredentialServiceImpl.*
95
  import IssueCredentialRecord.*
96

97
  override def getIssueCredentialRecords(
1✔
98
      ignoreWithZeroRetries: Boolean,
99
      offset: Option[Int],
100
      limit: Option[Int]
101
  ): URIO[WalletAccessContext, (Seq[IssueCredentialRecord], Int)] =
102
    credentialRepository.findAll(ignoreWithZeroRetries = ignoreWithZeroRetries, offset = offset, limit = limit)
1✔
103

104
  override def getIssueCredentialRecordByThreadId(
×
105
      thid: DidCommID,
106
      ignoreWithZeroRetries: Boolean
107
  ): URIO[WalletAccessContext, Option[IssueCredentialRecord]] =
108
    credentialRepository.findByThreadId(thid, ignoreWithZeroRetries)
×
109

110
  override def findById(
1✔
111
      recordId: DidCommID
112
  ): URIO[WalletAccessContext, Option[IssueCredentialRecord]] =
113
    credentialRepository.findById(recordId)
1✔
114

115
  override def getById(
×
116
      recordId: DidCommID
117
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] =
118
    for {
×
119
      maybeRecord <- credentialRepository.findById(recordId)
×
120
      record <- ZIO
×
121
        .fromOption(maybeRecord)
122
        .mapError(_ => RecordNotFound(recordId))
×
123
    } yield record
124

125
  private def createIssueCredentialRecord(
1✔
126
      pairwiseIssuerDID: DidId,
127
      kidIssuer: Option[KeyId],
128
      thid: DidCommID,
129
      schemaUris: Option[List[String]],
130
      validityPeriod: Option[Double],
131
      automaticIssuance: Option[Boolean],
132
      issuingDID: Option[CanonicalPrismDID],
133
      credentialFormat: CredentialFormat,
134
      offer: OfferCredential,
135
      credentialDefinitionGUID: Option[UUID] = None,
×
136
      credentialDefinitionId: Option[String] = None,
×
137
      connectionId: Option[UUID],
138
      goalCode: Option[String],
139
      goal: Option[String],
140
      expirationDuration: Option[Duration],
141
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
142
    for {
1✔
143
      invitation <- ZIO.succeed(
144
        connectionId.fold(
145
          Some(
146
            IssueCredentialInvitation.makeInvitation(
×
147
              pairwiseIssuerDID,
148
              goalCode,
149
              goal,
150
              thid.value,
×
151
              offer,
152
              expirationDuration
153
            )
154
          )
155
        )(_ => None)
1✔
156
      )
157
      record <- ZIO.succeed(
1✔
158
        IssueCredentialRecord(
159
          id = DidCommID(),
1✔
160
          createdAt = Instant.now,
1✔
161
          updatedAt = None,
162
          thid = thid,
163
          schemaUris = schemaUris,
164
          credentialDefinitionId = credentialDefinitionGUID,
165
          credentialDefinitionUri = credentialDefinitionId,
166
          credentialFormat = credentialFormat,
167
          invitation = invitation,
168
          role = IssueCredentialRecord.Role.Issuer,
169
          subjectId = None,
170
          keyId = kidIssuer,
171
          validityPeriod = validityPeriod,
172
          automaticIssuance = automaticIssuance,
173
          protocolState = invitation.fold(IssueCredentialRecord.ProtocolState.OfferPending)(_ =>
1✔
174
            IssueCredentialRecord.ProtocolState.InvitationGenerated
175
          ),
176
          offerCredentialData = Some(offer),
177
          requestCredentialData = None,
178
          anonCredsRequestMetadata = None,
179
          issueCredentialData = None,
180
          issuedCredentialRaw = None,
181
          issuingDID = issuingDID,
182
          metaRetries = maxRetries,
183
          metaNextRetry = Some(Instant.now()),
1✔
184
          metaLastFailure = None,
185
        )
186
      )
187
      count <- credentialRepository
1✔
188
        .create(record) @@ CustomMetricsAspect
1✔
189
        .startRecordingTime(s"${record.id}_issuer_offer_pending_to_sent_ms_gauge")
1✔
190
    } yield record
191
  }
192

193
  override def createJWTIssueCredentialRecord(
1✔
194
      pairwiseIssuerDID: DidId,
195
      pairwiseHolderDID: Option[DidId],
196
      kidIssuer: Option[KeyId],
197
      thid: DidCommID,
198
      maybeSchemaIds: Option[List[String]],
199
      claims: Json,
200
      validityPeriod: Option[Double],
201
      automaticIssuance: Option[Boolean],
202
      issuingDID: CanonicalPrismDID,
203
      goalCode: Option[String],
204
      goal: Option[String],
205
      expirationDuration: Option[Duration],
206
      connectionId: Option[UUID],
207
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
208
    for {
1✔
209
      _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaIds)
1✔
210
      attributes <- CredentialService.convertJsonClaimsToAttributes(claims)
1✔
211
      offer <- createDidCommOfferCredential(
1✔
212
        pairwiseIssuerDID = pairwiseIssuerDID,
213
        pairwiseHolderDID = pairwiseHolderDID,
214
        maybeSchemaIds = maybeSchemaIds,
215
        claims = attributes,
216
        thid = thid,
217
        UUID.randomUUID().toString,
1✔
218
        "domain",
219
        IssueCredentialOfferFormat.JWT
220
      )
221
      record <- createIssueCredentialRecord(
1✔
222
        pairwiseIssuerDID = pairwiseIssuerDID,
223
        kidIssuer = kidIssuer,
224
        thid = thid,
225
        schemaUris = maybeSchemaIds,
226
        validityPeriod = validityPeriod,
227
        automaticIssuance = automaticIssuance,
228
        issuingDID = Some(issuingDID),
229
        credentialFormat = CredentialFormat.JWT,
230
        offer = offer,
231
        credentialDefinitionGUID = None,
232
        credentialDefinitionId = None,
233
        connectionId = connectionId,
234
        goalCode = goalCode,
235
        goal = goal,
236
        expirationDuration = expirationDuration,
237
      )
238
    } yield record
239
  }
240

241
  override def createSDJWTIssueCredentialRecord(
×
242
      pairwiseIssuerDID: DidId,
243
      pairwiseHolderDID: Option[DidId],
244
      kidIssuer: Option[KeyId],
245
      thid: DidCommID,
246
      maybeSchemaIds: Option[List[String]],
247
      claims: io.circe.Json,
248
      validityPeriod: Option[Double] = None,
×
249
      automaticIssuance: Option[Boolean],
250
      issuingDID: CanonicalPrismDID,
251
      goalCode: Option[String],
252
      goal: Option[String],
253
      expirationDuration: Option[Duration],
254
      connectionId: Option[UUID],
255
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
256
    for {
×
257
      _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaIds)
×
258
      attributes <- CredentialService.convertJsonClaimsToAttributes(claims)
×
259
      offer <- createDidCommOfferCredential(
×
260
        pairwiseIssuerDID = pairwiseIssuerDID,
261
        pairwiseHolderDID = pairwiseHolderDID,
262
        maybeSchemaIds = maybeSchemaIds,
263
        claims = attributes,
264
        thid = thid,
265
        UUID.randomUUID().toString,
×
266
        "domain",
267
        IssueCredentialOfferFormat.SDJWT
268
      )
269
      record <- createIssueCredentialRecord(
×
270
        pairwiseIssuerDID = pairwiseIssuerDID,
271
        kidIssuer = kidIssuer,
272
        thid = thid,
273
        schemaUris = maybeSchemaIds,
274
        validityPeriod = validityPeriod,
275
        automaticIssuance = automaticIssuance,
276
        issuingDID = Some(issuingDID),
277
        credentialFormat = CredentialFormat.SDJWT,
278
        offer = offer,
279
        credentialDefinitionGUID = None,
280
        credentialDefinitionId = None,
281
        connectionId = connectionId,
282
        goalCode = goalCode,
283
        goal = goal,
284
        expirationDuration = expirationDuration,
285
      )
286
    } yield record
287
  }
288

289
  override def createAnonCredsIssueCredentialRecord(
1✔
290
      pairwiseIssuerDID: DidId,
291
      pairwiseHolderDID: Option[DidId],
292
      thid: DidCommID,
293
      credentialDefinitionGUID: UUID,
294
      credentialDefinitionId: String,
295
      claims: Json,
296
      validityPeriod: Option[Double],
297
      automaticIssuance: Option[Boolean],
298
      goalCode: Option[String],
299
      goal: Option[String],
300
      expirationDuration: Option[Duration],
301
      connectionId: Option[UUID],
302
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
303
    for {
1✔
304
      credentialDefinition <- getCredentialDefinition(credentialDefinitionGUID)
1✔
305
      _ <- CredentialSchema
1✔
306
        .validateAnonCredsClaims(
1✔
307
          credentialDefinition.schemaId,
308
          claims.noSpaces,
1✔
309
          uriResolver,
310
        )
311
        .orDieAsUnmanagedFailure
1✔
312
      attributes <- CredentialService.convertJsonClaimsToAttributes(claims)
1✔
313
      offer <- createAnonCredsDidCommOfferCredential(
1✔
314
        pairwiseIssuerDID = pairwiseIssuerDID,
315
        pairwiseHolderDID = pairwiseHolderDID,
316
        schemaUri = credentialDefinition.schemaId,
317
        credentialDefinitionGUID = credentialDefinitionGUID,
318
        credentialDefinitionId = credentialDefinitionId,
319
        claims = attributes,
320
        thid = thid,
321
      )
322
      record <- createIssueCredentialRecord(
1✔
323
        pairwiseIssuerDID = pairwiseIssuerDID,
324
        kidIssuer = None,
325
        thid = thid,
326
        schemaUris = Some(List(credentialDefinition.schemaId)),
1✔
327
        validityPeriod = validityPeriod,
328
        automaticIssuance = automaticIssuance,
329
        issuingDID = None,
330
        credentialFormat = CredentialFormat.AnonCreds,
331
        offer = offer,
332
        credentialDefinitionGUID = Some(credentialDefinitionGUID),
333
        credentialDefinitionId = Some(credentialDefinitionId),
334
        connectionId = connectionId,
335
        goalCode = goalCode,
336
        goal = goal,
337
        expirationDuration = expirationDuration,
338
      )
339
    } yield record
340
  }
341

342
  override def getIssueCredentialRecordsByStates(
1✔
343
      ignoreWithZeroRetries: Boolean,
344
      limit: Int,
345
      states: IssueCredentialRecord.ProtocolState*
346
  ): URIO[WalletAccessContext, Seq[IssueCredentialRecord]] =
347
    credentialRepository.findByStates(ignoreWithZeroRetries, limit, states*)
1✔
348

349
  override def getIssueCredentialRecordsByStatesForAllWallets(
×
350
      ignoreWithZeroRetries: Boolean,
351
      limit: Int,
352
      states: IssueCredentialRecord.ProtocolState*
353
  ): UIO[Seq[IssueCredentialRecord]] =
354
    credentialRepository.findByStatesForAllWallets(ignoreWithZeroRetries, limit, states*)
×
355

356
  override def receiveCredentialOffer(
1✔
357
      offer: OfferCredential
358
  ): ZIO[WalletAccessContext, InvalidCredentialOffer, IssueCredentialRecord] = {
359
    for {
1✔
360
      attachment <- ZIO
1✔
361
        .fromOption(offer.attachments.headOption)
1✔
362
        .mapError(_ => InvalidCredentialOffer("No attachment found"))
363

364
      format <- ZIO
1✔
365
        .fromOption(attachment.format)
366
        .mapError(_ => InvalidCredentialOffer("No attachment format found"))
367

368
      credentialFormat <- format match
1✔
369
        case value if value == IssueCredentialOfferFormat.JWT.name      => ZIO.succeed(CredentialFormat.JWT)
1✔
370
        case value if value == IssueCredentialOfferFormat.SDJWT.name    => ZIO.succeed(CredentialFormat.SDJWT)
×
371
        case value if value == IssueCredentialOfferFormat.Anoncred.name => ZIO.succeed(CredentialFormat.AnonCreds)
1✔
372
        case value => ZIO.fail(InvalidCredentialOffer(s"Unsupported credential format: $value"))
×
373

374
      _ <- validateCredentialOfferAttachment(credentialFormat, attachment)
1✔
375
      record <- ZIO.succeed(
1✔
376
        IssueCredentialRecord(
377
          id = DidCommID(),
1✔
378
          createdAt = Instant.now,
1✔
379
          updatedAt = None,
380
          thid = DidCommID(offer.thid.getOrElse(offer.id)),
1✔
381
          schemaUris = None,
382
          credentialDefinitionId = None,
383
          credentialDefinitionUri = None,
384
          credentialFormat = credentialFormat,
385
          invitation = None,
386
          role = Role.Holder,
387
          subjectId = None,
388
          keyId = None,
389
          validityPeriod = None,
390
          automaticIssuance = None,
391
          protocolState = IssueCredentialRecord.ProtocolState.OfferReceived,
392
          offerCredentialData = Some(offer),
393
          requestCredentialData = None,
394
          anonCredsRequestMetadata = None,
395
          issueCredentialData = None,
396
          issuedCredentialRaw = None,
397
          issuingDID = None,
398
          metaRetries = maxRetries,
399
          metaNextRetry = Some(Instant.now()),
1✔
400
          metaLastFailure = None,
401
        )
402
      )
403
      count <- credentialRepository.create(record)
1✔
404
    } yield record
405
  }
406

407
  private def validateCredentialOfferAttachment(
1✔
408
      credentialFormat: CredentialFormat,
409
      attachment: AttachmentDescriptor
410
  ): IO[InvalidCredentialOffer, Unit] = for {
1✔
411
    _ <- credentialFormat match
412
      case CredentialFormat.JWT | CredentialFormat.SDJWT =>
1✔
413
        attachment.data match
414
          case JsonData(json) =>
1✔
415
            ZIO
1✔
416
              .attempt(json.asJson.hcursor.downField("json").as[CredentialOfferAttachment])
1✔
417
              .mapError(e =>
418
                InvalidCredentialOffer(s"An error occurred when parsing the offer attachment: ${e.toString}")
×
419
              )
420
          case _ =>
×
421
            ZIO.fail(InvalidCredentialOffer(s"Only JSON attachments are supported in JWT offers"))
×
422
      case CredentialFormat.AnonCreds =>
1✔
423
        attachment.data match
424
          case Base64(value) =>
1✔
425
            for {
1✔
426
              _ <- ZIO
1✔
427
                .attempt(AnoncredCredentialOffer(value))
428
                .mapError(e =>
429
                  InvalidCredentialOffer(s"An error occurred when parsing the offer attachment: ${e.toString}")
×
430
                )
431
            } yield ()
1✔
432
          case _ =>
×
433
            ZIO.fail(InvalidCredentialOffer(s"Only Base64 attachments are supported in AnonCreds offers"))
×
434
  } yield ()
1✔
435

436
  private[this] def validatePrismDID(
1✔
437
      did: String
438
  ): IO[UnsupportedDidFormat, PrismDID] = ZIO
1✔
439
    .fromEither(PrismDID.fromString(did))
1✔
440
    .mapError(_ => UnsupportedDidFormat(did))
441

442
  private[this] def validateClaimsAgainstSchemaIfAny(
1✔
443
      claims: Json,
444
      maybeSchemaIds: Option[List[String]]
445
  ): UIO[Unit] = maybeSchemaIds match
446
    case Some(schemaIds) =>
1✔
447
      for {
1✔
448
        _ <- ZIO
1✔
449
          .collectAll(
450
            schemaIds.map(schemaId =>
1✔
451
              CredentialSchema
452
                .validateJWTCredentialSubject(schemaId, claims.noSpaces, uriResolver)
1✔
453
            )
454
          )
455
          .orDieAsUnmanagedFailure
1✔
456
      } yield ZIO.unit
1✔
457
    case None =>
1✔
458
      ZIO.unit
459

460
  private[this] def getCredentialDefinition(
1✔
461
      guid: UUID
462
  ): UIO[CredentialDefinition] = credentialDefinitionService
463
    .getByGUID(guid)
1✔
464
    .orDieAsUnmanagedFailure
1✔
465

466
  private[this] def getCredentialDefinitionPrivatePart(
1✔
467
      guid: UUID
468
  ): URIO[WalletAccessContext, CredentialDefinitionSecret] = for {
1✔
469
    maybeCredentialDefinitionSecret <- genericSecretStorage
1✔
470
      .get[UUID, CredentialDefinitionSecret](guid)
471
      .orDie
472
    credentialDefinitionSecret <- ZIO
1✔
473
      .fromOption(maybeCredentialDefinitionSecret)
474
      .mapError(_ => CredentialDefinitionPrivatePartNotFound(guid))
475
      .orDieAsUnmanagedFailure
1✔
476
  } yield credentialDefinitionSecret
477

478
  override def acceptCredentialOffer(
1✔
479
      recordId: DidCommID,
480
      maybeSubjectId: Option[String],
481
      keyId: Option[KeyId]
482
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] = {
483
    for {
1✔
484
      record <- getRecordWithState(recordId, ProtocolState.OfferReceived)
1✔
485
      count <- (record.credentialFormat, maybeSubjectId) match
1✔
486
        case (CredentialFormat.JWT | CredentialFormat.SDJWT, Some(subjectId)) =>
1✔
487
          for {
1✔
488
            _ <- validatePrismDID(subjectId)
1✔
489
            count <- credentialRepository
1✔
490
              .updateWithSubjectId(recordId, subjectId, keyId, ProtocolState.RequestPending)
1✔
491
              @@ CustomMetricsAspect.startRecordingTime(
1✔
492
                s"${record.id}_issuance_flow_holder_req_pending_to_generated"
1✔
493
              )
494
          } yield count
495
        case (CredentialFormat.AnonCreds, None) =>
1✔
496
          credentialRepository
1✔
497
            .updateProtocolState(recordId, ProtocolState.OfferReceived, ProtocolState.RequestPending)
1✔
498
            @@ CustomMetricsAspect.startRecordingTime(
1✔
499
              s"${record.id}_issuance_flow_holder_req_pending_to_generated"
1✔
500
            )
501
        case (format, maybeSubjectId) =>
×
502
          ZIO.dieMessage(s"Invalid subjectId input for $format offer acceptance: $maybeSubjectId")
×
503
      record <- credentialRepository.getById(record.id)
1✔
504
    } yield record
505
  }
506

507
  private def createPresentationPayload(
1✔
508
      record: IssueCredentialRecord,
509
      subject: JwtIssuer
510
  ): URIO[WalletAccessContext, PresentationPayload] = {
511
    for {
1✔
512
      maybeOptions <- getOptionsFromOfferCredentialData(record)
1✔
513
    } yield {
514
      W3cPresentationPayload(
1✔
515
        `@context` = Vector("https://www.w3.org/2018/presentations/v1"),
1✔
516
        maybeId = None,
517
        `type` = Vector("VerifiablePresentation"),
1✔
518
        verifiableCredential = IndexedSeq.empty,
1✔
519
        holder = subject.did.toString,
1✔
520
        verifier = IndexedSeq.empty ++ maybeOptions.map(_.domain),
1✔
521
        maybeIssuanceDate = None,
522
        maybeExpirationDate = None
523
      ).toJwtPresentationPayload.copy(maybeNonce = maybeOptions.map(_.challenge))
1✔
524
    }
525
  }
526

527
  private def getLongForm(
1✔
528
      did: PrismDID,
529
      allowUnpublishedIssuingDID: Boolean = false
×
530
  ): URIO[WalletAccessContext, LongFormPrismDID] = {
531
    for {
1✔
532
      maybeDidState <- managedDIDService
1✔
533
        .getManagedDIDState(did.asCanonical)
1✔
534
        .orDieWith(e => RuntimeException(s"Error occurred while getting DID from wallet: ${e.toString}"))
×
535
      didState <- ZIO
1✔
536
        .fromOption(maybeDidState)
537
        .mapError(_ => DIDNotFoundInWallet(did))
538
        .orDieAsUnmanagedFailure
1✔
539
      _ <- (didState match
1✔
540
        case s @ ManagedDIDState(_, _, PublicationState.Published(_)) => ZIO.succeed(s)
1✔
541
        case s => ZIO.cond(allowUnpublishedIssuingDID, s, DIDNotPublished(did, s.publicationState))
×
542
      ).orDieAsUnmanagedFailure
1✔
543
      longFormPrismDID = PrismDID.buildLongFormFromOperation(didState.createOperation)
1✔
544
    } yield longFormPrismDID
1✔
545
  }
546

547
  private[this] def getKeyId(
1✔
548
      did: PrismDID,
549
      verificationRelationship: VerificationRelationship,
550
      ellipticCurve: EllipticCurve
551
  ): UIO[KeyId] = {
552
    for {
1✔
553
      maybeDidData <- didService
1✔
554
        .resolveDID(did)
1✔
555
        .orDieWith(e => RuntimeException(s"Error occurred while resolving the DID: ${e.toString}"))
×
556
      didData <- ZIO
1✔
557
        .fromOption(maybeDidData)
558
        .mapError(_ => DIDNotResolved(did))
559
        .orDieAsUnmanagedFailure
1✔
560
      keyId <- ZIO
1✔
561
        .fromOption(
562
          didData._2.publicKeys
1✔
563
            .find(pk => pk.purpose == verificationRelationship && pk.publicKeyData.crv == ellipticCurve)
1✔
564
            .map(_.id)
1✔
565
        )
566
        .mapError(_ => KeyNotFoundInDID(did, verificationRelationship))
567
        .orDieAsUnmanagedFailure
1✔
568
    } yield keyId
569
  }
570

571
  override def getJwtIssuer(
1✔
572
      jwtIssuerDID: PrismDID,
573
      verificationRelationship: VerificationRelationship,
574
      keyId: Option[KeyId] = None
1✔
575
  ): URIO[WalletAccessContext, JwtIssuer] = {
576
    for {
1✔
577
      issuingKeyId <- getKeyId(jwtIssuerDID, verificationRelationship, EllipticCurve.SECP256K1)
1✔
578
      ecKeyPair <- managedDIDService
1✔
579
        .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingKeyId)
1✔
580
        .flatMap {
581
          case Some(keyPair: Secp256k1KeyPair) => ZIO.some(keyPair)
1✔
582
          case _                               => ZIO.none
×
583
        }
584
        .someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingKeyId, "Secp256k1"))
585
        .orDieAsUnmanagedFailure
1✔
586
      Secp256k1KeyPair(publicKey, privateKey) = ecKeyPair
1✔
587
      jwtIssuer = JwtIssuer(
588
        jwtIssuerDID.did,
1✔
589
        ES256KSigner(privateKey.toJavaPrivateKey, keyId),
1✔
590
        publicKey.toJavaPublicKey
1✔
591
      )
592
    } yield jwtIssuer
1✔
593
  }
594

595
  private def getEd25519SigningKeyPair(
×
596
      jwtIssuerDID: PrismDID,
597
      verificationRelationship: VerificationRelationship
598
  ): URIO[WalletAccessContext, Ed25519KeyPair] = {
599
    for {
×
600
      issuingKeyId <- getKeyId(jwtIssuerDID, verificationRelationship, EllipticCurve.ED25519)
×
601
      ed25519keyPair <- managedDIDService
×
602
        .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingKeyId)
×
603
        .map(_.collect { case keyPair: Ed25519KeyPair => keyPair })
×
604
        .someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingKeyId, "Ed25519"))
605
        .orDieAsUnmanagedFailure
×
606
    } yield ed25519keyPair
607
  }
608

609
  /** @param jwtIssuerDID
610
    *   This can holder prism did / issuer prism did
611
    * @param verificationRelationship
612
    *   Holder it Authentication and Issuer it is AssertionMethod
613
    * @param keyId
614
    *   Optional KID parameter in case of DID has multiple keys with same purpose
615
    * @return
616
    *   JwtIssuer
617
    * @see
618
    *   org.hyperledger.identus.pollux.vc.jwt.Issuer
619
    */
620
  private def getSDJwtIssuer(
×
621
      jwtIssuerDID: PrismDID,
622
      verificationRelationship: VerificationRelationship,
623
      keyId: Option[KeyId]
624
  ): URIO[WalletAccessContext, JwtIssuer] = {
625
    for {
×
626
      ed25519keyPair <- getEd25519SigningKeyPair(jwtIssuerDID, verificationRelationship)
×
627
    } yield {
628
      JwtIssuer(
629
        jwtIssuerDID.did,
×
630
        EdSigner(ed25519keyPair, keyId),
×
631
        ed25519keyPair.publicKey.toJava
×
632
      )
633
    }
634
  }
635

636
  private[this] def generateCredentialRequest(
1✔
637
      recordId: DidCommID,
638
      getIssuer: (
639
          did: LongFormPrismDID,
640
          verificationRelation: VerificationRelationship,
641
          keyId: Option[KeyId]
642
      ) => URIO[WalletAccessContext, JwtIssuer]
643
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] = {
644
    for {
1✔
645
      record <- getRecordWithState(recordId, ProtocolState.RequestPending)
1✔
646
      subjectId <- ZIO
1✔
647
        .fromOption(record.subjectId)
648
        .orDieWith(_ => RuntimeException(s"No 'subjectId' found in record: ${recordId.value}"))
×
649
      formatAndOffer <- ZIO
1✔
650
        .fromOption(record.offerCredentialFormatAndData)
1✔
651
        .orDieWith(_ => RuntimeException(s"No 'offer' found in record: ${recordId.value}"))
×
652
      subjectDID <- validatePrismDID(subjectId)
1✔
653
      longFormPrismDID <- getLongForm(subjectDID, true)
1✔
654
      jwtIssuer <- getIssuer(longFormPrismDID, VerificationRelationship.Authentication, record.keyId)
1✔
655
      presentationPayload <- createPresentationPayload(record, jwtIssuer)
1✔
656
      signedPayload = JwtPresentation.encodeJwt(presentationPayload.toJwtPresentationPayload, jwtIssuer)
1✔
657
      request = createDidCommRequestCredential(formatAndOffer._1, formatAndOffer._2, signedPayload)
1✔
658
      count <- credentialRepository
1✔
659
        .updateWithJWTRequestCredential(recordId, request, ProtocolState.RequestGenerated)
1✔
660
        @@ CustomMetricsAspect.endRecordingTime(
1✔
661
          s"${record.id}_issuance_flow_holder_req_pending_to_generated",
1✔
662
          "issuance_flow_holder_req_pending_to_generated_ms_gauge"
663
        ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent")
1✔
664
      record <- credentialRepository.getById(record.id)
1✔
665
    } yield record
666
  }
667

668
  override def generateJWTCredentialRequest(
1✔
669
      recordId: DidCommID
670
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] =
671
    generateCredentialRequest(recordId, getJwtIssuer)
1✔
672

673
  override def generateSDJWTCredentialRequest(
×
674
      recordId: DidCommID
675
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] =
676
    generateCredentialRequest(recordId, getSDJwtIssuer)
×
677

678
  override def generateAnonCredsCredentialRequest(
1✔
679
      recordId: DidCommID
680
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
681
    for {
1✔
682
      record <- getRecordWithState(recordId, ProtocolState.RequestPending)
1✔
683
      offerCredential <- ZIO
1✔
684
        .fromOption(record.offerCredentialData)
685
        .orDieWith(_ => RuntimeException(s"No 'offer' found in record: ${recordId.value}"))
×
686
      body = RequestCredential.Body(goal_code = Some("Request Credential"))
1✔
687
      createCredentialRequest <- createAnonCredsRequestCredential(offerCredential)
1✔
688
      attachments = Seq(
1✔
689
        AttachmentDescriptor.buildBase64Attachment(
1✔
690
          mediaType = Some("application/json"),
691
          format = Some(IssueCredentialRequestFormat.Anoncred.name),
692
          payload = createCredentialRequest.request.data.getBytes()
1✔
693
        )
694
      )
695
      requestMetadata = createCredentialRequest.metadata
696
      request = RequestCredential(
1✔
697
        body = body,
698
        attachments = attachments,
699
        from =
700
          offerCredential.to.getOrElse(throw new IllegalArgumentException("OfferCredential must have a recipient")),
1✔
701
        to = offerCredential.from,
702
        thid = offerCredential.thid
703
      )
704
      count <- credentialRepository
1✔
705
        .updateWithAnonCredsRequestCredential(recordId, request, requestMetadata, ProtocolState.RequestGenerated)
1✔
706
        @@ CustomMetricsAspect.endRecordingTime(
1✔
707
          s"${record.id}_issuance_flow_holder_req_pending_to_generated",
1✔
708
          "issuance_flow_holder_req_pending_to_generated_ms_gauge"
709
        ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent")
1✔
710
      record <- credentialRepository.getById(record.id)
1✔
711
    } yield record
712
  }
713

714
  private def createAnonCredsRequestCredential(
1✔
715
      offerCredential: OfferCredential
716
  ): URIO[WalletAccessContext, AnoncredCreateCrendentialRequest] = {
717
    for {
1✔
718
      attachmentData <- ZIO
1✔
719
        .fromOption(
720
          offerCredential.attachments
721
            .find(_.format.contains(IssueCredentialOfferFormat.Anoncred.name))
1✔
722
            .map(_.data)
1✔
723
            .flatMap {
1✔
724
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
725
              case _             => None
×
726
            }
727
        )
728
        .orDieWith(_ => RuntimeException(s"No AnonCreds attachment found in the offer"))
×
729
      credentialOffer = anoncreds.AnoncredCredentialOffer(attachmentData)
730
      credDefContent <- uriResolver
1✔
731
        .resolve(credentialOffer.getCredDefId)
1✔
732
        .orDieAsUnmanagedFailure
1✔
733
      credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent)
734
      linkSecret <- linkSecretService.fetchOrCreate()
1✔
735
      createCredentialRequest = AnoncredLib.createCredentialRequest(linkSecret, credentialDefinition, credentialOffer)
1✔
736
    } yield createCredentialRequest
1✔
737
  }
738

739
  override def receiveCredentialRequest(
1✔
740
      request: RequestCredential
741
  ): ZIO[WalletAccessContext, InvalidCredentialRequest | RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] = {
742
    for {
1✔
743
      thid <- ZIO
1✔
744
        .fromOption(request.thid.map(DidCommID(_)))
1✔
745
        .mapError(_ => InvalidCredentialRequest("No 'thid' found"))
746
      record <- getRecordWithThreadIdAndStates(
1✔
747
        thid,
748
        ignoreWithZeroRetries = true,
749
        ProtocolState.InvitationGenerated,
750
        ProtocolState.OfferPending,
751
        ProtocolState.OfferSent
752
      )
753
      _ <- credentialRepository.updateWithJWTRequestCredential(record.id, request, ProtocolState.RequestReceived)
1✔
754
      record <- credentialRepository.getById(record.id)
1✔
755
    } yield record
756
  }
757

758
  override def acceptCredentialRequest(
1✔
759
      recordId: DidCommID
760
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
761
    for {
1✔
762
      record <- getRecordWithState(recordId, ProtocolState.RequestReceived)
1✔
763
      request <- ZIO
1✔
764
        .fromOption(record.requestCredentialData)
765
        .orDieWith(_ => RuntimeException(s"No 'requestCredentialData' found in record: ${recordId.value}"))
×
766
      issue = createDidCommIssueCredential(request)
1✔
767
      count <- credentialRepository
1✔
768
        .updateWithIssueCredential(recordId, issue, ProtocolState.CredentialPending)
1✔
769
        @@ CustomMetricsAspect.startRecordingTime(
1✔
770
          s"${record.id}_issuance_flow_issuer_credential_pending_to_generated"
1✔
771
        )
772
      record <- credentialRepository.getById(record.id)
1✔
773
    } yield record
774
  }
775

776
  override def receiveCredentialIssue(
1✔
777
      issueCredential: IssueCredential
778
  ): ZIO[WalletAccessContext, InvalidCredentialIssue | RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] =
779
    for {
1✔
780
      thid <- ZIO
1✔
781
        .fromOption(issueCredential.thid.map(DidCommID(_)))
1✔
782
        .mapError(_ => InvalidCredentialIssue("No 'thid' found"))
783
      record <- getRecordWithThreadIdAndStates(
1✔
784
        thid,
785
        ignoreWithZeroRetries = true,
786
        ProtocolState.RequestPending,
787
        ProtocolState.RequestSent
788
      )
789
      attachment <- ZIO
1✔
790
        .fromOption(issueCredential.attachments.headOption)
1✔
791
        .mapError(_ => InvalidCredentialIssue("No attachment found"))
792

793
      _ <- {
1✔
794
        val result = attachment match {
795
          case AttachmentDescriptor(
796
                id,
797
                media_type,
798
                Base64(v),
799
                Some(IssueCredentialIssuedFormat.Anoncred.name),
800
                _,
801
                _,
802
                _,
803
                _
804
              ) =>
1✔
805
            for {
1✔
806
              processedCredential <- processAnonCredsCredential(record, java.util.Base64.getUrlDecoder.decode(v))
1✔
807
              attachment = AttachmentDescriptor.buildBase64Attachment(
1✔
808
                id = id,
809
                mediaType = media_type,
810
                format = Some(IssueCredentialIssuedFormat.Anoncred.name),
811
                payload = processedCredential.data.getBytes
1✔
812
              )
813
              processedIssuedCredential = issueCredential.copy(attachments = Seq(attachment))
1✔
814
              result <-
1✔
815
                updateWithCredential(
1✔
816
                  processedIssuedCredential,
817
                  record,
818
                  attachment,
819
                  Some(List(processedCredential.getSchemaId)),
1✔
820
                  Some(processedCredential.getCredDefId)
1✔
821
                )
822
            } yield result
823
          case attachment =>
1✔
824
            updateWithCredential(issueCredential, record, attachment, None, None)
1✔
825
        }
826
        result
827
      }
828
      record <- credentialRepository.getById(record.id)
1✔
829
    } yield record
830

831
  private def updateWithCredential(
1✔
832
      issueCredential: IssueCredential,
833
      record: IssueCredentialRecord,
834
      attachment: AttachmentDescriptor,
835
      schemaId: Option[List[String]],
836
      credDefId: Option[String]
837
  ) = {
838
    credentialRepository
839
      .updateWithIssuedRawCredential(
1✔
840
        record.id,
841
        issueCredential,
842
        attachment.data.asJson.noSpaces,
1✔
843
        schemaId,
844
        credDefId,
845
        ProtocolState.CredentialReceived
846
      )
847
  }
848

849
  private def processAnonCredsCredential(
1✔
850
      record: IssueCredentialRecord,
851
      credentialBytes: Array[Byte]
852
  ): URIO[WalletAccessContext, anoncreds.AnoncredCredential] = {
853
    for {
1✔
854
      credential <- ZIO.succeed(anoncreds.AnoncredCredential(new String(credentialBytes)))
1✔
855
      credDefContent <- uriResolver
1✔
856
        .resolve(credential.getCredDefId)
1✔
857
        .orDieAsUnmanagedFailure
1✔
858
      credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent)
859
      metadata <- ZIO
1✔
860
        .fromOption(record.anonCredsRequestMetadata)
861
        .orDieWith(_ => RuntimeException(s"No AnonCreds request metadata found in record: ${record.id.value}"))
×
862
      linkSecret <- linkSecretService.fetchOrCreate()
1✔
863
      credential <- ZIO
1✔
864
        .attempt(
865
          AnoncredLib.processCredential(
1✔
866
            anoncreds.AnoncredCredential(new String(credentialBytes)),
1✔
867
            metadata,
868
            linkSecret,
869
            credentialDefinition
870
          )
871
        )
872
        .orDieWith(error => RuntimeException(s"AnonCreds credential processing error: ${error.getMessage}"))
×
873
    } yield credential
874
  }
875

876
  override def markOfferSent(
1✔
877
      recordId: DidCommID
878
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
879
    updateCredentialRecordProtocolState(
1✔
880
      recordId,
881
      IssueCredentialRecord.ProtocolState.OfferPending,
882
      IssueCredentialRecord.ProtocolState.OfferSent
883
    )
884

885
  override def markCredentialOfferInvitationExpired(
×
886
      recordId: DidCommID
887
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
888
    updateCredentialRecordProtocolState(
×
889
      recordId,
890
      IssueCredentialRecord.ProtocolState.RequestReceived,
891
      IssueCredentialRecord.ProtocolState.InvitationExpired
892
    )
893
  override def markRequestSent(
1✔
894
      recordId: DidCommID
895
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
896
    updateCredentialRecordProtocolState(
1✔
897
      recordId,
898
      IssueCredentialRecord.ProtocolState.RequestGenerated,
899
      IssueCredentialRecord.ProtocolState.RequestSent
900
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
901
      s"${recordId}_issuance_flow_holder_req_generated_to_sent",
1✔
902
      "issuance_flow_holder_req_generated_to_sent_ms_gauge"
903
    )
904

905
  private def markCredentialGenerated(
1✔
906
      record: IssueCredentialRecord,
907
      issueCredential: IssueCredential
908
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
909
    for {
1✔
910
      count <- credentialRepository
1✔
911
        .updateWithIssueCredential(record.id, issueCredential, IssueCredentialRecord.ProtocolState.CredentialGenerated)
1✔
912
        @@ CustomMetricsAspect.endRecordingTime(
1✔
913
          s"${record.id}_issuance_flow_issuer_credential_pending_to_generated",
1✔
914
          "issuance_flow_issuer_credential_pending_to_generated_ms_gauge"
915
        ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_issuer_credential_generated_to_sent")
1✔
916
      record <- credentialRepository.getById(record.id)
1✔
917
    } yield record
918
  }
919

920
  override def markCredentialSent(
1✔
921
      recordId: DidCommID
922
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
923
    updateCredentialRecordProtocolState(
1✔
924
      recordId,
925
      IssueCredentialRecord.ProtocolState.CredentialGenerated,
926
      IssueCredentialRecord.ProtocolState.CredentialSent
927
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
928
      s"${recordId}_issuance_flow_issuer_credential_generated_to_sent",
1✔
929
      "issuance_flow_issuer_credential_generated_to_sent_ms_gauge"
930
    )
931

932
  override def reportProcessingFailure(
×
933
      recordId: DidCommID,
934
      failReason: Option[Failure]
935
  ): URIO[WalletAccessContext, Unit] =
936
    credentialRepository.updateAfterFail(recordId, failReason)
×
937

938
  private def getRecordWithState(
1✔
939
      recordId: DidCommID,
940
      state: ProtocolState
941
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
942
    for {
1✔
943
      record <- credentialRepository.getById(recordId)
1✔
944
      _ <- record.protocolState match {
1✔
945
        case s if s == state => ZIO.unit
1✔
946
        case s               => ZIO.fail(RecordNotFound(recordId, Some(s)))
1✔
947
      }
948
    } yield record
1✔
949
  }
950

951
  private def getRecordWithThreadIdAndStates(
1✔
952
      thid: DidCommID,
953
      ignoreWithZeroRetries: Boolean,
954
      states: ProtocolState*
955
  ): ZIO[WalletAccessContext, RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] = {
956
    for {
1✔
957
      record <- credentialRepository
1✔
958
        .findByThreadId(thid, ignoreWithZeroRetries)
1✔
959
        .someOrFail(RecordNotFoundForThreadIdAndStates(thid, states*))
960
      _ <- record.protocolState match {
1✔
961
        case s if states.contains(s) => ZIO.unit
1✔
962
        case state                   => ZIO.fail(RecordNotFoundForThreadIdAndStates(thid, states*))
1✔
963
      }
964
    } yield record
1✔
965
  }
966

967
  private def createDidCommOfferCredential(
1✔
968
      pairwiseIssuerDID: DidId,
969
      pairwiseHolderDID: Option[DidId],
970
      maybeSchemaIds: Option[List[String]],
971
      claims: Seq[Attribute],
972
      thid: DidCommID,
973
      challenge: String,
974
      domain: String,
975
      offerFormat: IssueCredentialOfferFormat
976
  ): UIO[OfferCredential] = {
977
    for {
1✔
978
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_ids = maybeSchemaIds, attributes = claims))
1✔
979
      body = OfferCredential.Body(
1✔
980
        goal_code = Some("Offer Credential"),
981
        credential_preview = credentialPreview,
982
      )
983
      attachments <- ZIO.succeed(
1✔
984
        Seq(
1✔
985
          AttachmentDescriptor.buildJsonAttachment(
1✔
986
            mediaType = Some("application/json"),
987
            format = Some(offerFormat.name),
988
            payload = PresentationAttachment(
989
              Some(Options(challenge, domain)),
990
              PresentationDefinition(format = Some(ClaimFormat(jwt = Some(Jwt(alg = Seq("ES256K"))))))
1✔
991
            )
992
          )
993
        )
994
      )
995
    } yield OfferCredential(
1✔
996
      body = body,
997
      attachments = attachments,
998
      from = pairwiseIssuerDID,
999
      to = pairwiseHolderDID,
1000
      thid = Some(thid.value)
1✔
1001
    )
1002
  }
1003

1004
  private def createAnonCredsDidCommOfferCredential(
1✔
1005
      pairwiseIssuerDID: DidId,
1006
      pairwiseHolderDID: Option[DidId],
1007
      schemaUri: String,
1008
      credentialDefinitionGUID: UUID,
1009
      credentialDefinitionId: String,
1010
      claims: Seq[Attribute],
1011
      thid: DidCommID
1012
  ): URIO[WalletAccessContext, OfferCredential] = {
1013
    for {
1✔
1014
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_ids = Some(List(schemaUri)), attributes = claims))
1✔
1015
      body = OfferCredential.Body(
1✔
1016
        goal_code = Some("Offer Credential"),
1017
        credential_preview = credentialPreview,
1018
      )
1019
      attachments <- createAnonCredsCredentialOffer(credentialDefinitionGUID, credentialDefinitionId).map { offer =>
1✔
1020
        Seq(
1✔
1021
          AttachmentDescriptor.buildBase64Attachment(
1✔
1022
            mediaType = Some("application/json"),
1023
            format = Some(IssueCredentialOfferFormat.Anoncred.name),
1024
            payload = offer.data.getBytes()
1✔
1025
          )
1026
        )
1027
      }
1028
    } yield OfferCredential(
1✔
1029
      body = body,
1030
      attachments = attachments,
1031
      from = pairwiseIssuerDID,
1032
      to = pairwiseHolderDID,
1033
      thid = Some(thid.value)
1✔
1034
    )
1035
  }
1036

1037
  private def createAnonCredsCredentialOffer(
1✔
1038
      credentialDefinitionGUID: UUID,
1039
      credentialDefinitionId: String
1040
  ): URIO[WalletAccessContext, AnoncredCredentialOffer] =
1041
    for {
1✔
1042
      credentialDefinition <- getCredentialDefinition(credentialDefinitionGUID)
1✔
1043
      cd = anoncreds.AnoncredCredentialDefinition(credentialDefinition.definition.toString)
1✔
1044
      kcp = anoncreds.AnoncredCredentialKeyCorrectnessProof(credentialDefinition.keyCorrectnessProof.toString)
1✔
1045
      credentialDefinitionSecret <- getCredentialDefinitionPrivatePart(credentialDefinition.guid)
1✔
1046
      cdp = anoncreds.AnoncredCredentialDefinitionPrivate(credentialDefinitionSecret.json.toString)
1✔
1047
      createCredentialDefinition = AnoncredCreateCredentialDefinition(cd, cdp, kcp)
1048
      offer = AnoncredLib.createOffer(createCredentialDefinition, credentialDefinitionId)
1✔
1049
    } yield offer
1✔
1050

1051
  private[this] def createDidCommRequestCredential(
1✔
1052
      format: IssueCredentialOfferFormat,
1053
      offer: OfferCredential,
1054
      signedPresentation: JWT
1055
  ): RequestCredential = {
1056
    RequestCredential(
1✔
1057
      body = RequestCredential.Body(
1058
        goal_code = offer.body.goal_code,
1059
        comment = offer.body.comment,
1060
      ),
1061
      attachments = Seq(
1✔
1062
        AttachmentDescriptor
1063
          .buildBase64Attachment(
1✔
1064
            mediaType = Some("application/json"),
1065
            format = Some(format.name),
1066
            // FIXME copy payload will probably not work for anoncreds!
1067
            payload = signedPresentation.value.getBytes(),
1✔
1068
          )
1069
      ),
1070
      thid = offer.thid.orElse(Some(offer.id)),
1✔
1071
      from = offer.to.getOrElse(throw new IllegalArgumentException("OfferCredential must have a recipient")),
×
1072
      to = offer.from
1073
    )
1074
  }
1075

1076
  private def createDidCommIssueCredential(request: RequestCredential): IssueCredential = {
1✔
1077
    IssueCredential(
1✔
1078
      body = IssueCredential.Body(
1079
        goal_code = request.body.goal_code,
1080
        comment = request.body.comment,
1081
        replacement_id = None,
1082
        more_available = None,
1083
      ),
1084
      attachments = Seq(), // FIXME !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1✔
1085
      thid = request.thid.orElse(Some(request.id)),
1✔
1086
      from = request.to,
1087
      to = request.from
1088
    )
1089
  }
1090

1091
  /** this is an auxiliary function.
1092
    *
1093
    * @note
1094
    *   Between updating and getting the CredentialRecord back the CredentialRecord can be updated by other operations
1095
    *   in the middle.
1096
    *
1097
    * TODO: this should be improved to behave exactly like atomic operation.
1098
    */
1099
  private def updateCredentialRecordProtocolState(
1✔
1100
      id: DidCommID,
1101
      from: IssueCredentialRecord.ProtocolState,
1102
      to: IssueCredentialRecord.ProtocolState
1103
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] = {
1104
    for {
1✔
1105
      record <- credentialRepository.getById(id)
1✔
1106
      updatedRecord <- record.protocolState match
1✔
1107
        case currentState if currentState == to => ZIO.succeed(record) // Idempotent behaviour
×
1108
        case currentState if currentState == from =>
1✔
1109
          credentialRepository.updateProtocolState(id, from, to) *> credentialRepository.getById(id)
1✔
1110
        case _ => ZIO.fail(InvalidStateForOperation(record.protocolState))
×
1111
    } yield updatedRecord
1112
  }
1113

1114
  override def generateJWTCredential(
1✔
1115
      recordId: DidCommID,
1116
      statusListRegistryUrl: String,
1117
  ): ZIO[WalletAccessContext, RecordNotFound | CredentialRequestValidationFailed, IssueCredentialRecord] = {
1118
    for {
1✔
1119
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
1✔
1120
      issuingDID <- ZIO
1✔
1121
        .fromOption(record.issuingDID)
1122
        .orElse(ZIO.dieMessage(s"Issuing DID not found in record: ${recordId.value}"))
×
1123
      issue <- ZIO
1✔
1124
        .fromOption(record.issueCredentialData)
1125
        .orElse(ZIO.dieMessage(s"Issue credential data not found in record: ${recordId.value}"))
×
1126
      longFormPrismDID <- getLongForm(issuingDID, true)
1✔
1127
      maybeOfferOptions <- getOptionsFromOfferCredentialData(record)
1✔
1128
      requestJwt <- getJwtFromRequestCredentialData(record)
1✔
1129
      offerCredentialData <- ZIO
1✔
1130
        .fromOption(record.offerCredentialData)
1131
        .orElse(ZIO.dieMessage(s"Offer credential data not found in record: ${recordId.value}"))
×
1132
      preview = offerCredentialData.body.credential_preview
1133
      claims <- CredentialService.convertAttributesToJsonClaims(preview.body.attributes).orDieAsUnmanagedFailure
1✔
1134
      jwtIssuer <- getJwtIssuer(longFormPrismDID, VerificationRelationship.AssertionMethod)
1✔
1135
      jwtPresentation <- validateRequestCredentialDataProof(maybeOfferOptions, requestJwt)
1✔
1136
        .tapError(error =>
1137
          credentialRepository
1138
            .updateProtocolState(record.id, ProtocolState.CredentialPending, ProtocolState.ProblemReportPending)
×
1139
        )
1140
        .orDieAsUnmanagedFailure
1✔
1141

1142
      // Custom for JWT
1143
      issuanceDate = Instant.now()
1✔
1144
      credentialStatus <- allocateNewCredentialInStatusListForWallet(record, statusListRegistryUrl, jwtIssuer)
1✔
1145
      // TODO: get schema when schema registry is available if schema ID is provided
1146
      w3Credential = W3cCredentialPayload(
1✔
1147
        `@context` = Set(
1✔
1148
          "https://www.w3.org/2018/credentials/v1"
1149
        ), // TODO: his information should come from Schema registry by record.schemaId
1150
        maybeId = None,
1151
        `type` =
1152
          Set("VerifiableCredential"), // TODO: This information should come from Schema registry by record.schemaId
1✔
1153
        issuer = CredentialIssuer(jwtIssuer.did.toString, `type` = "Profile"),
1✔
1154
        issuanceDate = issuanceDate,
1155
        maybeExpirationDate = record.validityPeriod.map(sec => issuanceDate.plusSeconds(sec.toLong)),
×
1156
        maybeCredentialSchema = record.schemaUris.map(ids =>
1✔
1157
          ids.map(id => org.hyperledger.identus.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE))
×
1158
        ),
1159
        maybeCredentialStatus = Some(credentialStatus),
1160
        credentialSubject = claims.add("id", jwtPresentation.iss.asJson).asJson,
1✔
1161
        maybeRefreshService = None,
1162
        maybeEvidence = None,
1163
        maybeTermsOfUse = None,
1164
        maybeValidFrom = None,
1165
        maybeValidUntil = None
1166
      )
1167
      signedJwtCredential = W3CCredential.toEncodedJwt(w3Credential, jwtIssuer)
1✔
1168
      issueCredential = IssueCredential.build(
1✔
1169
        fromDID = issue.from,
1170
        toDID = issue.to,
1171
        thid = issue.thid,
1172
        credentials = Seq(IssueCredentialIssuedFormat.JWT -> signedJwtCredential.value.getBytes)
1✔
1173
      )
1174
      // End custom
1175

1176
      record <- markCredentialGenerated(record, issueCredential)
1✔
1177
    } yield record
1178
  }
1179

1180
  override def generateSDJWTCredential(
×
1181
      recordId: DidCommID,
1182
      expirationTime: Duration,
1183
  ): ZIO[
1184
    WalletAccessContext,
1185
    RecordNotFound | ExpirationDateHasPassed | VCJwtHeaderParsingError,
1186
    IssueCredentialRecord
1187
  ] = {
1188
    for {
×
1189
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
×
1190
      issuingDID <- ZIO
×
1191
        .fromOption(record.issuingDID)
1192
        .orElse(ZIO.dieMessage(s"Issuing DID not found in record: ${recordId.value}"))
×
1193
      issue <- ZIO
×
1194
        .fromOption(record.issueCredentialData)
1195
        .orElse(ZIO.dieMessage(s"Issue credential data not found in record: ${recordId.value}"))
×
1196
      longFormPrismDID <- getLongForm(issuingDID, true)
×
1197
      maybeOfferOptions <- getOptionsFromOfferCredentialData(record)
×
1198
      requestJwt <- getJwtFromRequestCredentialData(record)
×
1199
      offerCredentialData <- ZIO
×
1200
        .fromOption(record.offerCredentialData)
1201
        .orElse(ZIO.dieMessage(s"Offer credential data not found in record: ${recordId.value}"))
×
1202
      preview = offerCredentialData.body.credential_preview
1203
      claims <- CredentialService.convertAttributesToJsonClaims(preview.body.attributes).orDieAsUnmanagedFailure
×
1204
      jwtPresentation <- validateRequestCredentialDataProof(maybeOfferOptions, requestJwt)
×
1205
        .tapError(error =>
1206
          credentialRepository
1207
            .updateProtocolState(record.id, ProtocolState.CredentialPending, ProtocolState.ProblemReportPending)
×
1208
        )
1209
        .orDieAsUnmanagedFailure
×
1210
      jwtHeader <- JWTVerification.extractJwtHeader(requestJwt) match
×
1211
        case ZValidation.Success(log, header) => ZIO.succeed(header)
×
1212
        case ZValidation.Failure(log, failure) =>
×
1213
          ZIO.fail(VCJwtHeaderParsingError(s"Extraction of JwtHeader failed ${failure.toChunk.toString}"))
×
1214
      ed25519KeyPair <- getEd25519SigningKeyPair(longFormPrismDID, VerificationRelationship.AssertionMethod)
×
1215
      sdJwtPrivateKey = sdjwt.IssuerPrivateKey(ed25519KeyPair.privateKey)
×
1216
      jsonWebKey <- didResolver.resolve(jwtPresentation.iss) flatMap {
×
1217
        case failed: DIDResolutionFailed =>
×
1218
          ZIO.dieMessage(s"Error occurred while resolving the DID: ${failed.error.toString}")
×
1219
        case succeeded: DIDResolutionSucceeded =>
×
1220
          jwtHeader.keyId match {
1221
            case Some(
1222
                  kid
1223
                ) => // TODO should we check in authentication and assertion or just in verificationMethod since this cane different how did document is implemented
×
1224
              ZIO
×
1225
                .fromOption(succeeded.didDocument.verificationMethod.find(_.id.endsWith(kid)).map(_.publicKeyJwk))
×
1226
                .orElse(
1227
                  ZIO.dieMessage(
×
1228
                    s"Required public Key for holder binding is not found in DID document for the kid: $kid"
×
1229
                  )
1230
                )
1231
            case None =>
×
1232
              ZIO.succeed(None) // JwtHeader keyId is None, Issued credential is not bound to any holder public key
1233
          }
1234
      }
1235

1236
      now = Instant.now.getEpochSecond
×
1237
      exp = claims("exp").flatMap(_.asNumber).flatMap(_.toLong)
×
1238
      expInSeconds <- ZIO.fromEither(exp match {
×
1239
        case Some(e) if e > now => Right(e)
×
1240
        case Some(e)            => Left(ExpirationDateHasPassed(e))
×
1241
        case _                  => Right(Instant.now.plus(expirationTime).getEpochSecond)
×
1242
      })
1243
      claimsUpdated = claims
1244
        .add("iss", issuingDID.did.toString.asJson) // This is issuer did
×
1245
        .add("sub", jwtPresentation.iss.asJson) // This is subject did
×
1246
        .add("iat", now.asJson)
×
1247
        .add("exp", expInSeconds.asJson)
×
1248
      credential = {
1249
        jsonWebKey match {
1250
          case Some(jwk) =>
×
1251
            SDJWT.issueCredential(
×
1252
              sdJwtPrivateKey,
1253
              claimsUpdated.asJson.noSpaces,
×
1254
              sdjwt.HolderPublicKey.fromJWT(jwk.toJson)
×
1255
            )
1256
          case None =>
×
1257
            SDJWT.issueCredential(
×
1258
              sdJwtPrivateKey,
1259
              claimsUpdated.asJson.noSpaces,
×
1260
            )
1261
        }
1262
      }
1263
      issueCredential = IssueCredential.build(
×
1264
        fromDID = issue.from,
1265
        toDID = issue.to,
1266
        thid = issue.thid,
1267
        credentials = Seq(IssueCredentialIssuedFormat.SDJWT -> credential.compact.getBytes)
×
1268
      )
1269
      record <- markCredentialGenerated(record, issueCredential)
×
1270
    } yield record
1271

1272
  }
1273

1274
  private def allocateNewCredentialInStatusListForWallet(
1✔
1275
      record: IssueCredentialRecord,
1276
      statusListRegistryUrl: String,
1277
      jwtIssuer: JwtIssuer
1278
  ): URIO[WalletAccessContext, CredentialStatus] = {
1279
    val effect = for {
1✔
1280
      lastStatusList <- credentialStatusListRepository.getLatestOfTheWallet
1✔
1281
      currentStatusList <- lastStatusList
1✔
1282
        .fold(credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl))(
1✔
1283
          ZIO.succeed(_)
1284
        )
1285
      size = currentStatusList.size
1286
      lastUsedIndex = currentStatusList.lastUsedIndex
1287
      statusListToBeUsed <-
1✔
1288
        if lastUsedIndex < size then ZIO.succeed(currentStatusList)
1✔
1289
        else credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl)
×
1290
      _ <- credentialStatusListRepository.allocateSpaceForCredential(
1✔
1291
        issueCredentialRecordId = record.id,
1292
        credentialStatusListId = statusListToBeUsed.id,
1293
        statusListIndex = statusListToBeUsed.lastUsedIndex + 1
1294
      )
1295
    } yield CredentialStatus(
1✔
1296
      id = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}#${statusListToBeUsed.lastUsedIndex + 1}",
1✔
1297
      `type` = "StatusList2021Entry",
1298
      statusPurpose = StatusPurpose.Revocation,
1299
      statusListIndex = lastUsedIndex + 1,
1300
      statusListCredential = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}"
1✔
1301
    )
1302
    issueCredentialSem.withPermit(effect)
1✔
1303
  }
1304

1305
  override def generateAnonCredsCredential(
1✔
1306
      recordId: DidCommID
1307
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
1308
    for {
1✔
1309
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
1✔
1310
      requestCredential <- ZIO
1✔
1311
        .fromOption(record.requestCredentialData)
1312
        .orElse(ZIO.dieMessage(s"No request credential data found in record: ${record.id}"))
×
1313
      body = IssueCredential.Body(goal_code = Some("Issue Credential"))
1✔
1314
      attachments <- createAnonCredsCredential(record).map { credential =>
1✔
1315
        Seq(
1✔
1316
          AttachmentDescriptor.buildBase64Attachment(
1✔
1317
            mediaType = Some("application/json"),
1318
            format = Some(IssueCredentialIssuedFormat.Anoncred.name),
1319
            payload = credential.data.getBytes()
1✔
1320
          )
1321
        )
1322
      }
1323
      issueCredential = IssueCredential(
1✔
1324
        body = body,
1325
        attachments = attachments,
1326
        from = requestCredential.to,
1327
        to = requestCredential.from,
1328
        thid = requestCredential.thid
1329
      )
1330
      record <- markCredentialGenerated(record, issueCredential)
1✔
1331
    } yield record
1332
  }
1333

1334
  private def createAnonCredsCredential(
1✔
1335
      record: IssueCredentialRecord
1336
  ): URIO[WalletAccessContext, AnoncredCredential] = {
1337
    for {
1✔
1338
      credentialDefinitionId <- ZIO
1✔
1339
        .fromOption(record.credentialDefinitionId)
1340
        .orElse(ZIO.dieMessage(s"No credential definition Id found in record: ${record.id}"))
×
1341
      credentialDefinition <- getCredentialDefinition(credentialDefinitionId)
1✔
1342
      cd = anoncreds.AnoncredCredentialDefinition(credentialDefinition.definition.toString)
1✔
1343
      offerCredential <- ZIO
1✔
1344
        .fromOption(record.offerCredentialData)
1345
        .orElse(ZIO.dieMessage(s"No offer credential data found in record: ${record.id}"))
×
1346
      offerCredentialAttachmentData <- ZIO
1✔
1347
        .fromOption(
1348
          offerCredential.attachments
1349
            .find(_.format.contains(IssueCredentialOfferFormat.Anoncred.name))
1✔
1350
            .map(_.data)
1✔
1351
            .flatMap {
1✔
1352
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
1353
              case _             => None
×
1354
            }
1355
        )
1356
        .orElse(ZIO.dieMessage(s"No 'AnonCreds' offer credential attachment found in record: ${record.id}"))
×
1357
      credentialOffer = anoncreds.AnoncredCredentialOffer(offerCredentialAttachmentData)
1358
      requestCredential <- ZIO
1✔
1359
        .fromOption(record.requestCredentialData)
1360
        .orElse(ZIO.dieMessage(s"No request credential data found in record: ${record.id}"))
×
1361
      requestCredentialAttachmentData <- ZIO
1✔
1362
        .fromOption(
1363
          requestCredential.attachments
1364
            .find(_.format.contains(IssueCredentialRequestFormat.Anoncred.name))
1✔
1365
            .map(_.data)
1✔
1366
            .flatMap {
1✔
1367
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
1368
              case _             => None
×
1369
            }
1370
        )
1371
        .orElse(ZIO.dieMessage(s"No 'AnonCreds' request credential attachment found in record: ${record.id}"))
×
1372
      credentialRequest = anoncreds.AnoncredCredentialRequest(requestCredentialAttachmentData)
1373
      attrValues = offerCredential.body.credential_preview.body.attributes.map { attr =>
1✔
1374
        (attr.name, attr.value)
1375
      }
1376
      credentialDefinitionSecret <- getCredentialDefinitionPrivatePart(credentialDefinition.guid)
1✔
1377
      cdp = anoncreds.AnoncredCredentialDefinitionPrivate(credentialDefinitionSecret.json.toString)
1✔
1378
      credential =
1379
        AnoncredLib.createCredential(
1✔
1380
          cd,
1381
          cdp,
1382
          credentialOffer,
1383
          credentialRequest,
1384
          attrValues
1385
        )
1386
    } yield credential
1✔
1387
  }
1388

1389
  private def getOptionsFromOfferCredentialData(record: IssueCredentialRecord): UIO[Option[Options]] = {
1✔
1390
    for {
1✔
1391
      offer <- ZIO
1✔
1392
        .fromOption(record.offerCredentialData)
1393
        .orElse(ZIO.dieMessage(s"Offer data not found in record: ${record.id}"))
×
1394
      attachmentDescriptor <- ZIO
1✔
1395
        .fromOption(offer.attachments.headOption)
1✔
1396
        .orElse(ZIO.dieMessage(s"Attachments not found in record: ${record.id}"))
×
1397
      json <- attachmentDescriptor.data match
1✔
1398
        case JsonData(json) => ZIO.succeed(json.asJson)
1✔
1399
        case _              => ZIO.dieMessage(s"Attachment doesn't contain JsonData: ${record.id}")
×
1400
      maybeOptions <- ZIO
1✔
1401
        .fromEither(json.as[PresentationAttachment].map(_.options))
1✔
1402
        .flatMapError(df => ZIO.dieMessage(df.getMessage))
×
1403
    } yield maybeOptions
1404
  }
1405

1406
  private def getJwtFromRequestCredentialData(record: IssueCredentialRecord): UIO[JWT] = {
1✔
1407
    for {
1✔
1408
      request <- ZIO
1✔
1409
        .fromOption(record.requestCredentialData)
1410
        .orElse(ZIO.dieMessage(s"Request data not found in record: ${record.id}"))
×
1411
      attachmentDescriptor <- ZIO
1✔
1412
        .fromOption(request.attachments.headOption)
1✔
1413
        .orElse(ZIO.dieMessage(s"Attachment not found in record: ${record.id}"))
×
1414
      jwt <- attachmentDescriptor.data match
1✔
1415
        case Base64(b64) =>
1✔
1416
          ZIO.succeed {
1417
            val base64Decoded = new String(java.util.Base64.getUrlDecoder.decode(b64))
1✔
1418
            JWT(base64Decoded)
1✔
1419
          }
1420
        case _ => ZIO.dieMessage(s"Attachment does not contain Base64Data: ${record.id}")
×
1421
    } yield jwt
1422
  }
1423

1424
  private def validateRequestCredentialDataProof(
1✔
1425
      maybeOptions: Option[Options],
1426
      jwt: JWT
1427
  ): IO[CredentialRequestValidationFailed, JwtPresentationPayload] = {
1428
    for {
1✔
1429
      _ <- maybeOptions match
1✔
1430
        case None => ZIO.unit
×
1431
        case Some(options) =>
1✔
1432
          JwtPresentation.validatePresentation(jwt, options.domain, options.challenge) match
1✔
1433
            case ZValidation.Success(log, value) => ZIO.unit
1✔
1434
            case ZValidation.Failure(log, error) =>
×
1435
              ZIO.fail(CredentialRequestValidationFailed("domain/challenge proof validation failed"))
×
1436

1437
      clock = java.time.Clock.system(ZoneId.systemDefault)
1✔
1438
      verificationResult <- JwtPresentation
1✔
1439
        .verify(
1440
          jwt,
1441
          JwtPresentation.PresentationVerificationOptions(
1✔
1442
            maybeProofPurpose = Some(VerificationRelationship.Authentication),
1443
            verifySignature = true,
1444
            verifyDates = false,
1445
            leeway = Duration.Zero
1446
          )
1447
        )(didResolver, uriResolver)(clock)
1✔
1448
        .mapError(errors => CredentialRequestValidationFailed(errors*))
1449

1450
      result <- verificationResult match
1✔
1451
        case ZValidation.Success(log, value) => ZIO.unit
1✔
1452
        case ZValidation.Failure(log, error) =>
×
1453
          ZIO.fail(CredentialRequestValidationFailed(s"JWT presentation verification failed: $error"))
×
1454

1455
      jwtPresentation <- ZIO
1✔
1456
        .fromTry(JwtPresentation.decodeJwt(jwt))
1✔
1457
        .mapError(t => CredentialRequestValidationFailed(s"JWT presentation decoding failed: ${t.getMessage}"))
×
1458
    } yield jwtPresentation
1459
  }
1460

1461
  override def getCredentialOfferInvitation(
×
1462
      pairwiseHolderDID: DidId,
1463
      invitation: String
1464
  ): ZIO[WalletAccessContext, CredentialServiceError, OfferCredential] = {
1465
    for {
×
1466
      invitation <- ZIO
×
1467
        .fromEither(io.circe.parser.decode[Invitation](Base64Utils.decodeUrlToString(invitation)))
×
1468
        .mapError(err => InvitationParsingError(err.getMessage))
×
1469
      _ <- invitation.expires_time match {
×
1470
        case Some(expiryTime) =>
×
1471
          ZIO
×
1472
            .fail(InvitationExpired(expiryTime))
1473
            .when(Instant.now().getEpochSecond > expiryTime)
×
1474
        case None => ZIO.unit
×
1475
      }
1476
      _ <- getIssueCredentialRecordByThreadId(DidCommID(invitation.id), false)
×
1477
        .flatMap {
1478
          case None    => ZIO.unit
×
1479
          case Some(_) => ZIO.fail(InvitationAlreadyReceived(invitation.id))
×
1480
        }
1481
      credentialOffer <- ZIO.fromEither {
×
1482
        invitation.attachments
1483
          .flatMap(
×
1484
            _.headOption.map(attachment =>
×
1485
              decode[org.hyperledger.identus.mercury.model.JsonData](
×
1486
                attachment.data.asJson.noSpaces
×
1487
              ) // TODO Move mercury to use ZIO JSON
1488
                .flatMap { data =>
×
1489
                  OfferCredential.given_Decoder_OfferCredential
×
1490
                    .decodeJson(data.json.asJson)
×
1491
                    .map(r => r.copy(to = Some(pairwiseHolderDID)))
×
1492
                    .leftMap(err =>
×
1493
                      CredentialOfferDecodingError(
1494
                        s"Credential Offer As Attachment decoding error: ${err.getMessage}"
×
1495
                      )
1496
                    )
1497
                }
1498
                .leftMap(err => CredentialOfferDecodingError(s"Invitation Attachment JsonData decoding error: $err"))
×
1499
            )
1500
          )
1501
          .getOrElse(
×
1502
            Left(MissingInvitationAttachment("Missing Invitation Attachment for Credential Offer"))
×
1503
          )
1504
      }
1505
    } yield credentialOffer
1506

1507
  }
1508
}
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