diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt index 9b2a07809..642e66ab4 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt @@ -328,5 +328,15 @@ interface Pluto { */ fun getCredentialMetadata(linkSecretName: String): Flow + /** + * Revokes an existing credential using the credential ID. + * + * @param credentialId The ID of the credential to be revoked + */ fun revokeCredential(credentialId: String) + + /** + * Provides a flow to listen for revoked credentials. + */ + fun observeRevokedCredentials(): Flow> } diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt index 56357e26c..0e3bf5b17 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt @@ -1026,7 +1026,29 @@ class PlutoImpl(private val connection: DbConnection) : Pluto { } } + /** + * Revokes an existing credential using the credential ID. + * + * @param credentialId The ID of the credential to be revoked + */ override fun revokeCredential(credentialId: String) { getInstance().storableCredentialQueries.revokeCredentialById(credentialId) } + + /** + * Provides a flow to listen for revoked credentials. + */ + override fun observeRevokedCredentials(): Flow> { + return getInstance().storableCredentialQueries.observeRevokedCredential() + .asFlow() + .map { + it.executeAsList().map { credential -> + CredentialRecovery( + restorationId = credential.recoveryId, + credentialData = credential.credentialData, + revoked = true + ) + } + } + } } diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt index 0f3d11e7a..553f1f057 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt @@ -230,7 +230,9 @@ class PrismAgent { if (flowState.subscriptionCount.value <= 0) { state = State.STOPPED } else { - throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError("Agent state only accepts one subscription.") + throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + "Agent state only accepts one subscription." + ) // throw Exception("Agent state only accepts one subscription.") } } @@ -427,7 +429,12 @@ class PrismAgent { verificationMethods.values.forEach { if (it.type.contains("X25519")) { - pluto.storePrivateKeys(keyAgreementKeyPair.privateKey as StorableKey, did, 0, it.id.toString()) + pluto.storePrivateKeys( + keyAgreementKeyPair.privateKey as StorableKey, + did, + 0, + it.id.toString() + ) } else if (it.type.contains("Ed25519")) { pluto.storePrivateKeys( authenticationKeyPair.privateKey as StorableKey, @@ -565,7 +572,10 @@ class PrismAgent { * @throws [PolluxError.InvalidPrismDID] if there is a problem creating the request credential. * @throws [io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError] if credential type is not supported **/ - @Throws(PolluxError.InvalidPrismDID::class, io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError::class) + @Throws( + PolluxError.InvalidPrismDID::class, + io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError::class + ) suspend fun prepareRequestCredentialWithIssuer( did: DID, offer: OfferCredential @@ -655,7 +665,9 @@ class PrismAgent { else -> { // TODO: Create new prism agent error message - throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError("Not supported credential type: $type") + throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + "Not supported credential type: $type" + ) // throw Error("Not supported credential type: $type") } } @@ -686,7 +698,9 @@ class PrismAgent { val metadata = if (credentialType == CredentialType.ANONCREDS_ISSUE) { val plutoMetadata = pluto.getCredentialMetadata(message.thid).first() - ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError("Invalid credential metadata") + ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + "Invalid credential metadata" + ) CredentialRequestMetadata( plutoMetadata.json ) @@ -709,8 +723,13 @@ class PrismAgent { ) pluto.storeCredential(storableCredential) return credential - } ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError("Thid should not be null") - } ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError("Cannot find attachment base64 in message") + } + ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + "Thid should not be null" + ) + } ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + "Cannot find attachment base64 in message" + ) } // Message Events @@ -811,7 +830,10 @@ class PrismAgent { prismOnboarding.from = did return prismOnboarding } catch (e: Exception) { - throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError(e.message, e.cause) + throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + e.message, + e.cause + ) } } @@ -913,7 +935,10 @@ class PrismAgent { val privateKeyKeyPath = pluto.getPrismDIDKeyPathIndex(subjectDID).first() val keyPair = - Secp256k1KeyPair.generateKeyPair(seed, KeyCurve(Curve.SECP256K1, privateKeyKeyPath)) + Secp256k1KeyPair.generateKeyPair( + seed, + KeyCurve(Curve.SECP256K1, privateKeyKeyPath) + ) val requestData = request.attachments.mapNotNull { when (it.data) { is AttachmentJsonData -> it.data.data @@ -936,7 +961,11 @@ class PrismAgent { throw PrismAgentError.InvalidCredentialFormatError(CredentialType.ANONCREDS_PROOF_REQUEST) } val linkSecret = getLinkSecret() - val presentation = pollux.createVerifiablePresentationAnoncred(request, credential as AnonCredential, linkSecret) + val presentation = pollux.createVerifiablePresentationAnoncred( + request, + credential as AnonCredential, + linkSecret + ) presentationString = presentation.getJson() } @@ -959,6 +988,15 @@ class PrismAgent { ) } + fun observeRevokedCredentials(): Flow> { + return pluto.observeRevokedCredentials() + .map { list -> + list.map { + pollux.restoreCredential(it.restorationId, it.credentialData, it.revoked) + } + } + } + /** * This method retrieves the link secret from Pluto. * diff --git a/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq b/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq index fe8e00766..d3f5db0f2 100644 --- a/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq +++ b/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq @@ -27,4 +27,9 @@ GROUP BY StorableCredential.id; revokeCredentialById: UPDATE StorableCredential SET revoked = 1 -WHERE id = :id; \ No newline at end of file +WHERE id = :id; + +observeRevokedCredential: +SELECT * +FROM StorableCredential +WHERE revoked = 1; \ No newline at end of file diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt index 3d97152ba..ca1a8a0e0 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt @@ -184,4 +184,8 @@ class PlutoMock : Pluto { override fun revokeCredential(credentialId: String) { TODO("Not yet implemented") } + + override fun observeRevokedCredentials(): Flow> { + TODO("Not yet implemented") + } } diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt index d1ea3704a..af68ac02a 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt @@ -242,4 +242,8 @@ class PlutoMock : Pluto { override fun revokeCredential(credentialId: String) { TODO("Not yet implemented") } + + override fun observeRevokedCredentials(): Flow> { + TODO("Not yet implemented") + } } diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt index 43768c8ec..99dca5cc5 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt @@ -357,7 +357,7 @@ class PrismAgentTests { @Test fun testStartPrismAgent_whenCalled_thenStatusIsRunning() = runTest { - val getLinkSecretReturn = flow { "linkSecret" } + val getLinkSecretReturn = flow { emit("linkSecret") } plutoMock.getLinkSecretReturn = getLinkSecretReturn val agent = PrismAgent( apollo = apolloMock, diff --git a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsViewModel.kt b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsViewModel.kt index e15f9b430..ff3890219 100644 --- a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsViewModel.kt +++ b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsViewModel.kt @@ -14,7 +14,7 @@ class CredentialsViewModel(application: Application) : AndroidViewModel(applicat private var credentials: MutableLiveData> = MutableLiveData() - init { + fun credentialsStream(): LiveData> { viewModelScope.launch { Sdk.getInstance().agent.let { it.getAllCredentials().collect { list -> @@ -22,9 +22,6 @@ class CredentialsViewModel(application: Application) : AndroidViewModel(applicat } } } - } - - fun credentialsStream(): LiveData> { return credentials } } diff --git a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesFragment.kt b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesFragment.kt index 8b656d557..4f4867df4 100644 --- a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesFragment.kt +++ b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesFragment.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import io.iohk.atala.prism.sampleapp.R @@ -52,7 +53,8 @@ class MessagesFragment : Fragment() { // Set up the spinner with the options context?.let { - val adapter = CustomArrayAdapter(it, android.R.layout.simple_spinner_dropdown_item, credentials) + val adapter = + CustomArrayAdapter(it, android.R.layout.simple_spinner_dropdown_item, credentials) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) dialogBinding.spinner.adapter = adapter @@ -81,6 +83,16 @@ class MessagesFragment : Fragment() { viewModel.preparePresentationProof(credential, message) } } + viewModel.revokedCredentialsStream() + .observe(this.viewLifecycleOwner) { revokedCredentials -> + if (revokedCredentials.isNotEmpty()) { + Toast.makeText( + context, + "Credential revoked ID: ${revokedCredentials.last().id}", + Toast.LENGTH_LONG + ).show() + } + } } companion object { diff --git a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesViewModel.kt b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesViewModel.kt index 3661373ba..652e67970 100644 --- a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesViewModel.kt +++ b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesViewModel.kt @@ -32,6 +32,8 @@ class MessagesViewModel(application: Application) : AndroidViewModel(application private val issuedCredentials: ArrayList = arrayListOf() private val processedOffers: ArrayList = arrayListOf() private val db: AppDatabase = DatabaseClient.getInstance() + private val revokedCredentialsNotified: MutableList = mutableListOf() + private var revokedCredentials: MutableLiveData> = MutableLiveData() init { viewModelScope.launch(Dispatchers.IO) { @@ -103,6 +105,27 @@ class MessagesViewModel(application: Application) : AndroidViewModel(application } } + fun revokedCredentialsStream(): LiveData> { + viewModelScope.launch { + Sdk.getInstance().agent.let { + it.observeRevokedCredentials().collect { list -> + val newRevokedCredentials = list.filter { newCredential -> + revokedCredentialsNotified.none { notifiedCredential -> + notifiedCredential.id == newCredential.id + } + } + if (newRevokedCredentials.isNotEmpty()) { + revokedCredentialsNotified.addAll(newRevokedCredentials) + revokedCredentials.postValue(newRevokedCredentials) + } else { + revokedCredentials.postValue(emptyList()) + } + } + } + } + return revokedCredentials + } + private suspend fun processMessages(messages: List) { val sdk = Sdk.getInstance() val messageIds: List = messages.map { it.id }