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

hyperledger / identus-cloud-agent / 10793991050

10 Sep 2024 01:56PM CUT coverage: 48.504% (-4.5%) from 52.962%
10793991050

push

web-flow
build: sbt and plugins dependency update (#1337)

Signed-off-by: Hyperledger Bot <hyperledger-bot@hyperledger.org>
Signed-off-by: Yurii Shynbuiev <yurii.shynbuiev@iohk.io>
Co-authored-by: Hyperledger Bot <hyperledger-bot@hyperledger.org>
Co-authored-by: Yurii Shynbuiev <yurii.shynbuiev@iohk.io>

7406 of 15269 relevant lines covered (48.5%)

0.49 hits per line

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

71.43
/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.pollux.vc.jwt.DID.*
30
import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Ed25519PublicKey, Secp256k1KeyPair}
31
import org.hyperledger.identus.shared.http.{DataUrlResolver, GenericUriResolver}
32
import org.hyperledger.identus.shared.models.*
33
import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect
34
import org.hyperledger.identus.shared.utils.Base64Utils
35
import zio.*
36
import zio.json.*
37
import zio.prelude.ZValidation
38

39
import java.net.URI
40
import java.time.{Instant, ZoneId}
41
import java.util.UUID
42
import scala.language.implicitConversions
43

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

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

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

96
  import CredentialServiceImpl.*
97
  import IssueCredentialRecord.*
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
      thid: DidCommID,
130
      schemaUri: Option[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
          schemaUri = schemaUri,
165
          credentialDefinitionId = credentialDefinitionGUID,
166
          credentialDefinitionUri = credentialDefinitionId,
167
          credentialFormat = credentialFormat,
168
          invitation = invitation,
169
          role = IssueCredentialRecord.Role.Issuer,
170
          subjectId = None,
171
          keyId = None,
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
      thid: DidCommID,
198
      maybeSchemaId: Option[String],
199
      claims: Json,
200
      validityPeriod: Option[Double],
201
      automaticIssuance: Option[Boolean],
202
      issuingDID: CanonicalPrismDID,
203
      goalCode: Option[String],
204
      goal: Option[String],
205
      expirationDuration: Option[Duration],
206
      connectionId: Option[UUID],
207
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
208
    for {
1✔
209
      _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaId)
1✔
210
      attributes <- CredentialService.convertJsonClaimsToAttributes(claims)
1✔
211
      offer <- createDidCommOfferCredential(
1✔
212
        pairwiseIssuerDID = pairwiseIssuerDID,
213
        pairwiseHolderDID = pairwiseHolderDID,
214
        maybeSchemaId = maybeSchemaId,
215
        claims = attributes,
216
        thid = thid,
217
        UUID.randomUUID().toString,
1✔
218
        "domain",
219
        IssueCredentialOfferFormat.JWT
220
      )
221
      record <- createIssueCredentialRecord(
1✔
222
        pairwiseIssuerDID = pairwiseIssuerDID,
223
        thid = thid,
224
        schemaUri = maybeSchemaId,
225
        validityPeriod = validityPeriod,
226
        automaticIssuance = automaticIssuance,
227
        issuingDID = Some(issuingDID),
228
        credentialFormat = CredentialFormat.JWT,
229
        offer = offer,
230
        credentialDefinitionGUID = None,
231
        credentialDefinitionId = None,
232
        connectionId = connectionId,
233
        goalCode = goalCode,
234
        goal = goal,
235
        expirationDuration = expirationDuration,
236
      )
237
    } yield record
238
  }
239

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

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

334
  override def getIssueCredentialRecordsByStates(
1✔
335
      ignoreWithZeroRetries: Boolean,
336
      limit: Int,
337
      states: IssueCredentialRecord.ProtocolState*
338
  ): URIO[WalletAccessContext, Seq[IssueCredentialRecord]] =
339
    credentialRepository.findByStates(ignoreWithZeroRetries, limit, states*)
1✔
340

341
  override def getIssueCredentialRecordsByStatesForAllWallets(
×
342
      ignoreWithZeroRetries: Boolean,
343
      limit: Int,
344
      states: IssueCredentialRecord.ProtocolState*
345
  ): UIO[Seq[IssueCredentialRecord]] =
346
    credentialRepository.findByStatesForAllWallets(ignoreWithZeroRetries, limit, states*)
×
347

348
  override def receiveCredentialOffer(
1✔
349
      offer: OfferCredential
350
  ): ZIO[WalletAccessContext, InvalidCredentialOffer, IssueCredentialRecord] = {
351
    for {
1✔
352
      attachment <- ZIO
1✔
353
        .fromOption(offer.attachments.headOption)
1✔
354
        .mapError(_ => InvalidCredentialOffer("No attachment found"))
355

356
      format <- ZIO
1✔
357
        .fromOption(attachment.format)
358
        .mapError(_ => InvalidCredentialOffer("No attachment format found"))
359

360
      credentialFormat <- format match
1✔
361
        case value if value == IssueCredentialOfferFormat.JWT.name      => ZIO.succeed(CredentialFormat.JWT)
1✔
362
        case value if value == IssueCredentialOfferFormat.SDJWT.name    => ZIO.succeed(CredentialFormat.SDJWT)
×
363
        case value if value == IssueCredentialOfferFormat.Anoncred.name => ZIO.succeed(CredentialFormat.AnonCreds)
1✔
364
        case value => ZIO.fail(InvalidCredentialOffer(s"Unsupported credential format: $value"))
×
365

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

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

428
  private[this] def validatePrismDID(
1✔
429
      did: String
430
  ): IO[UnsupportedDidFormat, PrismDID] = ZIO
1✔
431
    .fromEither(PrismDID.fromString(did))
1✔
432
    .mapError(_ => UnsupportedDidFormat(did))
433

434
  private[this] def validateClaimsAgainstSchemaIfAny(
1✔
435
      claims: Json,
436
      maybeSchemaId: Option[String]
437
  ): UIO[Unit] = maybeSchemaId match
438
    case Some(schemaId) =>
1✔
439
      CredentialSchema
440
        .validateJWTCredentialSubject(schemaId, claims.noSpaces, uriDereferencer)
1✔
441
        .orDieAsUnmanagedFailure
1✔
442
    case None =>
1✔
443
      ZIO.unit
444

445
  private[this] def getCredentialDefinition(
1✔
446
      guid: UUID
447
  ): UIO[CredentialDefinition] = credentialDefinitionService
448
    .getByGUID(guid)
1✔
449
    .orDieAsUnmanagedFailure
1✔
450

451
  private[this] def getCredentialDefinitionPrivatePart(
1✔
452
      guid: UUID
453
  ): URIO[WalletAccessContext, CredentialDefinitionSecret] = for {
1✔
454
    maybeCredentialDefinitionSecret <- genericSecretStorage
1✔
455
      .get[UUID, CredentialDefinitionSecret](guid)
456
      .orDie
457
    credentialDefinitionSecret <- ZIO
1✔
458
      .fromOption(maybeCredentialDefinitionSecret)
459
      .mapError(_ => CredentialDefinitionPrivatePartNotFound(guid))
460
      .orDieAsUnmanagedFailure
1✔
461
  } yield credentialDefinitionSecret
462

463
  override def acceptCredentialOffer(
1✔
464
      recordId: DidCommID,
465
      maybeSubjectId: Option[String],
466
      keyId: Option[KeyId]
467
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] = {
468
    for {
1✔
469
      record <- getRecordWithState(recordId, ProtocolState.OfferReceived)
1✔
470
      count <- (record.credentialFormat, maybeSubjectId) match
1✔
471
        case (CredentialFormat.JWT | CredentialFormat.SDJWT, Some(subjectId)) =>
1✔
472
          for {
1✔
473
            _ <- validatePrismDID(subjectId)
1✔
474
            count <- credentialRepository
1✔
475
              .updateWithSubjectId(recordId, subjectId, keyId, ProtocolState.RequestPending)
1✔
476
              @@ CustomMetricsAspect.startRecordingTime(
1✔
477
                s"${record.id}_issuance_flow_holder_req_pending_to_generated"
1✔
478
              )
479
          } yield count
480
        case (CredentialFormat.AnonCreds, None) =>
1✔
481
          credentialRepository
1✔
482
            .updateProtocolState(recordId, ProtocolState.OfferReceived, ProtocolState.RequestPending)
1✔
483
            @@ CustomMetricsAspect.startRecordingTime(
1✔
484
              s"${record.id}_issuance_flow_holder_req_pending_to_generated"
1✔
485
            )
486
        case (format, maybeSubjectId) =>
×
487
          ZIO.dieMessage(s"Invalid subjectId input for $format offer acceptance: $maybeSubjectId")
×
488
      record <- credentialRepository.getById(record.id)
1✔
489
    } yield record
490
  }
491

492
  private def createPresentationPayload(
1✔
493
      record: IssueCredentialRecord,
494
      subject: JwtIssuer
495
  ): URIO[WalletAccessContext, PresentationPayload] = {
496
    for {
1✔
497
      maybeOptions <- getOptionsFromOfferCredentialData(record)
1✔
498
    } yield {
499
      W3cPresentationPayload(
1✔
500
        `@context` = Vector("https://www.w3.org/2018/presentations/v1"),
1✔
501
        maybeId = None,
502
        `type` = Vector("VerifiablePresentation"),
1✔
503
        verifiableCredential = IndexedSeq.empty,
1✔
504
        holder = subject.did.value,
1✔
505
        verifier = IndexedSeq.empty ++ maybeOptions.map(_.domain),
1✔
506
        maybeIssuanceDate = None,
507
        maybeExpirationDate = None
508
      ).toJwtPresentationPayload.copy(maybeNonce = maybeOptions.map(_.challenge))
1✔
509
    }
510
  }
511

512
  private def getLongForm(
1✔
513
      did: PrismDID,
514
      allowUnpublishedIssuingDID: Boolean = false
×
515
  ): URIO[WalletAccessContext, LongFormPrismDID] = {
516
    for {
1✔
517
      maybeDidState <- managedDIDService
1✔
518
        .getManagedDIDState(did.asCanonical)
1✔
519
        .orDieWith(e => RuntimeException(s"Error occurred while getting DID from wallet: ${e.toString}"))
×
520
      didState <- ZIO
1✔
521
        .fromOption(maybeDidState)
522
        .mapError(_ => DIDNotFoundInWallet(did))
523
        .orDieAsUnmanagedFailure
1✔
524
      _ <- (didState match
1✔
525
        case s @ ManagedDIDState(_, _, PublicationState.Published(_)) => ZIO.succeed(s)
1✔
526
        case s => ZIO.cond(allowUnpublishedIssuingDID, s, DIDNotPublished(did, s.publicationState))
×
527
      ).orDieAsUnmanagedFailure
1✔
528
      longFormPrismDID = PrismDID.buildLongFormFromOperation(didState.createOperation)
1✔
529
    } yield longFormPrismDID
1✔
530
  }
531

532
  private[this] def getKeyId(
1✔
533
      did: PrismDID,
534
      verificationRelationship: VerificationRelationship,
535
      ellipticCurve: EllipticCurve
536
  ): UIO[String] = {
537
    for {
1✔
538
      maybeDidData <- didService
1✔
539
        .resolveDID(did)
1✔
540
        .orDieWith(e => RuntimeException(s"Error occurred while resolving the DID: ${e.toString}"))
×
541
      didData <- ZIO
1✔
542
        .fromOption(maybeDidData)
543
        .mapError(_ => DIDNotResolved(did))
544
        .orDieAsUnmanagedFailure
1✔
545
      keyId <- ZIO
1✔
546
        .fromOption(
547
          didData._2.publicKeys
1✔
548
            .find(pk => pk.purpose == verificationRelationship && pk.publicKeyData.crv == ellipticCurve)
1✔
549
            .map(_.id)
1✔
550
        )
551
        .mapError(_ => KeyNotFoundInDID(did, verificationRelationship))
552
        .orDieAsUnmanagedFailure
1✔
553
    } yield keyId
554
  }
555

556
  override def getJwtIssuer(
1✔
557
      jwtIssuerDID: PrismDID,
558
      verificationRelationship: VerificationRelationship,
559
      keyId: Option[KeyId] = None
1✔
560
  ): URIO[WalletAccessContext, JwtIssuer] = {
561
    for {
1✔
562
      issuingKeyId <- getKeyId(jwtIssuerDID, verificationRelationship, EllipticCurve.SECP256K1)
1✔
563
      ecKeyPair <- managedDIDService
1✔
564
        .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingKeyId)
1✔
565
        .flatMap {
566
          case Some(keyPair: Secp256k1KeyPair) => ZIO.some(keyPair)
1✔
567
          case _                               => ZIO.none
×
568
        }
569
        .someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingKeyId, "Secp256k1"))
570
        .orDieAsUnmanagedFailure
1✔
571
      Secp256k1KeyPair(publicKey, privateKey) = ecKeyPair
1✔
572
      jwtIssuer = JwtIssuer(
573
        org.hyperledger.identus.pollux.vc.jwt.DID(jwtIssuerDID.toString),
1✔
574
        ES256KSigner(privateKey.toJavaPrivateKey, keyId),
1✔
575
        publicKey.toJavaPublicKey
1✔
576
      )
577
    } yield jwtIssuer
1✔
578
  }
579

580
  private def getEd25519SigningKeyPair(
×
581
      jwtIssuerDID: PrismDID,
582
      verificationRelationship: VerificationRelationship
583
  ): URIO[WalletAccessContext, Ed25519KeyPair] = {
584
    for {
×
585
      issuingKeyId <- getKeyId(jwtIssuerDID, verificationRelationship, EllipticCurve.ED25519)
×
586
      ed25519keyPair <- managedDIDService
×
587
        .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingKeyId)
×
588
        .map(_.collect { case keyPair: Ed25519KeyPair => keyPair })
×
589
        .someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingKeyId, "Ed25519"))
590
        .orDieAsUnmanagedFailure
×
591
    } yield ed25519keyPair
592
  }
593

594
  /** @param jwtIssuerDID
595
    *   This can holder prism did / issuer prism did
596
    * @param verificationRelationship
597
    *   Holder it Authentication and Issuer it is AssertionMethod
598
    * @param keyId
599
    *   Optional KID parameter in case of DID has multiple keys with same purpose
600
    * @return
601
    *   JwtIssuer
602
    * @see
603
    *   org.hyperledger.identus.pollux.vc.jwt.Issuer
604
    */
605
  private def getSDJwtIssuer(
×
606
      jwtIssuerDID: PrismDID,
607
      verificationRelationship: VerificationRelationship,
608
      keyId: Option[KeyId]
609
  ): URIO[WalletAccessContext, JwtIssuer] = {
610
    for {
×
611
      ed25519keyPair <- getEd25519SigningKeyPair(jwtIssuerDID, verificationRelationship)
×
612
    } yield {
613
      JwtIssuer(
614
        org.hyperledger.identus.pollux.vc.jwt.DID(jwtIssuerDID.toString),
×
615
        EdSigner(ed25519keyPair, keyId),
×
616
        Ed25519PublicKey.toJavaEd25519PublicKey(ed25519keyPair.publicKey.getEncoded)
×
617
      )
618
    }
619
  }
620

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

653
  override def generateJWTCredentialRequest(
1✔
654
      recordId: DidCommID
655
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] =
656
    generateCredentialRequest(recordId, getJwtIssuer)
1✔
657

658
  override def generateSDJWTCredentialRequest(
×
659
      recordId: DidCommID
660
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] =
661
    generateCredentialRequest(recordId, getSDJwtIssuer)
×
662

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

699
  private def createAnonCredsRequestCredential(
1✔
700
      offerCredential: OfferCredential
701
  ): URIO[WalletAccessContext, AnoncredCreateCrendentialRequest] = {
702
    for {
1✔
703
      attachmentData <- ZIO
1✔
704
        .fromOption(
705
          offerCredential.attachments
706
            .find(_.format.contains(IssueCredentialOfferFormat.Anoncred.name))
1✔
707
            .map(_.data)
1✔
708
            .flatMap {
1✔
709
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
710
              case _             => None
×
711
            }
712
        )
713
        .orDieWith(_ => RuntimeException(s"No AnonCreds attachment found in the offer"))
×
714
      credentialOffer = anoncreds.AnoncredCredentialOffer(attachmentData)
715
      credDefContent <- uriDereferencer
1✔
716
        .dereference(new URI(credentialOffer.getCredDefId))
1✔
717
        .orDieAsUnmanagedFailure
1✔
718
      credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent)
719
      linkSecret <- linkSecretService.fetchOrCreate()
1✔
720
      createCredentialRequest = AnoncredLib.createCredentialRequest(linkSecret, credentialDefinition, credentialOffer)
1✔
721
    } yield createCredentialRequest
1✔
722
  }
723

724
  override def receiveCredentialRequest(
1✔
725
      request: RequestCredential
726
  ): ZIO[WalletAccessContext, InvalidCredentialRequest | RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] = {
727
    for {
1✔
728
      thid <- ZIO
1✔
729
        .fromOption(request.thid.map(DidCommID(_)))
1✔
730
        .mapError(_ => InvalidCredentialRequest("No 'thid' found"))
731
      record <- getRecordWithThreadIdAndStates(
1✔
732
        thid,
733
        ignoreWithZeroRetries = true,
734
        ProtocolState.InvitationGenerated,
735
        ProtocolState.OfferPending,
736
        ProtocolState.OfferSent
737
      )
738
      _ <- credentialRepository.updateWithJWTRequestCredential(record.id, request, ProtocolState.RequestReceived)
1✔
739
      record <- credentialRepository.getById(record.id)
1✔
740
    } yield record
741
  }
742

743
  override def acceptCredentialRequest(
1✔
744
      recordId: DidCommID
745
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
746
    for {
1✔
747
      record <- getRecordWithState(recordId, ProtocolState.RequestReceived)
1✔
748
      request <- ZIO
1✔
749
        .fromOption(record.requestCredentialData)
750
        .orDieWith(_ => RuntimeException(s"No 'requestCredentialData' found in record: ${recordId.value}"))
×
751
      issue = createDidCommIssueCredential(request)
1✔
752
      count <- credentialRepository
1✔
753
        .updateWithIssueCredential(recordId, issue, ProtocolState.CredentialPending)
1✔
754
        @@ CustomMetricsAspect.startRecordingTime(
1✔
755
          s"${record.id}_issuance_flow_issuer_credential_pending_to_generated"
1✔
756
        )
757
      record <- credentialRepository.getById(record.id)
1✔
758
    } yield record
759
  }
760

761
  override def receiveCredentialIssue(
1✔
762
      issueCredential: IssueCredential
763
  ): ZIO[WalletAccessContext, InvalidCredentialIssue | RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] =
764
    for {
1✔
765
      thid <- ZIO
1✔
766
        .fromOption(issueCredential.thid.map(DidCommID(_)))
1✔
767
        .mapError(_ => InvalidCredentialIssue("No 'thid' found"))
768
      record <- getRecordWithThreadIdAndStates(
1✔
769
        thid,
770
        ignoreWithZeroRetries = true,
771
        ProtocolState.RequestPending,
772
        ProtocolState.RequestSent
773
      )
774
      attachment <- ZIO
1✔
775
        .fromOption(issueCredential.attachments.headOption)
1✔
776
        .mapError(_ => InvalidCredentialIssue("No attachment found"))
777

778
      _ <- {
1✔
779
        val result = attachment match {
780
          case AttachmentDescriptor(
781
                id,
782
                media_type,
783
                Base64(v),
784
                Some(IssueCredentialIssuedFormat.Anoncred.name),
785
                _,
786
                _,
787
                _,
788
                _
789
              ) =>
1✔
790
            for {
1✔
791
              processedCredential <- processAnonCredsCredential(record, java.util.Base64.getUrlDecoder.decode(v))
1✔
792
              attachment = AttachmentDescriptor.buildBase64Attachment(
1✔
793
                id = id,
794
                mediaType = media_type,
795
                format = Some(IssueCredentialIssuedFormat.Anoncred.name),
796
                payload = processedCredential.data.getBytes
1✔
797
              )
798
              processedIssuedCredential = issueCredential.copy(attachments = Seq(attachment))
1✔
799
              result <-
1✔
800
                updateWithCredential(
1✔
801
                  processedIssuedCredential,
802
                  record,
803
                  attachment,
804
                  Some(processedCredential.getSchemaId),
1✔
805
                  Some(processedCredential.getCredDefId)
1✔
806
                )
807
            } yield result
808
          case attachment =>
1✔
809
            updateWithCredential(issueCredential, record, attachment, None, None)
1✔
810
        }
811
        result
812
      }
813
      record <- credentialRepository.getById(record.id)
1✔
814
    } yield record
815

816
  private def updateWithCredential(
1✔
817
      issueCredential: IssueCredential,
818
      record: IssueCredentialRecord,
819
      attachment: AttachmentDescriptor,
820
      schemaId: Option[String],
821
      credDefId: Option[String]
822
  ) = {
823
    credentialRepository
824
      .updateWithIssuedRawCredential(
1✔
825
        record.id,
826
        issueCredential,
827
        attachment.data.asJson.noSpaces,
1✔
828
        schemaId,
829
        credDefId,
830
        ProtocolState.CredentialReceived
831
      )
832
  }
833

834
  private def processAnonCredsCredential(
1✔
835
      record: IssueCredentialRecord,
836
      credentialBytes: Array[Byte]
837
  ): URIO[WalletAccessContext, anoncreds.AnoncredCredential] = {
838
    for {
1✔
839
      credential <- ZIO.succeed(anoncreds.AnoncredCredential(new String(credentialBytes)))
1✔
840
      credDefContent <- uriDereferencer
1✔
841
        .dereference(new URI(credential.getCredDefId))
1✔
842
        .orDieAsUnmanagedFailure
1✔
843
      credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent)
844
      metadata <- ZIO
1✔
845
        .fromOption(record.anonCredsRequestMetadata)
846
        .orDieWith(_ => RuntimeException(s"No AnonCreds request metadata found in record: ${record.id.value}"))
×
847
      linkSecret <- linkSecretService.fetchOrCreate()
1✔
848
      credential <- ZIO
1✔
849
        .attempt(
850
          AnoncredLib.processCredential(
1✔
851
            anoncreds.AnoncredCredential(new String(credentialBytes)),
1✔
852
            metadata,
853
            linkSecret,
854
            credentialDefinition
855
          )
856
        )
857
        .orDieWith(error => RuntimeException(s"AnonCreds credential processing error: ${error.getMessage}"))
×
858
    } yield credential
859
  }
860

861
  override def markOfferSent(
1✔
862
      recordId: DidCommID
863
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
864
    updateCredentialRecordProtocolState(
1✔
865
      recordId,
866
      IssueCredentialRecord.ProtocolState.OfferPending,
867
      IssueCredentialRecord.ProtocolState.OfferSent
868
    )
869

870
  override def markCredentialOfferInvitationExpired(
×
871
      recordId: DidCommID
872
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
873
    updateCredentialRecordProtocolState(
×
874
      recordId,
875
      IssueCredentialRecord.ProtocolState.RequestReceived,
876
      IssueCredentialRecord.ProtocolState.InvitationExpired
877
    )
878
  override def markRequestSent(
1✔
879
      recordId: DidCommID
880
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
881
    updateCredentialRecordProtocolState(
1✔
882
      recordId,
883
      IssueCredentialRecord.ProtocolState.RequestGenerated,
884
      IssueCredentialRecord.ProtocolState.RequestSent
885
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
886
      s"${recordId}_issuance_flow_holder_req_generated_to_sent",
1✔
887
      "issuance_flow_holder_req_generated_to_sent_ms_gauge"
888
    )
889

890
  private def markCredentialGenerated(
1✔
891
      record: IssueCredentialRecord,
892
      issueCredential: IssueCredential
893
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
894
    for {
1✔
895
      count <- credentialRepository
1✔
896
        .updateWithIssueCredential(record.id, issueCredential, IssueCredentialRecord.ProtocolState.CredentialGenerated)
1✔
897
        @@ CustomMetricsAspect.endRecordingTime(
1✔
898
          s"${record.id}_issuance_flow_issuer_credential_pending_to_generated",
1✔
899
          "issuance_flow_issuer_credential_pending_to_generated_ms_gauge"
900
        ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_issuer_credential_generated_to_sent")
1✔
901
      record <- credentialRepository.getById(record.id)
1✔
902
    } yield record
903
  }
904

905
  override def markCredentialSent(
1✔
906
      recordId: DidCommID
907
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
908
    updateCredentialRecordProtocolState(
1✔
909
      recordId,
910
      IssueCredentialRecord.ProtocolState.CredentialGenerated,
911
      IssueCredentialRecord.ProtocolState.CredentialSent
912
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
913
      s"${recordId}_issuance_flow_issuer_credential_generated_to_sent",
1✔
914
      "issuance_flow_issuer_credential_generated_to_sent_ms_gauge"
915
    )
916

917
  override def reportProcessingFailure(
×
918
      recordId: DidCommID,
919
      failReason: Option[Failure]
920
  ): URIO[WalletAccessContext, Unit] =
921
    credentialRepository.updateAfterFail(recordId, failReason)
×
922

923
  private def getRecordWithState(
1✔
924
      recordId: DidCommID,
925
      state: ProtocolState
926
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
927
    for {
1✔
928
      record <- credentialRepository.getById(recordId)
1✔
929
      _ <- record.protocolState match {
1✔
930
        case s if s == state => ZIO.unit
1✔
931
        case s               => ZIO.fail(RecordNotFound(recordId, Some(s)))
1✔
932
      }
933
    } yield record
1✔
934
  }
935

936
  private def getRecordWithThreadIdAndStates(
1✔
937
      thid: DidCommID,
938
      ignoreWithZeroRetries: Boolean,
939
      states: ProtocolState*
940
  ): ZIO[WalletAccessContext, RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] = {
941
    for {
1✔
942
      record <- credentialRepository
1✔
943
        .findByThreadId(thid, ignoreWithZeroRetries)
1✔
944
        .someOrFail(RecordNotFoundForThreadIdAndStates(thid, states*))
945
      _ <- record.protocolState match {
1✔
946
        case s if states.contains(s) => ZIO.unit
1✔
947
        case state                   => ZIO.fail(RecordNotFoundForThreadIdAndStates(thid, states*))
1✔
948
      }
949
    } yield record
1✔
950
  }
951

952
  private def createDidCommOfferCredential(
1✔
953
      pairwiseIssuerDID: DidId,
954
      pairwiseHolderDID: Option[DidId],
955
      maybeSchemaId: Option[String],
956
      claims: Seq[Attribute],
957
      thid: DidCommID,
958
      challenge: String,
959
      domain: String,
960
      offerFormat: IssueCredentialOfferFormat
961
  ): UIO[OfferCredential] = {
962
    for {
1✔
963
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = maybeSchemaId, attributes = claims))
1✔
964
      body = OfferCredential.Body(
1✔
965
        goal_code = Some("Offer Credential"),
966
        credential_preview = credentialPreview,
967
      )
968
      attachments <- ZIO.succeed(
1✔
969
        Seq(
1✔
970
          AttachmentDescriptor.buildJsonAttachment(
1✔
971
            mediaType = Some("application/json"),
972
            format = Some(offerFormat.name),
973
            payload = PresentationAttachment(
974
              Some(Options(challenge, domain)),
975
              PresentationDefinition(format = Some(ClaimFormat(jwt = Some(Jwt(alg = Seq("ES256K"))))))
1✔
976
            )
977
          )
978
        )
979
      )
980
    } yield OfferCredential(
1✔
981
      body = body,
982
      attachments = attachments,
983
      from = pairwiseIssuerDID,
984
      to = pairwiseHolderDID,
985
      thid = Some(thid.value)
1✔
986
    )
987
  }
988

989
  private def createAnonCredsDidCommOfferCredential(
1✔
990
      pairwiseIssuerDID: DidId,
991
      pairwiseHolderDID: Option[DidId],
992
      schemaUri: String,
993
      credentialDefinitionGUID: UUID,
994
      credentialDefinitionId: String,
995
      claims: Seq[Attribute],
996
      thid: DidCommID
997
  ): URIO[WalletAccessContext, OfferCredential] = {
998
    for {
1✔
999
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = Some(schemaUri), attributes = claims))
1✔
1000
      body = OfferCredential.Body(
1✔
1001
        goal_code = Some("Offer Credential"),
1002
        credential_preview = credentialPreview,
1003
      )
1004
      attachments <- createAnonCredsCredentialOffer(credentialDefinitionGUID, credentialDefinitionId).map { offer =>
1✔
1005
        Seq(
1✔
1006
          AttachmentDescriptor.buildBase64Attachment(
1✔
1007
            mediaType = Some("application/json"),
1008
            format = Some(IssueCredentialOfferFormat.Anoncred.name),
1009
            payload = offer.data.getBytes()
1✔
1010
          )
1011
        )
1012
      }
1013
    } yield OfferCredential(
1✔
1014
      body = body,
1015
      attachments = attachments,
1016
      from = pairwiseIssuerDID,
1017
      to = pairwiseHolderDID,
1018
      thid = Some(thid.value)
1✔
1019
    )
1020
  }
1021

1022
  private def createAnonCredsCredentialOffer(
1✔
1023
      credentialDefinitionGUID: UUID,
1024
      credentialDefinitionId: String
1025
  ): URIO[WalletAccessContext, AnoncredCredentialOffer] =
1026
    for {
1✔
1027
      credentialDefinition <- getCredentialDefinition(credentialDefinitionGUID)
1✔
1028
      cd = anoncreds.AnoncredCredentialDefinition(credentialDefinition.definition.toString)
1✔
1029
      kcp = anoncreds.AnoncredCredentialKeyCorrectnessProof(credentialDefinition.keyCorrectnessProof.toString)
1✔
1030
      credentialDefinitionSecret <- getCredentialDefinitionPrivatePart(credentialDefinition.guid)
1✔
1031
      cdp = anoncreds.AnoncredCredentialDefinitionPrivate(credentialDefinitionSecret.json.toString)
1✔
1032
      createCredentialDefinition = AnoncredCreateCredentialDefinition(cd, cdp, kcp)
1033
      offer = AnoncredLib.createOffer(createCredentialDefinition, credentialDefinitionId)
1✔
1034
    } yield offer
1✔
1035

1036
  private[this] def createDidCommRequestCredential(
1✔
1037
      format: IssueCredentialOfferFormat,
1038
      offer: OfferCredential,
1039
      signedPresentation: JWT
1040
  ): RequestCredential = {
1041
    RequestCredential(
1✔
1042
      body = RequestCredential.Body(
1043
        goal_code = offer.body.goal_code,
1044
        comment = offer.body.comment,
1045
      ),
1046
      attachments = Seq(
1✔
1047
        AttachmentDescriptor
1048
          .buildBase64Attachment(
1✔
1049
            mediaType = Some("application/json"),
1050
            format = Some(format.name),
1051
            // FIXME copy payload will probably not work for anoncreds!
1052
            payload = signedPresentation.value.getBytes(),
1✔
1053
          )
1054
      ),
1055
      thid = offer.thid.orElse(Some(offer.id)),
1✔
1056
      from = offer.to.getOrElse(throw new IllegalArgumentException("OfferCredential must have a recipient")),
×
1057
      to = offer.from
1058
    )
1059
  }
1060

1061
  private def createDidCommIssueCredential(request: RequestCredential): IssueCredential = {
1✔
1062
    IssueCredential(
1✔
1063
      body = IssueCredential.Body(
1064
        goal_code = request.body.goal_code,
1065
        comment = request.body.comment,
1066
        replacement_id = None,
1067
        more_available = None,
1068
      ),
1069
      attachments = Seq(), // FIXME !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1✔
1070
      thid = request.thid.orElse(Some(request.id)),
1✔
1071
      from = request.to,
1072
      to = request.from
1073
    )
1074
  }
1075

1076
  /** this is an auxiliary function.
1077
    *
1078
    * @note
1079
    *   Between updating and getting the CredentialRecord back the CredentialRecord can be updated by other operations
1080
    *   in the middle.
1081
    *
1082
    * TODO: this should be improved to behave exactly like atomic operation.
1083
    */
1084
  private def updateCredentialRecordProtocolState(
1✔
1085
      id: DidCommID,
1086
      from: IssueCredentialRecord.ProtocolState,
1087
      to: IssueCredentialRecord.ProtocolState
1088
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] = {
1089
    for {
1✔
1090
      record <- credentialRepository.getById(id)
1✔
1091
      updatedRecord <- record.protocolState match
1✔
1092
        case currentState if currentState == to => ZIO.succeed(record) // Idempotent behaviour
×
1093
        case currentState if currentState == from =>
1✔
1094
          credentialRepository.updateProtocolState(id, from, to) *> credentialRepository.getById(id)
1✔
1095
        case _ => ZIO.fail(InvalidStateForOperation(record.protocolState))
×
1096
    } yield updatedRecord
1097
  }
1098

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

1127
      // Custom for JWT
1128
      issuanceDate = Instant.now()
1✔
1129
      credentialStatus <- allocateNewCredentialInStatusListForWallet(record, statusListRegistryUrl, jwtIssuer)
1✔
1130
      // TODO: get schema when schema registry is available if schema ID is provided
1131
      w3Credential = W3cCredentialPayload(
1✔
1132
        `@context` = Set(
1✔
1133
          "https://www.w3.org/2018/credentials/v1"
1134
        ), // TODO: his information should come from Schema registry by record.schemaId
1135
        maybeId = None,
1136
        `type` =
1137
          Set("VerifiableCredential"), // TODO: This information should come from Schema registry by record.schemaId
1✔
1138
        issuer = Left(jwtIssuer.did.value),
1✔
1139
        issuanceDate = issuanceDate,
1140
        maybeExpirationDate = record.validityPeriod.map(sec => issuanceDate.plusSeconds(sec.toLong)),
×
1141
        maybeCredentialSchema =
1142
          record.schemaUri.map(id => org.hyperledger.identus.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE)),
1✔
1143
        maybeCredentialStatus = Some(credentialStatus),
1144
        credentialSubject = claims.add("id", jwtPresentation.iss.asJson).asJson,
1✔
1145
        maybeRefreshService = None,
1146
        maybeEvidence = None,
1147
        maybeTermsOfUse = None,
1148
        maybeValidFrom = None,
1149
        maybeValidUntil = None
1150
      )
1151
      signedJwtCredential = W3CCredential.toEncodedJwt(w3Credential, jwtIssuer)
1✔
1152
      issueCredential = IssueCredential.build(
1✔
1153
        fromDID = issue.from,
1154
        toDID = issue.to,
1155
        thid = issue.thid,
1156
        credentials = Seq(IssueCredentialIssuedFormat.JWT -> signedJwtCredential.value.getBytes)
1✔
1157
      )
1158
      // End custom
1159

1160
      record <- markCredentialGenerated(record, issueCredential)
1✔
1161
    } yield record
1162
  }
1163

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

1220
      now = Instant.now.getEpochSecond
×
1221
      exp = claims("exp").flatMap(_.asNumber).flatMap(_.toLong)
×
1222
      expInSeconds <- ZIO.fromEither(exp match {
×
1223
        case Some(e) if e > now => Right(e)
×
1224
        case Some(e)            => Left(ExpirationDateHasPassed(e))
×
1225
        case _                  => Right(Instant.now.plus(expirationTime).getEpochSecond)
×
1226
      })
1227
      claimsUpdated = claims
1228
        .add("iss", issuingDID.did.toString.asJson) // This is issuer did
×
1229
        .add("sub", jwtPresentation.iss.asJson) // This is subject did
×
1230
        .add("iat", now.asJson)
×
1231
        .add("exp", expInSeconds.asJson)
×
1232
      credential = {
1233
        jsonWebKey match {
1234
          case Some(jwk) =>
×
1235
            SDJWT.issueCredential(
×
1236
              sdJwtPrivateKey,
1237
              claimsUpdated.asJson.noSpaces,
×
1238
              sdjwt.HolderPublicKey.fromJWT(jwk.toJson)
×
1239
            )
1240
          case None =>
×
1241
            SDJWT.issueCredential(
×
1242
              sdJwtPrivateKey,
1243
              claimsUpdated.asJson.noSpaces,
×
1244
            )
1245
        }
1246
      }
1247
      issueCredential = IssueCredential.build(
×
1248
        fromDID = issue.from,
1249
        toDID = issue.to,
1250
        thid = issue.thid,
1251
        credentials = Seq(IssueCredentialIssuedFormat.SDJWT -> credential.compact.getBytes)
×
1252
      )
1253
      record <- markCredentialGenerated(record, issueCredential)
×
1254
    } yield record
1255

1256
  }
1257

1258
  private def allocateNewCredentialInStatusListForWallet(
1✔
1259
      record: IssueCredentialRecord,
1260
      statusListRegistryUrl: String,
1261
      jwtIssuer: JwtIssuer
1262
  ): URIO[WalletAccessContext, CredentialStatus] = {
1263
    val effect = for {
1✔
1264
      lastStatusList <- credentialStatusListRepository.getLatestOfTheWallet
1✔
1265
      currentStatusList <- lastStatusList
1✔
1266
        .fold(credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl))(
1✔
1267
          ZIO.succeed(_)
1268
        )
1269
      size = currentStatusList.size
1270
      lastUsedIndex = currentStatusList.lastUsedIndex
1271
      statusListToBeUsed <-
1✔
1272
        if lastUsedIndex < size then ZIO.succeed(currentStatusList)
1✔
1273
        else credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl)
×
1274
      _ <- credentialStatusListRepository.allocateSpaceForCredential(
1✔
1275
        issueCredentialRecordId = record.id,
1276
        credentialStatusListId = statusListToBeUsed.id,
1277
        statusListIndex = statusListToBeUsed.lastUsedIndex + 1
1278
      )
1279
    } yield CredentialStatus(
1✔
1280
      id = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}#${statusListToBeUsed.lastUsedIndex + 1}",
1✔
1281
      `type` = "StatusList2021Entry",
1282
      statusPurpose = StatusPurpose.Revocation,
1283
      statusListIndex = lastUsedIndex + 1,
1284
      statusListCredential = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}"
1✔
1285
    )
1286
    issueCredentialSem.withPermit(effect)
1✔
1287
  }
1288

1289
  override def generateAnonCredsCredential(
1✔
1290
      recordId: DidCommID
1291
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
1292
    for {
1✔
1293
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
1✔
1294
      requestCredential <- ZIO
1✔
1295
        .fromOption(record.requestCredentialData)
1296
        .orElse(ZIO.dieMessage(s"No request credential data found in record: ${record.id}"))
×
1297
      body = IssueCredential.Body(goal_code = Some("Issue Credential"))
1✔
1298
      attachments <- createAnonCredsCredential(record).map { credential =>
1✔
1299
        Seq(
1✔
1300
          AttachmentDescriptor.buildBase64Attachment(
1✔
1301
            mediaType = Some("application/json"),
1302
            format = Some(IssueCredentialIssuedFormat.Anoncred.name),
1303
            payload = credential.data.getBytes()
1✔
1304
          )
1305
        )
1306
      }
1307
      issueCredential = IssueCredential(
1✔
1308
        body = body,
1309
        attachments = attachments,
1310
        from = requestCredential.to,
1311
        to = requestCredential.from,
1312
        thid = requestCredential.thid
1313
      )
1314
      record <- markCredentialGenerated(record, issueCredential)
1✔
1315
    } yield record
1316
  }
1317

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

1373
  private def getOptionsFromOfferCredentialData(record: IssueCredentialRecord): UIO[Option[Options]] = {
1✔
1374
    for {
1✔
1375
      offer <- ZIO
1✔
1376
        .fromOption(record.offerCredentialData)
1377
        .orElse(ZIO.dieMessage(s"Offer data not found in record: ${record.id}"))
×
1378
      attachmentDescriptor <- ZIO
1✔
1379
        .fromOption(offer.attachments.headOption)
1✔
1380
        .orElse(ZIO.dieMessage(s"Attachments not found in record: ${record.id}"))
×
1381
      json <- attachmentDescriptor.data match
1✔
1382
        case JsonData(json) => ZIO.succeed(json.asJson)
1✔
1383
        case _              => ZIO.dieMessage(s"Attachment doesn't contain JsonData: ${record.id}")
×
1384
      maybeOptions <- ZIO
1✔
1385
        .fromEither(json.as[PresentationAttachment].map(_.options))
1✔
1386
        .flatMapError(df => ZIO.dieMessage(df.getMessage))
×
1387
    } yield maybeOptions
1388
  }
1389

1390
  private def getJwtFromRequestCredentialData(record: IssueCredentialRecord): UIO[JWT] = {
1✔
1391
    for {
1✔
1392
      request <- ZIO
1✔
1393
        .fromOption(record.requestCredentialData)
1394
        .orElse(ZIO.dieMessage(s"Request data not found in record: ${record.id}"))
×
1395
      attachmentDescriptor <- ZIO
1✔
1396
        .fromOption(request.attachments.headOption)
1✔
1397
        .orElse(ZIO.dieMessage(s"Attachment not found in record: ${record.id}"))
×
1398
      jwt <- attachmentDescriptor.data match
1✔
1399
        case Base64(b64) =>
1✔
1400
          ZIO.succeed {
1401
            val base64Decoded = new String(java.util.Base64.getUrlDecoder.decode(b64))
1✔
1402
            JWT(base64Decoded)
1✔
1403
          }
1404
        case _ => ZIO.dieMessage(s"Attachment does not contain Base64Data: ${record.id}")
×
1405
    } yield jwt
1406
  }
1407

1408
  private def validateRequestCredentialDataProof(
1✔
1409
      maybeOptions: Option[Options],
1410
      jwt: JWT
1411
  ): IO[CredentialRequestValidationFailed, JwtPresentationPayload] = {
1412
    for {
1✔
1413
      _ <- maybeOptions match
1✔
1414
        case None => ZIO.unit
×
1415
        case Some(options) =>
1✔
1416
          JwtPresentation.validatePresentation(jwt, options.domain, options.challenge) match
1✔
1417
            case ZValidation.Success(log, value) => ZIO.unit
1✔
1418
            case ZValidation.Failure(log, error) =>
×
1419
              ZIO.fail(CredentialRequestValidationFailed("domain/challenge proof validation failed"))
×
1420

1421
      clock = java.time.Clock.system(ZoneId.systemDefault)
1✔
1422

1423
      genericUriResolver = GenericUriResolver(
1✔
1424
        Map(
1✔
1425
          "data" -> DataUrlResolver(),
1✔
1426
        )
1427
      )
1428
      verificationResult <- JwtPresentation
1✔
1429
        .verify(
1430
          jwt,
1431
          JwtPresentation.PresentationVerificationOptions(
1✔
1432
            maybeProofPurpose = Some(VerificationRelationship.Authentication),
1433
            verifySignature = true,
1434
            verifyDates = false,
1435
            leeway = Duration.Zero
1436
          )
1437
        )(didResolver, genericUriResolver)(clock)
1✔
1438
        .mapError(errors => CredentialRequestValidationFailed(errors*))
1439

1440
      result <- verificationResult match
1✔
1441
        case ZValidation.Success(log, value) => ZIO.unit
1✔
1442
        case ZValidation.Failure(log, error) =>
×
1443
          ZIO.fail(CredentialRequestValidationFailed(s"JWT presentation verification failed: $error"))
×
1444

1445
      jwtPresentation <- ZIO
1✔
1446
        .fromTry(JwtPresentation.decodeJwt(jwt))
1✔
1447
        .mapError(t => CredentialRequestValidationFailed(s"JWT presentation decoding failed: ${t.getMessage}"))
×
1448
    } yield jwtPresentation
1449
  }
1450

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

1497
  }
1498
}
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