Skip to content

Commit

Permalink
implement allocation logic and repository functions
Browse files Browse the repository at this point in the history
Signed-off-by: Shota Jolbordi <shota.jolbordi@iohk.io>
  • Loading branch information
Shota Jolbordi committed Dec 8, 2023
1 parent 5aa345b commit 478a7cc
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package io.iohk.atala.pollux.core.model
import io.iohk.atala.castor.core.model.did.DID
import io.iohk.atala.castor.core.model.did.CanonicalPrismDID
import io.iohk.atala.pollux.vc.jwt.StatusPurpose
import io.iohk.atala.shared.models.WalletId

Expand All @@ -9,7 +9,7 @@ import java.util.UUID
final case class CredentialStatusList(
id: UUID,
walletId: WalletId,
issuer: DID,
issuer: CanonicalPrismDID,
issued: Instant,
purpose: StatusPurpose,
statusListJwtCredential: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
package io.iohk.atala.pollux.core.repository

import io.iohk.atala.castor.core.model.did.CanonicalPrismDID
import io.iohk.atala.mercury.protocol.issuecredential.{IssueCredential, RequestCredential}
import io.iohk.atala.pollux.anoncreds.CredentialRequestMetadata
import io.iohk.atala.pollux.core.model.*
import io.iohk.atala.pollux.core.model.IssueCredentialRecord.ProtocolState
import io.iohk.atala.pollux.vc.jwt.Issuer
import io.iohk.atala.shared.models.{WalletAccessContext, WalletId}
import zio.*

import java.util.UUID

trait CredentialStatusListRepository {
def getLatestOfTheWallet(walletId: WalletId): RIO[WalletAccessContext, CredentialStatusList]
def getLatestOfTheWallet(walletId: WalletId): RIO[WalletAccessContext, Option[CredentialStatusList]]

def createNewForTheWallet(walletId: WalletId, jwtIssuer: Issuer): RIO[WalletAccessContext, CredentialStatusList]

def allocateSpaceForCredential(
issueCredentialRecordId: DidCommID,
credentialStatusListId: UUID,
statusListIndex: Int
): RIO[WalletAccessContext, Unit]

}
Original file line number Diff line number Diff line change
Expand Up @@ -976,8 +976,6 @@ private class CredentialServiceImpl(
.mapError(_ =>
CredentialServiceError.UnexpectedError(s"Issue credential data not found in record: ${recordId.value}")
)
walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId)
_ <- ZIO.logInfo("wallet id is " + walletId)
longFormPrismDID <- getLongForm(issuingDID, true).mapError(err => UnexpectedError(err.getMessage))
jwtIssuer <- createJwtIssuer(longFormPrismDID, VerificationRelationship.AssertionMethod)
offerCredentialData <- ZIO
Expand Down Expand Up @@ -1005,6 +1003,8 @@ private class CredentialServiceImpl(
payload => ZIO.logInfo("JWT Presentation Validation Successful!")
)
issuanceDate = Instant.now()
walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId)
credentialStatus <- allocateNewCredentialInStatusListForWallet(walletId, record)
// TODO: get schema when schema registry is available if schema ID is provided
w3Credential = W3cCredentialPayload(
`@context` = Set(
Expand All @@ -1018,15 +1018,7 @@ private class CredentialServiceImpl(
maybeExpirationDate = record.validityPeriod.map(sec => issuanceDate.plusSeconds(sec.toLong)),
maybeCredentialSchema =
record.schemaId.map(id => io.iohk.atala.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE)),
maybeCredentialStatus = Some(
CredentialStatus(
id = "id",
`type` = "StatusList2021Entry",
statusPurpose = StatusPurpose.Revocation,
statusListIndex = 0,
statusListCredential = "abc"
)
),
maybeCredentialStatus = Some(credentialStatus),
credentialSubject = claims.add("id", jwtPresentation.iss.asJson).asJson,
maybeRefreshService = None,
maybeEvidence = None,
Expand All @@ -1045,17 +1037,54 @@ private class CredentialServiceImpl(

// what needs to happen:
// get the last status list of this wallet that was created
// if statusList does not even exists, create one
// check its size and last used index
// if last used index is less then size, that is the status list we will use, status list index is last_used_index + 1
// if the last used index is equals to the size create new status list for this wallet, and use that, status list_index is 0
// in both cases, after aquring status list to be used,
//

private[this] def allocateNewCredentialInStatusListForWallet(walletId: WalletId, recordId: DidCommID) = {
private[this] def allocateNewCredentialInStatusListForWallet(
walletId: WalletId,
record: IssueCredentialRecord
): ZIO[WalletAccessContext, CredentialServiceError, CredentialStatus] = {
for {
lastStatusList <- credentialStatusListRepository.getLatestOfTheWallet(walletId)
} yield ()
}
lastStatusList <- credentialStatusListRepository.getLatestOfTheWallet(walletId).mapError(RepositoryError.apply)
issuingDID <- ZIO
.fromOption(record.issuingDID)
.mapError(_ => UnexpectedError(s"Issuing Id not found in record: ${record.id.value}"))
jwtIssuer <- createJwtIssuer(issuingDID, VerificationRelationship.AssertionMethod)
currentStatusList <- lastStatusList
.fold(credentialStatusListRepository.createNewForTheWallet(walletId, jwtIssuer))(
ZIO.succeed(_)
)
.mapError(RepositoryError.apply)
size = currentStatusList.size
lastUsedIndex = currentStatusList.lastUsedIndex
statusListToBeUsed <-
if lastUsedIndex < size then ZIO.succeed(currentStatusList)
else credentialStatusListRepository.createNewForTheWallet(walletId, jwtIssuer).mapError(RepositoryError.apply)

// creates record in credentials_in_status_list table with provided recordId, credentialStatusListId and statusListIndex
// after that updates credentialStatusLists table, sets lastUsedIndex = statusListIndex
// all of this needs to happen in one postgres transaction
_ <- credentialStatusListRepository
.allocateSpaceForCredential(
issueCredentialRecordId = record.id,
credentialStatusListId = statusListToBeUsed.id,
statusListIndex = lastUsedIndex + 1
)
.mapError(RepositoryError.apply)

} yield CredentialStatus(
// TODO: change URL to agent URL
id = s"https://example.com/credentials/status/${statusListToBeUsed.id}#${lastUsedIndex + 1}",
`type` = "StatusList2021Entry",
statusPurpose = StatusPurpose.Revocation,
statusListIndex = lastUsedIndex + 1,
statusListCredential = s"https://example.com/credentials/status/${statusListToBeUsed.id}"
)
}

override def generateAnonCredsCredential(
recordId: DidCommID
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.iohk.atala.pollux.sql.repository

import doobie.util.{Get, Put}
import io.iohk.atala.castor.core.model.did.{CanonicalPrismDID, PrismDID}
import io.iohk.atala.pollux.core.model.*
import io.iohk.atala.pollux.vc.jwt.StatusPurpose
import io.iohk.atala.shared.models.WalletId

given didCommIDGet: Get[DidCommID] = Get[String].map(DidCommID(_))
given didCommIDPut: Put[DidCommID] = Put[String].contramap(_.value)

given walletIdGet: Get[WalletId] = Get[String].map(WalletId.fromUUIDString)
given walletIdPut: Put[WalletId] = Put[String].contramap(_.toString)

given prismDIDGet: Get[CanonicalPrismDID] =
Get[String].map(s => PrismDID.fromString(s).fold(e => throw RuntimeException(e), _.asCanonical))
given prismDIDPut: Put[CanonicalPrismDID] = Put[String].contramap(_.toString)

given statusPurposeGet: Get[StatusPurpose] = Get[String].map {
case "Revocation" => StatusPurpose.Revocation
case "Suspension" => StatusPurpose.Suspension
case purpose => throw RuntimeException(s"Invalid status purpose - $purpose")
}

given statusPurposePut: Put[StatusPurpose] = Put[String].contramap {
case StatusPurpose.Revocation => StatusPurpose.Revocation.str
case StatusPurpose.Suspension => StatusPurpose.Suspension.str
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[

import IssueCredentialRecord.*

given didCommIDGet: Get[DidCommID] = Get[String].map(DidCommID(_))
given didCommIDPut: Put[DidCommID] = Put[String].contramap(_.value)

given credentialFormatGet: Get[CredentialFormat] = Get[String].map(CredentialFormat.valueOf)
given credentialFormatPut: Put[CredentialFormat] = Put[String].contramap(_.toString)

Expand All @@ -57,10 +54,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[

given issueCredentialGet: Get[IssueCredential] = Get[String].map(decode[IssueCredential](_).getOrElse(???))
given issueCredentialPut: Put[IssueCredential] = Put[String].contramap(_.asJson.toString)

given prismDIDGet: Get[CanonicalPrismDID] =
Get[String].map(s => PrismDID.fromString(s).fold(e => throw RuntimeException(e), _.asCanonical))
given prismDIDPut: Put[CanonicalPrismDID] = Put[String].contramap(_.toString)


override def createIssueCredentialRecord(record: IssueCredentialRecord): RIO[WalletAccessContext, Int] = {
val cxnIO = sql"""
Expand Down Expand Up @@ -244,6 +238,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[
): Task[Seq[IssueCredentialRecord]] = {
getRecordsByStates(ignoreWithZeroRetries, limit, states: _*).transact(xb)
}
// TODO remove this comment
override def getIssueCredentialRecord(
recordId: DidCommID
): RIO[WalletAccessContext, Option[IssueCredentialRecord]] = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,120 @@
package io.iohk.atala.pollux.sql.repository

import cats.data.NonEmptyList
import doobie.*
import doobie.free.connection
import doobie.implicits.*
import doobie.postgres.*
import doobie.postgres.implicits.*
import io.circe.*
import io.circe.parser.*
import io.circe.syntax.*
import io.iohk.atala.pollux.vc.jwt.{Issuer, StatusPurpose}
import io.iohk.atala.pollux.vc.jwt.revocation.{BitString, BitStringError, VCStatusList2021}
//import doobie.implicits.legacy.instant.* TODO: might need for Instance Meta
import io.iohk.atala.castor.core.model.did.*
import io.iohk.atala.mercury.protocol.issuecredential.{IssueCredential, OfferCredential, RequestCredential}
import io.iohk.atala.pollux.anoncreds.CredentialRequestMetadata
import io.iohk.atala.pollux.core.model.*
import io.iohk.atala.pollux.core.model.error.CredentialRepositoryError
import io.iohk.atala.pollux.core.model.error.CredentialRepositoryError.*
import io.iohk.atala.pollux.core.repository.{CredentialRepository, CredentialStatusListRepository}
import io.iohk.atala.pollux.core.repository.CredentialStatusListRepository
import io.iohk.atala.shared.db.ContextAwareTask
import io.iohk.atala.shared.db.Implicits.*
import io.iohk.atala.shared.models.WalletAccessContext
import org.postgresql.util.PSQLException
import io.iohk.atala.pollux.vc.jwt.revocation.BitStringError.*
import zio.*
import zio.interop.catz.*
import zio.json.*
import io.iohk.atala.shared.models.{WalletAccessContext, WalletId}

import java.time.Instant
import java.util.UUID

class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: Transactor[Task])
extends CredentialStatusListRepository {
def getLatestOfTheWallet(walletId: WalletId): RIO[WalletAccessContext, CredentialStatusList] = ???
extends CredentialStatusListRepository {
def getLatestOfTheWallet(walletId: WalletId): RIO[WalletAccessContext, Option[CredentialStatusList]] = {

val cxnIO =
sql"""
| SELECT
| id,
| wallet_id,
| issuer,
| issued,
| purpose,
| status_list_jwt_credential,
| created_at,
| updated_at
| from public.credential_status_lists order by created_at DESC limit 1
|"""
.query[CredentialStatusList]
.option

cxnIO
.transactWallet(xa)

}

def createNewForTheWallet(walletId: WalletId, jwtIssuer: Issuer): RIO[WalletAccessContext, CredentialStatusList] = {

val id = UUID.randomUUID()
val issued = Instant.now()
val issuerDid = jwtIssuer.did.value

val encodedJwtCredential = for {
bitString <- BitString.getInstance().mapError {
case InvalidSize(message) => new Throwable(message)
case EncodingError(message) => new Throwable(message)
case DecodingError(message) => new Throwable(message)
case IndexOutOfBounds(message) => new Throwable(message)
}
emptyJwtCredential <- VCStatusList2021
.build(
vcId = s"https://example.com/credentials/status/$id", // TODO: change URL to real one
slId = "",
revocationData = bitString,
jwtIssuer = jwtIssuer
)
.mapError(x => new Throwable(x.msg))

encodedJwtCredential <- emptyJwtCredential.encoded
} yield encodedJwtCredential

for {
jwtCredential <- encodedJwtCredential
query = sql"""
|INSERT INTO public.credential_status_lists (id, wallet_id, issuer, issued, purpose,
| status_list_jwt_credential, size, last_used_index)
|VALUES ($id, ${walletId.toUUID}, $issuerDid, $issued, ${StatusPurpose.Revocation.str}, ${jwtCredential.value},
| ${BitString.MIN_SL2021_SIZE}, 0)
|RETURNING id, wallet_id, issuer, issued, purpose, status_list_jwt_credential, created_at, updated_at
""".stripMargin.query[CredentialStatusList].unique
newStatusList <- query
.transactWallet(xa)
} yield newStatusList

}

def allocateSpaceForCredential(
issueCredentialRecordId: DidCommID,
credentialStatusListId: UUID,
statusListIndex: Int
): RIO[WalletAccessContext, Unit] = {

val statusListEntryCreationQuery =
sql"""
| INSERT INTO public.credentials_in_status_list (id, issue_credential_record_id, credential_status_list_id, status_list_index, is_canceled)
| VALUES (${UUID.randomUUID()}, $issueCredentialRecordId, $credentialStatusListId, $statusListIndex, false)
|""".stripMargin.update.run

val statusListUpdateQuery =
sql"""
| UPDATE public.credential_status_lists
| SET
| last_used_index = $statusListIndex,
| updated_at = ${Instant.now()}
| WHERE
| id = $credentialStatusListId
|""".stripMargin.update.run

val res: ConnectionIO[Unit] = for {
_ <- statusListEntryCreationQuery
_ <- statusListUpdateQuery
} yield ()


res.transactWallet(xa)

}

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,6 @@ class JdbcPresentationRepository(

import PresentationRecord.*

given didCommIDGet: Get[DidCommID] = Get[String].map(DidCommID(_))
given didCommIDPut: Put[DidCommID] = Put[String].contramap(_.value)

given protocolStateGet: Get[ProtocolState] = Get[String].map(ProtocolState.valueOf)
given protocolStatePut: Put[ProtocolState] = Put[String].contramap(_.toString)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ opaque type WalletId = UUID

object WalletId {
def fromUUID(uuid: UUID): WalletId = uuid

def fromUUIDString(uuidStr: String): WalletId = UUID.fromString(uuidStr)
def random: WalletId = fromUUID(UUID.randomUUID())

def default: WalletId = fromUUID(UUID.fromString("00000000-0000-0000-0000-000000000000"))
Expand Down

0 comments on commit 478a7cc

Please sign in to comment.