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

hyperledger / identus-cloud-agent / 16023418720

02 Jul 2025 11:05AM UTC coverage: 47.891% (-0.6%) from 48.511%
16023418720

Pull #1591

patextreme
lint

Signed-off-by: Pat Losoponkul <pat.losoponkul@iohk.io>
Pull Request #1591: feat: vdr integration

5 of 174 new or added lines in 13 files covered. (2.87%)

303 existing lines in 90 files now uncovered.

8151 of 17020 relevant lines covered (47.89%)

0.48 hits per line

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

71.15
/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 org.hyperledger.identus.agent.walletapi.model.{ManagedDIDState, PublicationState}
5
import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService
6
import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage
7
import org.hyperledger.identus.castor.core.model.did.*
8
import org.hyperledger.identus.castor.core.service.DIDService
9
import org.hyperledger.identus.mercury.model.*
10
import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation
11
import org.hyperledger.identus.mercury.protocol.issuecredential.*
12
import org.hyperledger.identus.pollux.*
13
import org.hyperledger.identus.pollux.anoncreds.*
14
import org.hyperledger.identus.pollux.core.model.*
15
import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError
16
import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError.*
17
import org.hyperledger.identus.pollux.core.model.presentation.*
18
import org.hyperledger.identus.pollux.core.model.primitives.UriString
19
import org.hyperledger.identus.pollux.core.model.schema.{CredentialDefinition, CredentialSchema, CredentialSchemaRef}
20
import org.hyperledger.identus.pollux.core.model.secret.CredentialDefinitionSecret
21
import org.hyperledger.identus.pollux.core.repository.{CredentialRepository, CredentialStatusListRepository}
22
import org.hyperledger.identus.pollux.prex.{ClaimFormat, Jwt, PresentationDefinition}
23
import org.hyperledger.identus.pollux.sdjwt.*
24
import org.hyperledger.identus.pollux.vc.jwt.{Issuer as JwtIssuer, *}
25
import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Secp256k1KeyPair}
26
import org.hyperledger.identus.shared.http.UriResolver
27
import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId}
28
import org.hyperledger.identus.shared.models.*
29
import org.hyperledger.identus.shared.models.Failure.orDieAsUnmanagedFailure
30
import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect
31
import org.hyperledger.identus.shared.utils.Base64Utils
32
import zio.*
33
import zio.json.*
34
import zio.json.ast.Json
35
import zio.prelude.ZValidation
36

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

41
object CredentialServiceImpl {
42
  val layer: URLayer[
43
    CredentialRepository & CredentialStatusListRepository & DidResolver & UriResolver & GenericSecretStorage &
44
      CredentialDefinitionService & LinkSecretService & DIDService & ManagedDIDService &
45
      Producer[UUID, WalletIdAndRecordId],
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
        messageProducer <- ZIO.service[Producer[UUID, WalletIdAndRecordId]]
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
        messageProducer
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
    messageProducer: Producer[UUID, WalletIdAndRecordId],
92
) extends CredentialService {
93

94
  import CredentialServiceImpl.*
95
  import IssueCredentialRecord.*
96

97
  private val TOPIC_NAME = "issue"
98

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

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

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

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

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

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

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

298
  override def createAnonCredsIssueCredentialRecord(
1✔
299
      pairwiseIssuerDID: DidId,
300
      pairwiseHolderDID: Option[DidId],
301
      thid: DidCommID,
302
      credentialDefinitionGUID: UUID,
303
      credentialDefinitionId: String,
304
      claims: Json,
305
      validityPeriod: Option[Double],
306
      automaticIssuance: Option[Boolean],
307
      goalCode: Option[String],
308
      goal: Option[String],
309
      expirationDuration: Option[Duration],
310
      connectionId: Option[UUID],
311
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
312
    for {
1✔
313
      credentialDefinition <- getCredentialDefinition(credentialDefinitionGUID)
1✔
314
      _ <- CredentialSchema
1✔
315
        .validateAnonCredsClaims(
1✔
316
          credentialDefinition.schemaId,
317
          claims.toJson,
1✔
318
          uriResolver,
319
        )
320
        .orDieAsUnmanagedFailure
1✔
321
      attributes <- CredentialService.convertJsonClaimsToAttributes(claims)
1✔
322
      offer <- createAnonCredsDidCommOfferCredential(
1✔
323
        pairwiseIssuerDID = pairwiseIssuerDID,
324
        pairwiseHolderDID = pairwiseHolderDID,
325
        schemaUri = credentialDefinition.schemaId,
326
        credentialDefinitionGUID = credentialDefinitionGUID,
327
        credentialDefinitionId = credentialDefinitionId,
328
        claims = attributes,
329
        thid = thid,
330
      )
331
      schemaUris <- UriString
1✔
332
        .make(credentialDefinition.schemaId)
1✔
333
        .toZIO
1✔
334
        .orDieWith(error => RuntimeException(s"The schemaIs is not a valid URI: $error"))
×
335
        .map(uri => Option(List(uri)))
1✔
336
      record <- createIssueCredentialRecord(
1✔
337
        pairwiseIssuerDID = pairwiseIssuerDID,
338
        kidIssuer = None,
339
        thid = thid,
340
        schemaUris = schemaUris,
341
        validityPeriod = validityPeriod,
342
        automaticIssuance = automaticIssuance,
343
        issuingDID = None,
344
        credentialFormat = CredentialFormat.AnonCreds,
345
        offer = offer,
346
        credentialDefinitionGUID = Some(credentialDefinitionGUID),
347
        credentialDefinitionId = Some(credentialDefinitionId),
348
        connectionId = connectionId,
349
        goalCode = goalCode,
350
        goal = goal,
351
        expirationDuration = expirationDuration,
352
      )
353
    } yield record
354
  }
355

356
  override def getIssueCredentialRecordsByStates(
1✔
357
      ignoreWithZeroRetries: Boolean,
358
      limit: Int,
359
      states: IssueCredentialRecord.ProtocolState*
360
  ): URIO[WalletAccessContext, Seq[IssueCredentialRecord]] =
361
    credentialRepository.findByStates(ignoreWithZeroRetries, limit, states*)
1✔
362

363
  override def getIssueCredentialRecordsByStatesForAllWallets(
×
364
      ignoreWithZeroRetries: Boolean,
365
      limit: Int,
366
      states: IssueCredentialRecord.ProtocolState*
367
  ): UIO[Seq[IssueCredentialRecord]] =
368
    credentialRepository.findByStatesForAllWallets(ignoreWithZeroRetries, limit, states*)
×
369

370
  override def receiveCredentialOffer(
1✔
371
      offer: OfferCredential
372
  ): ZIO[WalletAccessContext, InvalidCredentialOffer, IssueCredentialRecord] = {
373
    for {
1✔
374
      attachment <- ZIO
1✔
375
        .fromOption(offer.attachments.headOption)
1✔
376
        .mapError(_ => InvalidCredentialOffer("No attachment found"))
377

378
      format <- ZIO
1✔
379
        .fromOption(attachment.format)
380
        .mapError(_ => InvalidCredentialOffer("No attachment format found"))
381

382
      credentialFormat <- format match
1✔
383
        case value if value == IssueCredentialOfferFormat.JWT.name      => ZIO.succeed(CredentialFormat.JWT)
1✔
384
        case value if value == IssueCredentialOfferFormat.SDJWT.name    => ZIO.succeed(CredentialFormat.SDJWT)
×
385
        case value if value == IssueCredentialOfferFormat.Anoncred.name => ZIO.succeed(CredentialFormat.AnonCreds)
1✔
386
        case value => ZIO.fail(InvalidCredentialOffer(s"Unsupported credential format: $value"))
×
387

388
      _ <- validateCredentialOfferAttachment(credentialFormat, attachment)
1✔
389
      record <- ZIO.succeed(
1✔
390
        IssueCredentialRecord(
391
          id = DidCommID(),
1✔
392
          createdAt = Instant.now,
1✔
393
          updatedAt = None,
394
          thid = DidCommID(offer.thid.getOrElse(offer.id)),
1✔
395
          schemaUris = None,
396
          credentialDefinitionId = None,
397
          credentialDefinitionUri = None,
398
          credentialFormat = credentialFormat,
399
          invitation = None,
400
          role = Role.Holder,
401
          subjectId = None,
402
          keyId = None,
403
          validityPeriod = None,
404
          automaticIssuance = None,
405
          protocolState = IssueCredentialRecord.ProtocolState.OfferReceived,
406
          offerCredentialData = Some(offer),
407
          requestCredentialData = None,
408
          anonCredsRequestMetadata = None,
409
          issueCredentialData = None,
410
          issuedCredentialRaw = None,
411
          issuingDID = None,
412
          metaRetries = maxRetries,
413
          metaNextRetry = Some(Instant.now()),
1✔
414
          metaLastFailure = None,
415
        )
416
      )
417
      count <- credentialRepository.create(record)
1✔
418
    } yield record
419
  }
420

421
  private def validateCredentialOfferAttachment(
1✔
422
      credentialFormat: CredentialFormat,
423
      attachment: AttachmentDescriptor
424
  ): IO[InvalidCredentialOffer, Unit] = for {
1✔
425
    _ <- credentialFormat match
426
      case CredentialFormat.JWT | CredentialFormat.SDJWT =>
1✔
427
        attachment.data match
428
          case JsonData(json) =>
1✔
429
            ZIO
1✔
430
              .fromEither(json.as[CredentialOfferAttachment])
1✔
431
              .mapError(err => InvalidCredentialOffer(s"An error occurred when parsing the offer attachment: $err"))
×
432
          case _ =>
×
433
            ZIO.fail(InvalidCredentialOffer(s"Only JSON attachments are supported in JWT offers"))
×
434
      case CredentialFormat.AnonCreds =>
1✔
435
        attachment.data match
436
          case Base64(value) =>
1✔
437
            for {
1✔
438
              _ <- ZIO
1✔
439
                .attempt(AnoncredCredentialOffer(value))
440
                .mapError(e =>
441
                  InvalidCredentialOffer(s"An error occurred when parsing the offer attachment: ${e.toString}")
×
442
                )
443
            } yield ()
1✔
444
          case _ =>
×
445
            ZIO.fail(InvalidCredentialOffer(s"Only Base64 attachments are supported in AnonCreds offers"))
×
446
  } yield ()
1✔
447

448
  private[this] def validatePrismDID(
1✔
449
      did: String
450
  ): IO[UnsupportedDidFormat, PrismDID] = ZIO
1✔
451
    .fromEither(PrismDID.fromString(did))
1✔
452
    .mapError(_ => UnsupportedDidFormat(did))
453

454
  // TODO: Refactor this method in order to use more strict signatures
455
  private[this] def validateClaimsAgainstSchemaIfAny(
1✔
456
      claims: Json,
457
      maybeSchemaIds: Option[List[CredentialSchemaRef]]
458
  ): UIO[Unit] = maybeSchemaIds match
459
    case Some(schemaIds) =>
1✔
460
      for {
1✔
461
        _ <- ZIO
1✔
462
          .collectAll(
463
            schemaIds
464
              .map(_.id)
1✔
465
              .map(schemaId =>
1✔
466
                CredentialSchema
467
                  .validateJWTCredentialSubject(schemaId, claims.toJson, uriResolver)
1✔
468
              )
469
          )
470
          .orDieAsUnmanagedFailure
1✔
471
      } yield ZIO.unit
1✔
472
    case None =>
1✔
473
      ZIO.unit
474

475
  private[this] def getCredentialDefinition(
1✔
476
      guid: UUID
477
  ): UIO[CredentialDefinition] = credentialDefinitionService
478
    .getByGUID(guid)
1✔
479
    .orDieAsUnmanagedFailure
1✔
480

481
  private[this] def getCredentialDefinitionPrivatePart(
1✔
482
      guid: UUID
483
  ): URIO[WalletAccessContext, CredentialDefinitionSecret] = for {
1✔
484
    maybeCredentialDefinitionSecret <- genericSecretStorage
1✔
485
      .get[UUID, CredentialDefinitionSecret](guid)
486
      .orDie
487
    credentialDefinitionSecret <- ZIO
1✔
488
      .fromOption(maybeCredentialDefinitionSecret)
489
      .mapError(_ => CredentialDefinitionPrivatePartNotFound(guid))
490
      .orDieAsUnmanagedFailure
1✔
491
  } yield credentialDefinitionSecret
492

493
  override def acceptCredentialOffer(
1✔
494
      recordId: DidCommID,
495
      maybeSubjectId: Option[String],
496
      keyId: Option[KeyId]
497
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] = {
498
    for {
1✔
499
      record <- getRecordWithState(recordId, ProtocolState.OfferReceived)
1✔
500
      count <- (record.credentialFormat, maybeSubjectId) match
1✔
501
        case (CredentialFormat.JWT | CredentialFormat.SDJWT, Some(subjectId)) =>
1✔
502
          for {
1✔
503
            _ <- validatePrismDID(subjectId)
1✔
504
            count <- credentialRepository
1✔
505
              .updateWithSubjectId(recordId, subjectId, keyId, ProtocolState.RequestPending)
1✔
506
              @@ CustomMetricsAspect.startRecordingTime(
1✔
507
                s"${record.id}_issuance_flow_holder_req_pending_to_generated"
1✔
508
              )
509
          } yield count
510
        case (CredentialFormat.AnonCreds, None) =>
1✔
511
          credentialRepository
1✔
512
            .updateProtocolState(recordId, ProtocolState.OfferReceived, ProtocolState.RequestPending)
1✔
513
            @@ CustomMetricsAspect.startRecordingTime(
1✔
514
              s"${record.id}_issuance_flow_holder_req_pending_to_generated"
1✔
515
            )
516
        case (format, maybeSubjectId) =>
×
517
          ZIO.dieMessage(s"Invalid subjectId input for $format offer acceptance: $maybeSubjectId")
×
518
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
519
      _ <- messageProducer
1✔
520
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
521
        .orDie
522
      record <- credentialRepository.getById(record.id)
1✔
523
    } yield record
524
  }
525

526
  private def createPresentationPayload(
1✔
527
      record: IssueCredentialRecord,
528
      subject: JwtIssuer
529
  ): URIO[WalletAccessContext, PresentationPayload] = {
530
    for {
1✔
531
      maybeOptions <- getOptionsFromOfferCredentialData(record)
1✔
532
    } yield {
533
      W3cPresentationPayload(
1✔
534
        `@context` = Vector("https://www.w3.org/2018/presentations/v1"),
1✔
535
        maybeId = None,
536
        `type` = Vector("VerifiablePresentation"),
1✔
537
        verifiableCredential = IndexedSeq.empty,
1✔
538
        holder = subject.did.toString,
1✔
539
        verifier = IndexedSeq.empty ++ maybeOptions.map(_.domain),
1✔
540
        maybeIssuanceDate = None,
541
        maybeExpirationDate = None
542
      ).toJwtPresentationPayload.copy(maybeNonce = maybeOptions.map(_.challenge))
1✔
543
    }
544
  }
545

546
  private def getLongForm(
1✔
547
      did: PrismDID,
548
      allowUnpublishedIssuingDID: Boolean = false
×
549
  ): URIO[WalletAccessContext, LongFormPrismDID] = {
550
    for {
1✔
551
      maybeDidState <- managedDIDService
1✔
552
        .getManagedDIDState(did.asCanonical)
1✔
553
        .orDieWith(e => RuntimeException(s"Error occurred while getting DID from wallet: ${e.toString}"))
×
554
      didState <- ZIO
1✔
555
        .fromOption(maybeDidState)
556
        .mapError(_ => DIDNotFoundInWallet(did))
557
        .orDieAsUnmanagedFailure
1✔
558
      _ <- (didState match
1✔
559
        case s @ ManagedDIDState(_, _, PublicationState.Published(_)) => ZIO.succeed(s)
1✔
560
        case s => ZIO.cond(allowUnpublishedIssuingDID, s, DIDNotPublished(did, s.publicationState))
×
561
      ).orDieAsUnmanagedFailure
1✔
562
      longFormPrismDID = PrismDID.buildLongFormFromOperation(didState.createOperation)
1✔
563
    } yield longFormPrismDID
1✔
564
  }
565

566
  private[this] def getKeyId(
1✔
567
      did: PrismDID,
568
      verificationRelationship: VerificationRelationship,
569
      keyId: Option[KeyId]
570
  ): UIO[PublicKey] = {
571
    for {
1✔
572
      maybeDidData <- didService
1✔
573
        .resolveDID(did)
1✔
574
        .orDieWith(e => RuntimeException(s"Error occurred while resolving the DID: ${e.toString}"))
×
575
      didData <- ZIO
1✔
576
        .fromOption(maybeDidData)
577
        .mapError(_ => DIDNotResolved(did))
578
        .orDieAsUnmanagedFailure
1✔
579
      matchingKeys = didData._2.publicKeys.filter(pk => pk.purpose == verificationRelationship)
1✔
580
      result <- (matchingKeys, keyId) match {
1✔
581
        case (Seq(), _) =>
×
582
          ZIO.fail(KeyNotFoundInDID(did, verificationRelationship)).orDieAsUnmanagedFailure
×
583
        case (Seq(singleKey), None) =>
1✔
584
          ZIO.succeed(singleKey)
585
        case (multipleKeys, Some(kid)) =>
1✔
586
          ZIO
1✔
587
            .fromOption(multipleKeys.find(_.id.value.endsWith(kid.value)))
1✔
588
            .mapError(_ => KeyNotFoundInDID(did, verificationRelationship))
589
            .orDieAsUnmanagedFailure
1✔
590
        case (multipleKeys, None) =>
×
591
          ZIO
×
592
            .fail(
593
              MultipleKeysWithSamePurposeFoundInDID(did, verificationRelationship)
594
            )
595
            .orDieAsUnmanagedFailure
×
596
      }
597
    } yield result
598
  }
599

600
  override def getJwtIssuer(
1✔
601
      jwtIssuerDID: PrismDID,
602
      verificationRelationship: VerificationRelationship,
603
      keyId: Option[KeyId] = None
×
604
  ): URIO[WalletAccessContext, JwtIssuer] = {
605
    for {
1✔
606
      issuingPublicKey <- getKeyId(jwtIssuerDID, verificationRelationship, keyId)
1✔
607
      jwtIssuer <- managedDIDService
1✔
608
        .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingPublicKey.id)
1✔
609
        .flatMap {
610
          case Some(keyPair: Secp256k1KeyPair) => {
1✔
611
            val jwtIssuer = JwtIssuer(
612
              jwtIssuerDID.did,
1✔
613
              ES256KSigner(keyPair.privateKey.toJavaPrivateKey, keyId),
1✔
614
              keyPair.publicKey.toJavaPublicKey
1✔
615
            )
616
            ZIO.some(jwtIssuer)
1✔
617
          }
618
          case Some(keyPair: Ed25519KeyPair) => {
×
619
            val jwtIssuer = JwtIssuer(
620
              jwtIssuerDID.did,
×
621
              EdSigner(keyPair, keyId),
×
622
              keyPair.publicKey.toJava
×
623
            )
624
            ZIO.some(jwtIssuer)
×
625
          }
626
          case _ => ZIO.none
×
627
        }
628
        .someOrFail(
629
          KeyPairNotFoundInWallet(jwtIssuerDID, issuingPublicKey.id, issuingPublicKey.publicKeyData.crv.name)
×
630
        )
631
        .orDieAsUnmanagedFailure
1✔
632
    } yield jwtIssuer
633
  }
634

635
  private def getEd25519SigningKeyPair(
×
636
      jwtIssuerDID: PrismDID,
637
      verificationRelationship: VerificationRelationship,
638
      keyId: Option[KeyId] = None
×
639
  ): URIO[WalletAccessContext, Ed25519KeyPair] = {
640
    for {
×
641
      issuingPublicKey <- getKeyId(jwtIssuerDID, verificationRelationship, keyId)
×
642
      ed25519keyPair <- managedDIDService
×
643
        .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingPublicKey.id)
×
644
        .map(_.collect { case keyPair: Ed25519KeyPair => keyPair })
×
645
        .someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingPublicKey.id, issuingPublicKey.publicKeyData.crv.name))
×
646
        .orDieAsUnmanagedFailure
×
647
    } yield ed25519keyPair
648
  }
649

650
  /** @param jwtIssuerDID
651
    *   This can holder prism did / issuer prism did
652
    * @param verificationRelationship
653
    *   Holder it Authentication and Issuer it is AssertionMethod
654
    * @param keyId
655
    *   Optional KID parameter in case of DID has multiple keys with same purpose
656
    * @return
657
    *   JwtIssuer
658
    * @see
659
    *   org.hyperledger.identus.pollux.vc.jwt.Issuer
660
    */
661
  private def getSDJwtIssuer(
×
662
      jwtIssuerDID: PrismDID,
663
      verificationRelationship: VerificationRelationship,
664
      keyId: Option[KeyId]
665
  ): URIO[WalletAccessContext, JwtIssuer] = {
666
    for {
×
667
      ed25519keyPair <- getEd25519SigningKeyPair(jwtIssuerDID, verificationRelationship, keyId)
×
668
    } yield {
669
      JwtIssuer(
670
        jwtIssuerDID.did,
×
671
        EdSigner(ed25519keyPair, keyId),
×
672
        ed25519keyPair.publicKey.toJava
×
673
      )
674
    }
675
  }
676

677
  private[this] def generateCredentialRequest(
1✔
678
      recordId: DidCommID,
679
      getIssuer: (
680
          did: LongFormPrismDID,
681
          verificationRelation: VerificationRelationship,
682
          keyId: Option[KeyId]
683
      ) => URIO[WalletAccessContext, JwtIssuer]
684
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] = {
685
    for {
1✔
686
      record <- getRecordWithState(recordId, ProtocolState.RequestPending)
1✔
687
      subjectId <- ZIO
1✔
688
        .fromOption(record.subjectId)
689
        .orDieWith(_ => RuntimeException(s"No 'subjectId' found in record: ${recordId.value}"))
×
690
      formatAndOffer <- ZIO
1✔
691
        .fromOption(record.offerCredentialFormatAndData)
1✔
692
        .orDieWith(_ => RuntimeException(s"No 'offer' found in record: ${recordId.value}"))
×
693
      subjectDID <- validatePrismDID(subjectId)
1✔
694
      longFormPrismDID <- getLongForm(subjectDID, true)
1✔
695
      jwtIssuer <- getIssuer(longFormPrismDID, VerificationRelationship.Authentication, record.keyId)
1✔
696
      presentationPayload <- createPresentationPayload(record, jwtIssuer)
1✔
697
      signedPayload = JwtPresentation.encodeJwt(presentationPayload.toJwtPresentationPayload, jwtIssuer)
1✔
698
      request = createDidCommRequestCredential(formatAndOffer._1, formatAndOffer._2, signedPayload)
1✔
699
      count <- credentialRepository
1✔
700
        .updateWithJWTRequestCredential(recordId, request, ProtocolState.RequestGenerated)
1✔
701
        @@ CustomMetricsAspect.endRecordingTime(
1✔
702
          s"${record.id}_issuance_flow_holder_req_pending_to_generated",
1✔
703
          "issuance_flow_holder_req_pending_to_generated_ms_gauge"
704
        ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent")
1✔
705
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
706
      _ <- messageProducer
1✔
707
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
708
        .orDie
709
      record <- credentialRepository.getById(record.id)
1✔
710
    } yield record
711
  }
712

713
  override def generateJWTCredentialRequest(
1✔
714
      recordId: DidCommID
715
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] =
716
    generateCredentialRequest(recordId, getJwtIssuer)
1✔
717

718
  override def generateSDJWTCredentialRequest(
×
719
      recordId: DidCommID
720
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] =
721
    generateCredentialRequest(recordId, getSDJwtIssuer)
×
722

723
  override def generateAnonCredsCredentialRequest(
1✔
724
      recordId: DidCommID
725
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
726
    for {
1✔
727
      record <- getRecordWithState(recordId, ProtocolState.RequestPending)
1✔
728
      offerCredential <- ZIO
1✔
729
        .fromOption(record.offerCredentialData)
730
        .orDieWith(_ => RuntimeException(s"No 'offer' found in record: ${recordId.value}"))
×
731
      body = RequestCredential.Body(goal_code = Some("Request Credential"))
1✔
732
      createCredentialRequest <- createAnonCredsRequestCredential(offerCredential)
1✔
733
      attachments = Seq(
1✔
734
        AttachmentDescriptor.buildBase64Attachment(
1✔
735
          mediaType = Some("application/json"),
736
          format = Some(IssueCredentialRequestFormat.Anoncred.name),
737
          payload = createCredentialRequest.request.data.getBytes()
1✔
738
        )
739
      )
740
      requestMetadata = createCredentialRequest.metadata
741
      request = RequestCredential(
1✔
742
        body = body,
743
        attachments = attachments,
744
        from =
745
          offerCredential.to.getOrElse(throw new IllegalArgumentException("OfferCredential must have a recipient")),
×
746
        to = offerCredential.from,
747
        thid = offerCredential.thid
748
      )
749
      count <- credentialRepository
1✔
750
        .updateWithAnonCredsRequestCredential(recordId, request, requestMetadata, ProtocolState.RequestGenerated)
1✔
751
        @@ CustomMetricsAspect.endRecordingTime(
1✔
752
          s"${record.id}_issuance_flow_holder_req_pending_to_generated",
1✔
753
          "issuance_flow_holder_req_pending_to_generated_ms_gauge"
754
        ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent")
1✔
755
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
756
      _ <- messageProducer
1✔
757
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
758
        .orDie
759
      record <- credentialRepository.getById(record.id)
1✔
760
    } yield record
761
  }
762

763
  private def createAnonCredsRequestCredential(
1✔
764
      offerCredential: OfferCredential
765
  ): URIO[WalletAccessContext, AnoncredCreateCrendentialRequest] = {
766
    for {
1✔
767
      attachmentData <- ZIO
1✔
768
        .fromOption(
769
          offerCredential.attachments
770
            .find(_.format.contains(IssueCredentialOfferFormat.Anoncred.name))
1✔
771
            .map(_.data)
1✔
772
            .flatMap {
1✔
773
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
774
              case _             => None
×
775
            }
776
        )
777
        .orDieWith(_ => RuntimeException(s"No AnonCreds attachment found in the offer"))
×
778
      credentialOffer = anoncreds.AnoncredCredentialOffer(attachmentData)
779
      credDefContent <- uriResolver
1✔
780
        .resolve(credentialOffer.getCredDefId)
1✔
781
        .orDieAsUnmanagedFailure
1✔
782
      credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent)
783
      linkSecret <- linkSecretService.fetchOrCreate()
1✔
784
      createCredentialRequest = AnoncredLib.createCredentialRequest(linkSecret, credentialDefinition, credentialOffer)
1✔
785
    } yield createCredentialRequest
1✔
786
  }
787

788
  override def receiveCredentialRequest(
1✔
789
      request: RequestCredential
790
  ): ZIO[WalletAccessContext, InvalidCredentialRequest | RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] = {
791
    for {
1✔
792
      thid <- ZIO
1✔
793
        .fromOption(request.thid.map(DidCommID(_)))
1✔
794
        .mapError(_ => InvalidCredentialRequest("No 'thid' found"))
795
      record <- getRecordWithThreadIdAndStates(
1✔
796
        thid,
797
        ignoreWithZeroRetries = true,
798
        ProtocolState.InvitationGenerated,
799
        ProtocolState.OfferPending,
800
        ProtocolState.OfferSent
801
      )
802
      _ <- credentialRepository.updateWithJWTRequestCredential(record.id, request, ProtocolState.RequestReceived)
1✔
803
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
804
      _ <- messageProducer
1✔
805
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
806
        .orDie
807
      record <- credentialRepository.getById(record.id)
1✔
808
    } yield record
809
  }
810

811
  override def acceptCredentialRequest(
1✔
812
      recordId: DidCommID
813
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
814
    for {
1✔
815
      record <- getRecordWithState(recordId, ProtocolState.RequestReceived)
1✔
816
      request <- ZIO
1✔
817
        .fromOption(record.requestCredentialData)
818
        .orDieWith(_ => RuntimeException(s"No 'requestCredentialData' found in record: ${recordId.value}"))
×
819
      issue = createDidCommIssueCredential(request)
1✔
820
      count <- credentialRepository
1✔
821
        .updateWithIssueCredential(recordId, issue, ProtocolState.CredentialPending)
1✔
822
        @@ CustomMetricsAspect.startRecordingTime(
1✔
823
          s"${record.id}_issuance_flow_issuer_credential_pending_to_generated"
1✔
824
        )
825
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
826
      _ <- messageProducer
1✔
827
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
828
        .orDie
829
      record <- credentialRepository.getById(record.id)
1✔
830
    } yield record
831
  }
832

833
  override def receiveCredentialIssue(
1✔
834
      issueCredential: IssueCredential
835
  ): ZIO[WalletAccessContext, InvalidCredentialIssue | RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] =
836
    for {
1✔
837
      thid <- ZIO
1✔
838
        .fromOption(issueCredential.thid.map(DidCommID(_)))
1✔
839
        .mapError(_ => InvalidCredentialIssue("No 'thid' found"))
840
      record <- getRecordWithThreadIdAndStates(
1✔
841
        thid,
842
        ignoreWithZeroRetries = true,
843
        ProtocolState.RequestPending,
844
        ProtocolState.RequestSent
845
      )
846
      attachment <- ZIO
1✔
847
        .fromOption(issueCredential.attachments.headOption)
1✔
848
        .mapError(_ => InvalidCredentialIssue("No attachment found"))
849

850
      _ <- {
1✔
851
        val result = attachment match {
852
          case AttachmentDescriptor(
853
                id,
854
                media_type,
855
                Base64(v),
856
                Some(IssueCredentialIssuedFormat.Anoncred.name),
857
                _,
858
                _,
859
                _,
860
                _
861
              ) =>
1✔
862
            for {
1✔
863
              processedCredential <- processAnonCredsCredential(record, java.util.Base64.getUrlDecoder.decode(v))
1✔
864
              attachment = AttachmentDescriptor.buildBase64Attachment(
1✔
865
                id = id,
866
                mediaType = media_type,
867
                format = Some(IssueCredentialIssuedFormat.Anoncred.name),
868
                payload = processedCredential.data.getBytes
1✔
869
              )
870
              processedIssuedCredential = issueCredential.copy(attachments = Seq(attachment))
1✔
871
              result <-
1✔
872
                updateWithCredential(
1✔
873
                  processedIssuedCredential,
874
                  record,
875
                  attachment,
876
                  Some(List(processedCredential.getSchemaId)),
1✔
877
                  Some(processedCredential.getCredDefId)
1✔
878
                )
879
            } yield result
880
          case attachment =>
1✔
881
            updateWithCredential(issueCredential, record, attachment, None, None)
1✔
882
        }
883
        result
884
      }
885
      record <- credentialRepository.getById(record.id)
1✔
886
    } yield record
887

888
  private def updateWithCredential(
1✔
889
      issueCredential: IssueCredential,
890
      record: IssueCredentialRecord,
891
      attachment: AttachmentDescriptor,
892
      schemaId: Option[List[String]],
893
      credDefId: Option[String]
894
  ) = {
895
    credentialRepository
896
      .updateWithIssuedRawCredential(
1✔
897
        record.id,
898
        issueCredential,
899
        attachment.data.toJson,
1✔
900
        schemaId,
901
        credDefId,
902
        ProtocolState.CredentialReceived
903
      )
904
  }
905

906
  private def processAnonCredsCredential(
1✔
907
      record: IssueCredentialRecord,
908
      credentialBytes: Array[Byte]
909
  ): URIO[WalletAccessContext, anoncreds.AnoncredCredential] = {
910
    for {
1✔
911
      credential <- ZIO.succeed(anoncreds.AnoncredCredential(new String(credentialBytes)))
1✔
912
      credDefContent <- uriResolver
1✔
913
        .resolve(credential.getCredDefId)
1✔
914
        .orDieAsUnmanagedFailure
1✔
915
      credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent)
916
      metadata <- ZIO
1✔
917
        .fromOption(record.anonCredsRequestMetadata)
918
        .orDieWith(_ => RuntimeException(s"No AnonCreds request metadata found in record: ${record.id.value}"))
×
919
      linkSecret <- linkSecretService.fetchOrCreate()
1✔
920
      credential <- ZIO
1✔
921
        .attempt(
922
          AnoncredLib.processCredential(
1✔
923
            anoncreds.AnoncredCredential(new String(credentialBytes)),
1✔
924
            metadata,
925
            linkSecret,
926
            credentialDefinition
927
          )
928
        )
929
        .orDieWith(error => RuntimeException(s"AnonCreds credential processing error: ${error.getMessage}"))
×
930
    } yield credential
931
  }
932

933
  override def markOfferSent(
1✔
934
      recordId: DidCommID
935
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
936
    updateCredentialRecordProtocolState(
1✔
937
      recordId,
938
      IssueCredentialRecord.ProtocolState.OfferPending,
939
      IssueCredentialRecord.ProtocolState.OfferSent
940
    )
941

942
  override def markCredentialOfferInvitationExpired(
×
943
      recordId: DidCommID
944
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
945
    updateCredentialRecordProtocolState(
×
946
      recordId,
947
      IssueCredentialRecord.ProtocolState.RequestReceived,
948
      IssueCredentialRecord.ProtocolState.InvitationExpired
949
    )
950
  override def markRequestSent(
1✔
951
      recordId: DidCommID
952
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
953
    updateCredentialRecordProtocolState(
1✔
954
      recordId,
955
      IssueCredentialRecord.ProtocolState.RequestGenerated,
956
      IssueCredentialRecord.ProtocolState.RequestSent
957
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
958
      s"${recordId}_issuance_flow_holder_req_generated_to_sent",
1✔
959
      "issuance_flow_holder_req_generated_to_sent_ms_gauge"
960
    )
961

962
  private def markCredentialGenerated(
1✔
963
      record: IssueCredentialRecord,
964
      issueCredential: IssueCredential
965
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
966
    for {
1✔
967
      count <- credentialRepository
1✔
968
        .updateWithIssueCredential(record.id, issueCredential, IssueCredentialRecord.ProtocolState.CredentialGenerated)
1✔
969
        @@ CustomMetricsAspect.endRecordingTime(
1✔
970
          s"${record.id}_issuance_flow_issuer_credential_pending_to_generated",
1✔
971
          "issuance_flow_issuer_credential_pending_to_generated_ms_gauge"
972
        ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_issuer_credential_generated_to_sent")
1✔
973
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
974
      _ <- messageProducer
1✔
975
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
976
        .orDie
977
      record <- credentialRepository.getById(record.id)
1✔
978
    } yield record
979
  }
980

981
  override def markCredentialSent(
1✔
982
      recordId: DidCommID
983
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
984
    updateCredentialRecordProtocolState(
1✔
985
      recordId,
986
      IssueCredentialRecord.ProtocolState.CredentialGenerated,
987
      IssueCredentialRecord.ProtocolState.CredentialSent
988
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
989
      s"${recordId}_issuance_flow_issuer_credential_generated_to_sent",
1✔
990
      "issuance_flow_issuer_credential_generated_to_sent_ms_gauge"
991
    )
992

993
  override def reportProcessingFailure(
×
994
      recordId: DidCommID,
995
      failReason: Option[Failure]
996
  ): URIO[WalletAccessContext, Unit] =
997
    credentialRepository.updateAfterFail(recordId, failReason)
×
998

999
  private def getRecordWithState(
1✔
1000
      recordId: DidCommID,
1001
      state: ProtocolState
1002
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
1003
    for {
1✔
1004
      record <- credentialRepository.getById(recordId)
1✔
1005
      _ <- record.protocolState match {
1✔
1006
        case s if s == state => ZIO.unit
1✔
1007
        case s               => ZIO.fail(RecordNotFound(recordId, Some(s)))
1✔
1008
      }
1009
    } yield record
1✔
1010
  }
1011

1012
  private def getRecordWithThreadIdAndStates(
1✔
1013
      thid: DidCommID,
1014
      ignoreWithZeroRetries: Boolean,
1015
      states: ProtocolState*
1016
  ): ZIO[WalletAccessContext, RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] = {
1017
    for {
1✔
1018
      record <- credentialRepository
1✔
1019
        .findByThreadId(thid, ignoreWithZeroRetries)
1✔
1020
        .someOrFail(RecordNotFoundForThreadIdAndStates(thid, states*))
1021
      _ <- record.protocolState match {
1✔
1022
        case s if states.contains(s) => ZIO.unit
1✔
1023
        case state                   => ZIO.fail(RecordNotFoundForThreadIdAndStates(thid, states*))
1✔
1024
      }
1025
    } yield record
1✔
1026
  }
1027

1028
  private def createDidCommOfferCredential(
1✔
1029
      pairwiseIssuerDID: DidId,
1030
      pairwiseHolderDID: Option[DidId],
1031
      credentialSchemaRef: Option[List[CredentialSchemaRef]],
1032
      claims: Seq[Attribute],
1033
      thid: DidCommID,
1034
      challenge: String,
1035
      domain: String,
1036
      offerFormat: IssueCredentialOfferFormat
1037
  ): UIO[OfferCredential] = {
1038
    val maybeSchemaIds = credentialSchemaRef.map(_.map(_.id.toString))
1✔
1039
    for {
1✔
1040
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_ids = maybeSchemaIds, attributes = claims))
1✔
1041
      body = OfferCredential.Body(
1✔
1042
        goal_code = Some("Offer Credential"),
1043
        credential_preview = credentialPreview,
1044
      )
1045
      attachments <- ZIO.succeed(
1✔
1046
        Seq(
1✔
1047
          AttachmentDescriptor.buildJsonAttachment(
1✔
1048
            mediaType = Some("application/json"),
1049
            format = Some(offerFormat.name),
1050
            payload = PresentationAttachment(
1051
              Some(Options(challenge, domain)),
1052
              PresentationDefinition(format = Some(ClaimFormat(jwt = Some(Jwt(alg = Seq("ES256K"))))))
1✔
1053
            )
1054
          )
1055
        )
1056
      )
1057
    } yield OfferCredential(
1✔
1058
      body = body,
1059
      attachments = attachments,
1060
      from = pairwiseIssuerDID,
1061
      to = pairwiseHolderDID,
1062
      thid = Some(thid.value)
1✔
1063
    )
1064
  }
1065

1066
  private def createAnonCredsDidCommOfferCredential(
1✔
1067
      pairwiseIssuerDID: DidId,
1068
      pairwiseHolderDID: Option[DidId],
1069
      schemaUri: String,
1070
      credentialDefinitionGUID: UUID,
1071
      credentialDefinitionId: String,
1072
      claims: Seq[Attribute],
1073
      thid: DidCommID
1074
  ): URIO[WalletAccessContext, OfferCredential] = {
1075
    for {
1✔
1076
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_ids = Some(List(schemaUri)), attributes = claims))
1✔
1077
      body = OfferCredential.Body(
1✔
1078
        goal_code = Some("Offer Credential"),
1079
        credential_preview = credentialPreview,
1080
      )
1081
      attachments <- createAnonCredsCredentialOffer(credentialDefinitionGUID, credentialDefinitionId).map { offer =>
1✔
1082
        Seq(
1✔
1083
          AttachmentDescriptor.buildBase64Attachment(
1✔
1084
            mediaType = Some("application/json"),
1085
            format = Some(IssueCredentialOfferFormat.Anoncred.name),
1086
            payload = offer.data.getBytes()
1✔
1087
          )
1088
        )
1089
      }
1090
    } yield OfferCredential(
1✔
1091
      body = body,
1092
      attachments = attachments,
1093
      from = pairwiseIssuerDID,
1094
      to = pairwiseHolderDID,
1095
      thid = Some(thid.value)
1✔
1096
    )
1097
  }
1098

1099
  private def createAnonCredsCredentialOffer(
1✔
1100
      credentialDefinitionGUID: UUID,
1101
      credentialDefinitionId: String
1102
  ): URIO[WalletAccessContext, AnoncredCredentialOffer] =
1103
    for {
1✔
1104
      credentialDefinition <- getCredentialDefinition(credentialDefinitionGUID)
1✔
1105
      cd = anoncreds.AnoncredCredentialDefinition(credentialDefinition.definition.toString)
1✔
1106
      kcp = anoncreds.AnoncredCredentialKeyCorrectnessProof(credentialDefinition.keyCorrectnessProof.toString)
1✔
1107
      credentialDefinitionSecret <- getCredentialDefinitionPrivatePart(credentialDefinition.guid)
1✔
1108
      cdp = anoncreds.AnoncredCredentialDefinitionPrivate(credentialDefinitionSecret.json.toString)
1✔
1109
      createCredentialDefinition = AnoncredCreateCredentialDefinition(cd, cdp, kcp)
1110
      offer = AnoncredLib.createOffer(createCredentialDefinition, credentialDefinitionId)
1✔
1111
    } yield offer
1✔
1112

1113
  private[this] def createDidCommRequestCredential(
1✔
1114
      format: IssueCredentialOfferFormat,
1115
      offer: OfferCredential,
1116
      signedPresentation: JWT
1117
  ): RequestCredential = {
1118
    RequestCredential(
1✔
1119
      body = RequestCredential.Body(
1120
        goal_code = offer.body.goal_code,
1121
        comment = offer.body.comment,
1122
      ),
1123
      attachments = Seq(
1✔
1124
        AttachmentDescriptor
1125
          .buildBase64Attachment(
1✔
1126
            mediaType = Some("application/json"),
1127
            format = Some(format.name),
1128
            // FIXME copy payload will probably not work for anoncreds!
1129
            payload = signedPresentation.value.getBytes(),
1✔
1130
          )
1131
      ),
1132
      thid = offer.thid.orElse(Some(offer.id)),
1✔
UNCOV
1133
      from = offer.to.getOrElse(throw new IllegalArgumentException("OfferCredential must have a recipient")),
×
1134
      to = offer.from
1135
    )
1136
  }
1137

1138
  private def createDidCommIssueCredential(request: RequestCredential): IssueCredential = {
1✔
1139
    IssueCredential(
1✔
1140
      body = IssueCredential.Body(
1141
        goal_code = request.body.goal_code,
1142
        comment = request.body.comment,
1143
        replacement_id = None,
1144
        more_available = None,
1145
      ),
1146
      attachments = Seq(), // FIXME !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1✔
1147
      thid = request.thid.orElse(Some(request.id)),
1✔
1148
      from = request.to,
1149
      to = request.from
1150
    )
1151
  }
1152

1153
  /** this is an auxiliary function.
1154
    *
1155
    * @note
1156
    *   Between updating and getting the CredentialRecord back the CredentialRecord can be updated by other operations
1157
    *   in the middle.
1158
    *
1159
    * TODO: this should be improved to behave exactly like atomic operation.
1160
    */
1161
  private def updateCredentialRecordProtocolState(
1✔
1162
      id: DidCommID,
1163
      from: IssueCredentialRecord.ProtocolState,
1164
      to: IssueCredentialRecord.ProtocolState
1165
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] = {
1166
    for {
1✔
1167
      record <- credentialRepository.getById(id)
1✔
1168
      updatedRecord <- record.protocolState match
1✔
1169
        case currentState if currentState == to => ZIO.succeed(record) // Idempotent behaviour
×
1170
        case currentState if currentState == from =>
1✔
1171
          credentialRepository.updateProtocolState(id, from, to) *> credentialRepository.getById(id)
1✔
1172
        case _ => ZIO.fail(InvalidStateForOperation(record.protocolState))
×
1173
    } yield updatedRecord
1174
  }
1175

1176
  override def generateJWTCredential(
1✔
1177
      recordId: DidCommID,
1178
      statusListRegistryUrl: String,
1179
  ): ZIO[WalletAccessContext, RecordNotFound | CredentialRequestValidationFailed, IssueCredentialRecord] = {
1180
    for {
1✔
1181
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
1✔
1182
      issuingDID <- ZIO
1✔
1183
        .fromOption(record.issuingDID)
1184
        .orElse(ZIO.dieMessage(s"Issuing DID not found in record: ${recordId.value}"))
×
1185
      issue <- ZIO
1✔
1186
        .fromOption(record.issueCredentialData)
1187
        .orElse(ZIO.dieMessage(s"Issue credential data not found in record: ${recordId.value}"))
×
1188
      longFormPrismDID <- getLongForm(issuingDID, true)
1✔
1189
      maybeOfferOptions <- getOptionsFromOfferCredentialData(record)
1✔
1190
      requestJwt <- getJwtFromRequestCredentialData(record)
1✔
1191
      offerCredentialData <- ZIO
1✔
1192
        .fromOption(record.offerCredentialData)
1193
        .orElse(ZIO.dieMessage(s"Offer credential data not found in record: ${recordId.value}"))
×
1194
      preview = offerCredentialData.body.credential_preview
1195
      claims <- CredentialService.convertAttributesToJsonClaims(preview.body.attributes).orDieAsUnmanagedFailure
1✔
1196
      jwtIssuer <- getJwtIssuer(longFormPrismDID, VerificationRelationship.AssertionMethod, record.keyId)
1✔
1197
      jwtPresentation <- validateRequestCredentialDataProof(maybeOfferOptions, requestJwt)
1✔
1198
        .tapError(error =>
1199
          credentialRepository
1200
            .updateProtocolState(record.id, ProtocolState.CredentialPending, ProtocolState.ProblemReportPending)
×
1201
        )
1202
        .orDieAsUnmanagedFailure
1✔
1203

1204
      // Custom for JWT
1205
      issuanceDate = Instant.now()
1✔
1206
      credentialStatus <- allocateNewCredentialInStatusListForWallet(record, statusListRegistryUrl, jwtIssuer)
1✔
1207
      // TODO: get schema when schema registry is available if schema ID is provided
1208
      w3Credential = W3cCredentialPayload(
1✔
1209
        `@context` = Set(
1✔
1210
          "https://www.w3.org/2018/credentials/v1"
1211
        ), // TODO: his information should come from Schema registry by record.schemaId
1212
        maybeId = None,
1213
        `type` =
1214
          Set("VerifiableCredential"), // TODO: This information should come from Schema registry by record.schemaId
1✔
1215
        issuer = CredentialIssuer(issuingDID.did.toString, `type` = "Profile"),
1✔
1216
        issuanceDate = issuanceDate,
1217
        maybeExpirationDate = record.validityPeriod.map(sec => issuanceDate.plusSeconds(sec.toLong)),
1✔
1218
        maybeCredentialSchema = record.schemaUris.map(ids =>
1✔
1219
          ids.map(id => org.hyperledger.identus.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE))
×
1220
        ),
1221
        maybeCredentialStatus = Some(credentialStatus),
1222
        credentialSubject = claims.add("id", Json.Str(jwtPresentation.iss)),
1✔
1223
        maybeRefreshService = None,
1224
        maybeEvidence = None,
1225
        maybeTermsOfUse = None,
1226
        maybeValidFrom = None,
1227
        maybeValidUntil = None
1228
      )
1229
      signedJwtCredential = W3CCredential.toEncodedJwt(w3Credential, jwtIssuer)
1✔
1230
      issueCredential = IssueCredential.build(
1✔
1231
        fromDID = issue.from,
1232
        toDID = issue.to,
1233
        thid = issue.thid,
1234
        credentials = Seq(IssueCredentialIssuedFormat.JWT -> signedJwtCredential.value.getBytes)
1✔
1235
      )
1236
      // End custom
1237

1238
      record <- markCredentialGenerated(record, issueCredential)
1✔
1239
    } yield record
1240
  }
1241

1242
  override def generateSDJWTCredential(
×
1243
      recordId: DidCommID,
1244
      expirationTime: Duration,
1245
  ): ZIO[
1246
    WalletAccessContext,
1247
    RecordNotFound | ExpirationDateHasPassed | VCJwtHeaderParsingError,
1248
    IssueCredentialRecord
1249
  ] = {
1250
    for {
×
1251
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
×
1252
      issuingDID <- ZIO
×
1253
        .fromOption(record.issuingDID)
1254
        .orElse(ZIO.dieMessage(s"Issuing DID not found in record: ${recordId.value}"))
×
1255
      issue <- ZIO
×
1256
        .fromOption(record.issueCredentialData)
1257
        .orElse(ZIO.dieMessage(s"Issue credential data not found in record: ${recordId.value}"))
×
1258
      longFormPrismDID <- getLongForm(issuingDID, true)
×
1259
      maybeOfferOptions <- getOptionsFromOfferCredentialData(record)
×
1260
      requestJwt <- getJwtFromRequestCredentialData(record)
×
1261
      offerCredentialData <- ZIO
×
1262
        .fromOption(record.offerCredentialData)
1263
        .orElse(ZIO.dieMessage(s"Offer credential data not found in record: ${recordId.value}"))
×
1264
      preview = offerCredentialData.body.credential_preview
1265
      claims <- CredentialService.convertAttributesToJsonClaims(preview.body.attributes).orDieAsUnmanagedFailure
×
1266
      jwtPresentation <- validateRequestCredentialDataProof(maybeOfferOptions, requestJwt)
×
1267
        .tapError(error =>
1268
          credentialRepository
1269
            .updateProtocolState(record.id, ProtocolState.CredentialPending, ProtocolState.ProblemReportPending)
×
1270
        )
1271
        .orDieAsUnmanagedFailure
×
1272
      jwtHeader <- JWTVerification.extractJwtHeader(requestJwt) match
×
1273
        case ZValidation.Success(log, header) => ZIO.succeed(header)
×
1274
        case ZValidation.Failure(log, failure) =>
×
1275
          ZIO.fail(VCJwtHeaderParsingError(s"Extraction of JwtHeader failed ${failure.toChunk.toString}"))
×
1276
      ed25519KeyPair <- getEd25519SigningKeyPair(
×
1277
        longFormPrismDID,
1278
        VerificationRelationship.AssertionMethod,
1279
        record.keyId
1280
      )
1281
      sdJwtPrivateKey = sdjwt.IssuerPrivateKey(ed25519KeyPair.privateKey)
×
1282
      jsonWebKey <- didResolver.resolve(jwtPresentation.iss) flatMap {
×
1283
        case failed: DIDResolutionFailed =>
×
1284
          ZIO.dieMessage(s"Error occurred while resolving the DID: ${failed.error.toString}")
×
1285
        case succeeded: DIDResolutionSucceeded =>
×
1286
          jwtHeader.keyId match {
1287
            case Some(
1288
                  kid
1289
                ) => // TODO should we check in authentication and assertion or just in verificationMethod since this cane different how did document is implemented
×
1290
              ZIO
×
1291
                .fromOption(succeeded.didDocument.verificationMethod.find(_.id.endsWith(kid)).map(_.publicKeyJwk))
×
1292
                .orElse(
1293
                  ZIO.dieMessage(
×
1294
                    s"Required public Key for holder binding is not found in DID document for the kid: $kid"
×
1295
                  )
1296
                )
1297
            case None =>
×
1298
              ZIO.succeed(None) // JwtHeader keyId is None, Issued credential is not bound to any holder public key
1299
          }
1300
      }
1301

1302
      now = Instant.now.getEpochSecond
×
1303
      exp = claims.get("exp").flatMap(_.asNumber).map(_.value.longValue())
×
1304
      expInSeconds <- ZIO.fromEither(exp match {
×
1305
        case Some(e) if e > now => Right(e)
×
1306
        case Some(e)            => Left(ExpirationDateHasPassed(e))
×
1307
        case _                  => Right(Instant.now.plus(expirationTime).getEpochSecond)
×
1308
      })
1309
      claimsUpdated = claims
1310
        .add("iss", Json.Str(issuingDID.did.toString)) // This is issuer did
×
1311
        .add("sub", Json.Str(jwtPresentation.iss)) // This is subject did
×
1312
        .add("iat", Json.Num(now))
×
1313
        .add("exp", Json.Num(expInSeconds))
×
1314
      credential = {
1315
        jsonWebKey match {
1316
          case Some(jwk) =>
×
1317
            SDJWT.issueCredential(
×
1318
              sdJwtPrivateKey,
1319
              claimsUpdated.toJson,
×
1320
              sdjwt.HolderPublicKey.fromJWT(jwk.toJson)
×
1321
            )
1322
          case None =>
×
1323
            SDJWT.issueCredential(
×
1324
              sdJwtPrivateKey,
1325
              claimsUpdated.toJson,
×
1326
            )
1327
        }
1328
      }
1329
      issueCredential = IssueCredential.build(
×
1330
        fromDID = issue.from,
1331
        toDID = issue.to,
1332
        thid = issue.thid,
1333
        credentials = Seq(IssueCredentialIssuedFormat.SDJWT -> credential.compact.getBytes)
×
1334
      )
1335
      record <- markCredentialGenerated(record, issueCredential)
×
1336
    } yield record
1337

1338
  }
1339

1340
  private def allocateNewCredentialInStatusListForWallet(
1✔
1341
      record: IssueCredentialRecord,
1342
      statusListRegistryUrl: String,
1343
      jwtIssuer: JwtIssuer
1344
  ): URIO[WalletAccessContext, CredentialStatus] =
1345
    for {
1✔
1346
      cslAndIndex <- credentialStatusListRepository.incrementAndGetStatusListIndex(
1✔
1347
        jwtIssuer,
1348
        statusListRegistryUrl
1349
      )
1350
      statusListId = cslAndIndex._1
1✔
1351
      indexInStatusList = cslAndIndex._2
1✔
1352
      _ <- credentialStatusListRepository.allocateSpaceForCredential(
1✔
1353
        issueCredentialRecordId = record.id,
1354
        credentialStatusListId = statusListId,
1355
        statusListIndex = indexInStatusList
1356
      )
1357
    } yield CredentialStatus(
1✔
1358
      id = s"$statusListRegistryUrl/credential-status/$statusListId#$indexInStatusList",
1✔
1359
      `type` = "StatusList2021Entry",
1360
      statusPurpose = StatusPurpose.Revocation,
1361
      statusListIndex = indexInStatusList,
1362
      statusListCredential = s"$statusListRegistryUrl/credential-status/$statusListId"
1✔
1363
    )
1364

1365
  override def generateAnonCredsCredential(
1✔
1366
      recordId: DidCommID
1367
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
1368
    for {
1✔
1369
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
1✔
1370
      requestCredential <- ZIO
1✔
1371
        .fromOption(record.requestCredentialData)
1372
        .orElse(ZIO.dieMessage(s"No request credential data found in record: ${record.id}"))
×
1373
      body = IssueCredential.Body(goal_code = Some("Issue Credential"))
1✔
1374
      attachments <- createAnonCredsCredential(record).map { credential =>
1✔
1375
        Seq(
1✔
1376
          AttachmentDescriptor.buildBase64Attachment(
1✔
1377
            mediaType = Some("application/json"),
1378
            format = Some(IssueCredentialIssuedFormat.Anoncred.name),
1379
            payload = credential.data.getBytes()
1✔
1380
          )
1381
        )
1382
      }
1383
      issueCredential = IssueCredential(
1✔
1384
        body = body,
1385
        attachments = attachments,
1386
        from = requestCredential.to,
1387
        to = requestCredential.from,
1388
        thid = requestCredential.thid
1389
      )
1390
      record <- markCredentialGenerated(record, issueCredential)
1✔
1391
    } yield record
1392
  }
1393

1394
  private def createAnonCredsCredential(
1✔
1395
      record: IssueCredentialRecord
1396
  ): URIO[WalletAccessContext, AnoncredCredential] = {
1397
    for {
1✔
1398
      credentialDefinitionId <- ZIO
1✔
1399
        .fromOption(record.credentialDefinitionId)
1400
        .orElse(ZIO.dieMessage(s"No credential definition Id found in record: ${record.id}"))
×
1401
      credentialDefinition <- getCredentialDefinition(credentialDefinitionId)
1✔
1402
      cd = anoncreds.AnoncredCredentialDefinition(credentialDefinition.definition.toString)
1✔
1403
      offerCredential <- ZIO
1✔
1404
        .fromOption(record.offerCredentialData)
1405
        .orElse(ZIO.dieMessage(s"No offer credential data found in record: ${record.id}"))
×
1406
      offerCredentialAttachmentData <- ZIO
1✔
1407
        .fromOption(
1408
          offerCredential.attachments
1409
            .find(_.format.contains(IssueCredentialOfferFormat.Anoncred.name))
1✔
1410
            .map(_.data)
1✔
1411
            .flatMap {
1✔
1412
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
1413
              case _             => None
×
1414
            }
1415
        )
1416
        .orElse(ZIO.dieMessage(s"No 'AnonCreds' offer credential attachment found in record: ${record.id}"))
×
1417
      credentialOffer = anoncreds.AnoncredCredentialOffer(offerCredentialAttachmentData)
1418
      requestCredential <- ZIO
1✔
1419
        .fromOption(record.requestCredentialData)
1420
        .orElse(ZIO.dieMessage(s"No request credential data found in record: ${record.id}"))
×
1421
      requestCredentialAttachmentData <- ZIO
1✔
1422
        .fromOption(
1423
          requestCredential.attachments
1424
            .find(_.format.contains(IssueCredentialRequestFormat.Anoncred.name))
1✔
1425
            .map(_.data)
1✔
1426
            .flatMap {
1✔
1427
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
1428
              case _             => None
×
1429
            }
1430
        )
1431
        .orElse(ZIO.dieMessage(s"No 'AnonCreds' request credential attachment found in record: ${record.id}"))
×
1432
      credentialRequest = anoncreds.AnoncredCredentialRequest(requestCredentialAttachmentData)
1433
      attrValues = offerCredential.body.credential_preview.body.attributes.map { attr =>
1✔
1434
        (attr.name, attr.value)
1435
      }
1436
      credentialDefinitionSecret <- getCredentialDefinitionPrivatePart(credentialDefinition.guid)
1✔
1437
      cdp = anoncreds.AnoncredCredentialDefinitionPrivate(credentialDefinitionSecret.json.toString)
1✔
1438
      credential =
1439
        AnoncredLib.createCredential(
1✔
1440
          cd,
1441
          cdp,
1442
          credentialOffer,
1443
          credentialRequest,
1444
          attrValues
1445
        )
1446
    } yield credential
1✔
1447
  }
1448

1449
  private def getOptionsFromOfferCredentialData(record: IssueCredentialRecord): UIO[Option[Options]] = {
1✔
1450
    for {
1✔
1451
      offer <- ZIO
1✔
1452
        .fromOption(record.offerCredentialData)
1453
        .orElse(ZIO.dieMessage(s"Offer data not found in record: ${record.id}"))
×
1454
      attachmentDescriptor <- ZIO
1✔
1455
        .fromOption(offer.attachments.headOption)
1✔
1456
        .orElse(ZIO.dieMessage(s"Attachments not found in record: ${record.id}"))
×
1457
      json <- attachmentDescriptor.data match
1✔
1458
        case JsonData(json) => ZIO.succeed(json)
1✔
1459
        case _              => ZIO.dieMessage(s"Attachment doesn't contain JsonData: ${record.id}")
×
1460
      maybeOptions <- ZIO
1✔
1461
        .fromEither(json.as[PresentationAttachment].map(_.options))
1✔
1462
        .flatMapError(err => ZIO.dieMessage(err))
×
1463
    } yield maybeOptions
1464
  }
1465

1466
  private def getJwtFromRequestCredentialData(record: IssueCredentialRecord): UIO[JWT] = {
1✔
1467
    for {
1✔
1468
      request <- ZIO
1✔
1469
        .fromOption(record.requestCredentialData)
1470
        .orElse(ZIO.dieMessage(s"Request data not found in record: ${record.id}"))
×
1471
      attachmentDescriptor <- ZIO
1✔
1472
        .fromOption(request.attachments.headOption)
1✔
1473
        .orElse(ZIO.dieMessage(s"Attachment not found in record: ${record.id}"))
×
1474
      jwt <- attachmentDescriptor.data match
1✔
1475
        case Base64(b64) =>
1✔
1476
          ZIO.succeed {
1477
            val base64Decoded = new String(java.util.Base64.getUrlDecoder.decode(b64))
1✔
1478
            JWT(base64Decoded)
1✔
1479
          }
1480
        case _ => ZIO.dieMessage(s"Attachment does not contain Base64Data: ${record.id}")
×
1481
    } yield jwt
1482
  }
1483

1484
  private def validateRequestCredentialDataProof(
1✔
1485
      maybeOptions: Option[Options],
1486
      jwt: JWT
1487
  ): IO[CredentialRequestValidationFailed, JwtPresentationPayload] = {
1488
    for {
1✔
1489
      _ <- maybeOptions match
1✔
1490
        case None => ZIO.unit
×
1491
        case Some(options) =>
1✔
1492
          JwtPresentation.validatePresentation(jwt, options.domain, options.challenge) match
1✔
1493
            case ZValidation.Success(log, value) => ZIO.unit
1✔
1494
            case ZValidation.Failure(log, error) =>
×
1495
              ZIO.fail(
×
1496
                CredentialRequestValidationFailed(s"JWTPresentation validation failed: ${error.toList.mkString(";")}")
×
1497
              )
1498

1499
      clock = java.time.Clock.system(ZoneId.systemDefault)
1✔
1500
      verificationResult <- JwtPresentation
1✔
1501
        .verify(
1502
          jwt,
1503
          JwtPresentation.PresentationVerificationOptions(
1✔
1504
            maybeProofPurpose = Some(VerificationRelationship.Authentication),
1505
            verifySignature = true,
1506
            verifyDates = false,
1507
            leeway = Duration.Zero
1508
          )
1509
        )(didResolver, uriResolver)(clock)
1✔
1510
        .mapError(errors => CredentialRequestValidationFailed(errors*))
1511

1512
      result <- verificationResult match
1✔
1513
        case ZValidation.Success(log, value) => ZIO.unit
1✔
1514
        case ZValidation.Failure(log, error) =>
×
1515
          ZIO.fail(CredentialRequestValidationFailed(s"JWT presentation verification failed: $error"))
×
1516

1517
      jwtPresentation <- ZIO
1✔
1518
        .fromTry(JwtPresentation.decodeJwt[JwtPresentationPayload](jwt))
1✔
1519
        .mapError(t => CredentialRequestValidationFailed(s"JWT presentation decoding failed: ${t.getMessage}"))
×
1520
    } yield jwtPresentation
1521
  }
1522

1523
  override def getCredentialOfferInvitation(
×
1524
      pairwiseHolderDID: DidId,
1525
      invitation: String
1526
  ): ZIO[WalletAccessContext, CredentialServiceError, OfferCredential] = {
1527
    for {
×
1528
      invitation <- ZIO
×
1529
        .fromEither(Base64Utils.decodeUrlToString(invitation).fromJson[Invitation])
×
1530
        .mapError(err => InvitationParsingError(err))
1531
      _ <- invitation.expires_time match {
×
1532
        case Some(expiryTime) =>
×
1533
          ZIO
×
1534
            .fail(InvitationExpired(expiryTime))
1535
            .when(Instant.now().getEpochSecond > expiryTime)
×
1536
        case None => ZIO.unit
×
1537
      }
1538
      _ <- getIssueCredentialRecordByThreadId(DidCommID(invitation.id), false)
×
1539
        .flatMap {
1540
          case None    => ZIO.unit
×
1541
          case Some(_) => ZIO.fail(InvitationAlreadyReceived(invitation.id))
×
1542
        }
1543
      credentialOffer <- ZIO.fromEither {
×
1544
        invitation.attachments
1545
          .flatMap(
×
1546
            _.headOption.map(attachment =>
×
1547
              attachment.data.toJson
×
1548
                .fromJson[org.hyperledger.identus.mercury.model.JsonData]
1549
                .flatMap { data =>
×
1550
                  OfferCredential.given_JsonDecoder_OfferCredential
×
1551
                    .decodeJson(data.json.toJson)
×
1552
                    .map(r => r.copy(to = Some(pairwiseHolderDID)))
×
1553
                    .leftMap(err =>
×
1554
                      CredentialOfferDecodingError(
1555
                        s"Credential Offer As Attachment decoding error: $err"
×
1556
                      )
1557
                    )
1558
                }
1559
                .leftMap(err => CredentialOfferDecodingError(s"Invitation Attachment JsonData decoding error: $err"))
×
1560
            )
1561
          )
1562
          .getOrElse(
×
1563
            Left(MissingInvitationAttachment("Missing Invitation Attachment for Credential Offer"))
×
1564
          )
1565
      }
1566
    } yield credentialOffer
1567

1568
  }
1569
}
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