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

hyperledger / identus-cloud-agent / 10928456511

18 Sep 2024 06:45PM CUT coverage: 48.836% (-0.1%) from 48.942%
10928456511

Pull #1366

CryptoKnightIOG
ATL-7775: Default Backend API to Array Of Credential Schema

Signed-off-by: Bassam Riman <bassam.riman@iohk.io>
Pull Request #1366: feat: Default Backend API to Array Of Credential Schema

24 of 32 new or added lines in 6 files covered. (75.0%)

310 existing lines in 58 files now uncovered.

7570 of 15501 relevant lines covered (48.84%)

0.49 hits per line

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

71.71
/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.{DataUrlResolver, GenericUriResolver}
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.net.URI
39
import java.time.{Instant, ZoneId}
40
import java.util.UUID
41
import scala.language.implicitConversions
42

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

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

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

95
  import CredentialServiceImpl.*
96
  import IssueCredentialRecord.*
97

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

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

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

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

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

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

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

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

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

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

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

361
      format <- ZIO
1✔
362
        .fromOption(attachment.format)
363
        .mapError(_ => InvalidCredentialOffer("No attachment format found"))
364

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

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

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

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

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

457
  private[this] def getCredentialDefinition(
1✔
458
      guid: UUID
459
  ): UIO[CredentialDefinition] = credentialDefinitionService
460
    .getByGUID(guid)
1✔
461
    .orDieAsUnmanagedFailure
1✔
462

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

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

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

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

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

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

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

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

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

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

670
  override def generateSDJWTCredentialRequest(
×
671
      recordId: DidCommID
672
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] =
673
    generateCredentialRequest(recordId, getSDJwtIssuer)
×
674

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1173
      record <- markCredentialGenerated(record, issueCredential)
1✔
1174
    } yield record
1175
  }
1176

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

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

1269
  }
1270

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

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

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

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

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

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

1434
      clock = java.time.Clock.system(ZoneId.systemDefault)
1✔
1435

1436
      genericUriResolver = GenericUriResolver(
1✔
1437
        Map(
1✔
1438
          "data" -> DataUrlResolver(),
1✔
1439
        )
1440
      )
1441
      verificationResult <- JwtPresentation
1✔
1442
        .verify(
1443
          jwt,
1444
          JwtPresentation.PresentationVerificationOptions(
1✔
1445
            maybeProofPurpose = Some(VerificationRelationship.Authentication),
1446
            verifySignature = true,
1447
            verifyDates = false,
1448
            leeway = Duration.Zero
1449
          )
1450
        )(didResolver, genericUriResolver)(clock)
1✔
1451
        .mapError(errors => CredentialRequestValidationFailed(errors*))
1452

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

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

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

1510
  }
1511
}
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