From 0f635f34c94e3446c8beb4e38d572a9b0dd36d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rib=C3=B3?= Date: Fri, 17 Feb 2023 09:48:03 +0100 Subject: [PATCH] feat(Agent): Implement Credential Issue Protocol in PrismAgent (#27) * fix(DID) Adding JsExport and JsName for secondary constructor. * Making Message JsExportable and add some default values for Message and MessageAttachment. * Add apollo UUID and IssueProtocol data clases in preparation for the Issuance protocol itself. * Add missing error to AgentErrors * Add mocked ConnectionsManager and create interface. * Fix existing code + add interface DIDCommConnection * Create Issue Credential Protocol * Adding OfferCredentialTest and adjust default values on models. * Update prism-agent/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/issueCredential/IssueCredentialTest.kt --------- Co-authored-by: Cristian G <113917899+cristianIOHK@users.noreply.github.com> --- domain/build.gradle.kts | 1 + .../models/DID.kt | 2 +- .../models/Errors.kt | 1 + .../models/Message.kt | 25 +-- .../models/MessageAttachment.kt | 3 +- prism-agent/build.gradle.kts | 1 + .../connectionsManager/ConnectionsManager.kt | 8 + .../ConnectionsManagerImpl.kt | 13 ++ .../connectionsManager/DIDCommConnection.kt | 9 + .../helpers/AttachmentDescriptorBuild.kt | 19 ++ .../issueCredential/IssueCredential.kt | 177 ++++++++++++++++++ .../IssueCredentialProtocol.kt | 153 +++++++++++++++ .../issueCredential/OfferCredential.kt | 171 +++++++++++++++++ .../issueCredential/ProposeCredential.kt | 148 +++++++++++++++ .../issueCredential/RequestCredential.kt | 157 ++++++++++++++++ .../helpers/DID.kt | 20 ++ .../issueCredential/IssueCredentialTest.kt | 80 ++++++++ .../issueCredential/OfferCredentialTest.kt | 57 ++++++ .../issueCredential/ProposeCredentialTest.kt | 57 ++++++ .../issueCredential/RequestCredentialTest.kt | 50 +++++ 20 files changed, 1139 insertions(+), 13 deletions(-) create mode 100644 prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/ConnectionsManager.kt create mode 100644 prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/ConnectionsManagerImpl.kt create mode 100644 prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/DIDCommConnection.kt create mode 100644 prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/helpers/AttachmentDescriptorBuild.kt create mode 100644 prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredential.kt create mode 100644 prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredentialProtocol.kt create mode 100644 prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/OfferCredential.kt create mode 100644 prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/ProposeCredential.kt create mode 100644 prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/RequestCredential.kt create mode 100644 prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/helpers/DID.kt create mode 100644 prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredentialTest.kt create mode 100644 prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/OfferCredentialTest.kt create mode 100644 prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/ProposeCredentialTest.kt create mode 100644 prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/RequestCredentialTest.kt diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index e3e8dfb2e..03a8f5fae 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -70,6 +70,7 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("io.iohk.atala.prism:uuid:1.0.0-alpha") implementation("io.ktor:ktor-client-core:2.1.3") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") } } val commonTest by getting { diff --git a/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/DID.kt b/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/DID.kt index de8feb73f..e517aecfb 100644 --- a/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/DID.kt +++ b/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/DID.kt @@ -10,7 +10,7 @@ import kotlin.jvm.JvmStatic @Serializable @JsExport data class DID( - val schema: String, + val schema: String = "did", val method: String, val methodId: String, ) { diff --git a/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/Errors.kt b/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/Errors.kt index bb4e47b36..a2872f984 100644 --- a/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/Errors.kt +++ b/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/Errors.kt @@ -75,4 +75,5 @@ sealed class PrismAgentError(message: String? = null) : Throwable(message) { class invalidMessageError(message: String? = null) : PrismAgentError(message) class noMediatorAvailableError(message: String? = null) : PrismAgentError(message) class mediationRequestFailedError(message: String? = null) : PrismAgentError(message) + class invalidStepError(message: String? = null) : PrismAgentError(message) } diff --git a/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/Message.kt b/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/Message.kt index 3163359ac..c66f0ee54 100644 --- a/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/Message.kt +++ b/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/Message.kt @@ -1,27 +1,30 @@ package io.iohk.atala.prism.walletsdk.domain.models +import io.iohk.atala.prism.apollo.uuid.UUID +import kotlinx.datetime.Clock import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.js.JsExport +import kotlin.time.Duration.Companion.days @Serializable @JsExport -data class Message( - val id: String, +data class Message constructor( + val id: String = UUID.randomUUID4().toString(), val piuri: String, - val from: DID?, - val to: DID?, - val fromPrior: String?, + val from: DID? = null, + val to: DID? = null, + val fromPrior: String? = null, val body: String, - val extraHeaders: Array, - val createdTime: String, - val expiresTimePlus: String, - val attachments: Array, + val extraHeaders: Array = arrayOf(), + val createdTime: String = Clock.System.now().toString(), + val expiresTimePlus: String = Clock.System.now().plus(1.days).toString(), + val attachments: Array = arrayOf(), val thid: String? = null, val pthid: String? = null, - val ack: Array, - val direction: Direction, + val ack: Array? = emptyArray(), + val direction: Direction = Direction.RECEIVED, ) { override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/MessageAttachment.kt b/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/MessageAttachment.kt index 5dbaab916..58500e113 100644 --- a/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/MessageAttachment.kt +++ b/domain/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.domain/models/MessageAttachment.kt @@ -70,12 +70,13 @@ data class AttachmentDescriptor( val id: String, val mediaType: String? = null, val data: AttachmentData, - val filename: Array, + val filename: Array? = null, val format: String? = null, val lastModTime: String? = null, // Date format val byteCount: Int? = null, val description: String? = null, ) : AttachmentData { + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false diff --git a/prism-agent/build.gradle.kts b/prism-agent/build.gradle.kts index dc26e44d3..6db70d809 100644 --- a/prism-agent/build.gradle.kts +++ b/prism-agent/build.gradle.kts @@ -68,6 +68,7 @@ kotlin { dependencies { implementation("io.iohk.atala.prism:uuid:1.0.0-alpha") implementation(project(":domain")) + implementation("io.iohk.atala.prism:apollo:1.0.0-alpha") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") diff --git a/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/ConnectionsManager.kt b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/ConnectionsManager.kt new file mode 100644 index 000000000..48bfd70e3 --- /dev/null +++ b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/ConnectionsManager.kt @@ -0,0 +1,8 @@ +package io.iohk.atala.prism.walletsdk.prismagent.connectionsManager + +import io.iohk.atala.prism.walletsdk.domain.models.DIDPair + +interface ConnectionsManager { + suspend fun addConnection(paired: DIDPair) + suspend fun removeConnection(pair: DIDPair): DIDPair? +} diff --git a/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/ConnectionsManagerImpl.kt b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/ConnectionsManagerImpl.kt new file mode 100644 index 000000000..73c56b968 --- /dev/null +++ b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/ConnectionsManagerImpl.kt @@ -0,0 +1,13 @@ +package io.iohk.atala.prism.walletsdk.prismagent.connectionsManager + +import io.iohk.atala.prism.walletsdk.domain.models.DIDPair + +class ConnectionsManagerImpl : ConnectionsManager { + override suspend fun addConnection(paired: DIDPair) { + TODO("Not yet implemented") + } + + override suspend fun removeConnection(pair: DIDPair): DIDPair? { + TODO("Not yet implemented") + } +} diff --git a/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/DIDCommConnection.kt b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/DIDCommConnection.kt new file mode 100644 index 000000000..979a64a19 --- /dev/null +++ b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/connectionsManager/DIDCommConnection.kt @@ -0,0 +1,9 @@ +package io.iohk.atala.prism.walletsdk.prismagent.connectionsManager + +import io.iohk.atala.prism.walletsdk.domain.models.Message + +interface DIDCommConnection { + suspend fun awaitMessages(): Array + suspend fun awaitMessageResponse(id: String): Message? + suspend fun sendMessage(message: Message): Message? +} diff --git a/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/helpers/AttachmentDescriptorBuild.kt b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/helpers/AttachmentDescriptorBuild.kt new file mode 100644 index 000000000..07b2bda63 --- /dev/null +++ b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/helpers/AttachmentDescriptorBuild.kt @@ -0,0 +1,19 @@ +package io.iohk.atala.prism.walletsdk.prismagent.helpers + +import io.iohk.atala.prism.apollo.base64.base64UrlEncoded +import io.iohk.atala.prism.apollo.uuid.UUID +import io.iohk.atala.prism.walletsdk.domain.models.AttachmentBase64 +import io.iohk.atala.prism.walletsdk.domain.models.AttachmentDescriptor +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +inline fun AttachmentDescriptor.Companion.build( + id: String = UUID.randomUUID4().toString(), + payload: T, + mediaType: String? = "application/json" +): AttachmentDescriptor { + val encoded = Json.encodeToString(payload).base64UrlEncoded + val attachment = AttachmentBase64(base64 = encoded) + return AttachmentDescriptor(id, mediaType, attachment) +} diff --git a/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredential.kt b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredential.kt new file mode 100644 index 000000000..e1909c464 --- /dev/null +++ b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredential.kt @@ -0,0 +1,177 @@ +package io.iohk.atala.prism.walletsdk.prismagent.protocols.issueCredential + +import io.iohk.atala.prism.apollo.base64.base64UrlEncoded +import io.iohk.atala.prism.apollo.uuid.UUID +import io.iohk.atala.prism.walletsdk.domain.models.AttachmentBase64 +import io.iohk.atala.prism.walletsdk.domain.models.AttachmentDescriptor +import io.iohk.atala.prism.walletsdk.domain.models.DID +import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.domain.models.PrismAgentError +import io.iohk.atala.prism.walletsdk.prismagent.helpers.build +import io.iohk.atala.prism.walletsdk.prismagent.protocols.ProtocolType +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.js.JsExport + +@Serializable +@JsExport +data class IssueCredential( + val id: String? = UUID.randomUUID4().toString(), + val body: Body, + val attachments: Array, + val thid: String?, + val from: DID, + val to: DID, +) { + val type: String = ProtocolType.DidcommIssueCredential.value + + fun makeMessage(): Message { + return Message( + id = id ?: UUID.randomUUID4().toString(), + piuri = type, + from = from, + to = to, + body = Json.encodeToString(body), + attachments = attachments, + thid = thid, + ) + } + + fun getCredentialStrings(): Array { + return attachments.mapNotNull { + when (it.data) { + is AttachmentBase64 -> { + (it.data as AttachmentBase64).base64.base64UrlEncoded + } + else -> null + } + }.toTypedArray() + } + + companion object { + fun fromMessage(fromMessage: Message): IssueCredential { + require( + fromMessage.piuri == ProtocolType.DidcommIssueCredential.value && + fromMessage.from != null && + fromMessage.to != null, + ) { + throw PrismAgentError.invalidIssueCredentialMessageError() + } + + val fromDID = fromMessage.from!! + val toDID = fromMessage.to!! + val body = Json.decodeFromString(fromMessage.body) + + return IssueCredential( + id = fromMessage.id, + body = body, + attachments = fromMessage.attachments, + thid = fromMessage.thid, + from = fromDID, + to = toDID, + ) + } + + fun makeIssueFromRequestCedential(msg: Message): IssueCredential { + val request = RequestCredential.fromMessage(msg) + return IssueCredential( + body = Body( + goalCode = request.body.goalCode, + comment = request.body.comment, + formats = request.body.formats, + ), + attachments = request.attachments, + thid = msg.id, + from = request.to, + to = request.from, + ) + } + } + + @Serializable + data class Body( + val goalCode: String? = null, + val comment: String? = null, + val replacementId: String? = null, + val moreAvailable: String? = null, + val formats: Array, + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as Body + + if (goalCode != other.goalCode) return false + if (comment != other.comment) return false + if (replacementId != other.replacementId) return false + if (moreAvailable != other.moreAvailable) return false + if (!formats.contentEquals(other.formats)) return false + + return true + } + + override fun hashCode(): Int { + var result = goalCode?.hashCode() ?: 0 + result = 31 * result + (comment?.hashCode() ?: 0) + result = 31 * result + (replacementId?.hashCode() ?: 0) + result = 31 * result + (moreAvailable?.hashCode() ?: 0) + result = 31 * result + formats.contentHashCode() + return result + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as IssueCredential + + if (id != other.id) return false + if (body != other.body) return false + if (!attachments.contentEquals(other.attachments)) return false + if (thid != other.thid) return false + if (from != other.from) return false + if (to != other.to) return false + if (type != other.type) return false + + return true + } + + override fun hashCode(): Int { + var result = id?.hashCode() ?: 0 + result = 31 * result + body.hashCode() + result = 31 * result + attachments.contentHashCode() + result = 31 * result + (thid?.hashCode() ?: 0) + result = 31 * result + from.hashCode() + result = 31 * result + to.hashCode() + result = 31 * result + type.hashCode() + return result + } +} + +inline fun IssueCredential.Companion.build( + fromDID: DID, + toDID: DID, + thid: String?, + credentials: Map = mapOf(), +): IssueCredential { + val aux = credentials.map { (key, value) -> + val attachment = AttachmentDescriptor.build( + payload = value, + ) + val format = CredentialFormat(attachId = attachment.id, format = key) + format to attachment + } + return IssueCredential( + body = IssueCredential.Body( + formats = aux.map { it.first }.toTypedArray(), + ), + attachments = aux.map { it.second }.toTypedArray(), + thid = thid, + from = fromDID, + to = toDID, + ) +} diff --git a/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredentialProtocol.kt b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredentialProtocol.kt new file mode 100644 index 000000000..d819924a5 --- /dev/null +++ b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredentialProtocol.kt @@ -0,0 +1,153 @@ +package io.iohk.atala.prism.walletsdk.prismagent.protocols.issueCredential + +import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.domain.models.PrismAgentError +import io.iohk.atala.prism.walletsdk.prismagent.connectionsManager.DIDCommConnection +import kotlinx.serialization.Serializable + +@Serializable +class IssueCredentialProtocol { + + enum class Stage { + PROPOSE, + OFFER, + REQUEST, + COMPLETED, + REFUSED + } + + var stage: Stage + var propose: ProposeCredential? = null + var offer: OfferCredential? = null + var request: RequestCredential? = null + val connector: DIDCommConnection + + constructor( + stage: Stage, + proposeMessage: Message? = null, + offerMessage: Message? = null, + requestMessage: Message? = null, + connector: DIDCommConnection + ) { + this.stage = stage + this.connector = connector + this.propose = proposeMessage?.let { + try { + ProposeCredential.fromMessage(it) + } catch (e: Throwable) { + null + } + } + this.offer = offerMessage?.let { + try { + OfferCredential.fromMessage(it) + } catch (e: Throwable) { + null + } + } + this.request = requestMessage?.let { + try { + RequestCredential.fromMessage(it) + } catch (e: Throwable) { + null + } + } + } + + constructor(message: Message, connector: DIDCommConnection) { + this.connector = connector + val proposed = try { + ProposeCredential.fromMessage(message) + } catch (e: Throwable) { + null + } + val offered = try { + OfferCredential.fromMessage(message) + } catch (e: Throwable) { + null + } + val requested = try { + RequestCredential.fromMessage(message) + } catch (e: Throwable) { + null + } + + when { + proposed != null -> { + this.stage = Stage.PROPOSE + this.propose = proposed + } + offered != null -> { + this.stage = Stage.OFFER + this.offer = offered + } + requested != null -> { + this.stage = Stage.REQUEST + this.request = requested + } + else -> throw PrismAgentError.invalidStepError() + } + } + + suspend fun nextStage() { + if (this.stage == Stage.PROPOSE) { + if (propose == null) { + stage = Stage.REFUSED + return + } + } else if (this.stage == Stage.OFFER) { + if (offer == null) { + stage = Stage.REFUSED + return + } + } + + val messageId: String = when (this.stage) { + Stage.PROPOSE -> { + val message = OfferCredential.makeOfferFromProposedCredential(proposed = propose!!) + connector.sendMessage(message.makeMessage()) + message.id + } + Stage.OFFER -> { + val message = RequestCredential.makeRequestFromOfferCredential(offer = offer!!).makeMessage() + connector.sendMessage(message) + message.id + } + Stage.REQUEST -> null + Stage.COMPLETED -> null + Stage.REFUSED -> null + } ?: return + + val response = connector.awaitMessageResponse(id = messageId) ?: return + + val issued = try { + IssueCredential.fromMessage(response) + } catch (e: Throwable) { + null + } + val offered = try { + OfferCredential.fromMessage(response) + } catch (e: Throwable) { + null + } + val requested = try { + RequestCredential.fromMessage(response) + } catch (e: Throwable) { + null + } + + when { + offered != null -> { + this.stage = Stage.OFFER + this.offer = offered + } + issued != null -> { + this.stage = Stage.COMPLETED + } + requested != null -> { + this.stage = Stage.REQUEST + this.request = requested + } + } + } +} diff --git a/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/OfferCredential.kt b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/OfferCredential.kt new file mode 100644 index 000000000..e7748a619 --- /dev/null +++ b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/OfferCredential.kt @@ -0,0 +1,171 @@ +package io.iohk.atala.prism.walletsdk.prismagent.protocols.issueCredential + +import io.iohk.atala.prism.apollo.uuid.UUID +import io.iohk.atala.prism.walletsdk.domain.models.AttachmentDescriptor +import io.iohk.atala.prism.walletsdk.domain.models.DID +import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.domain.models.PrismAgentError +import io.iohk.atala.prism.walletsdk.prismagent.helpers.build +import io.iohk.atala.prism.walletsdk.prismagent.protocols.ProtocolType +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.js.JsExport + +@Serializable +@JsExport +data class OfferCredential( + val id: String? = UUID.randomUUID4().toString(), + val body: Body, + val attachments: Array, + val thid: String?, + val from: DID, + val to: DID +) { + public val type: String = ProtocolType.DidcommOfferCredential.value + + fun makeMessage(): Message { + return Message( + id = id ?: UUID.randomUUID4().toString(), + piuri = type, + from = from, + to = to, + body = Json.encodeToString(body), + attachments = attachments, + thid = thid + ) + } + + companion object { + + fun makeOfferFromProposedCredential(proposed: ProposeCredential): OfferCredential { + return OfferCredential( + body = Body( + goalCode = proposed.body.goalCode, + comment = proposed.body.comment, + credentialPreview = proposed.body.credentialPreview, + formats = proposed.body.formats + ), + attachments = proposed.attachments, + thid = proposed.thid, + from = proposed.from, + to = proposed.to + ) + } + + @Throws(PrismAgentError.invalidOfferCredentialMessageError::class) + fun fromMessage(fromMessage: Message): OfferCredential { + require( + fromMessage.piuri == ProtocolType.DidcommOfferCredential.value && + fromMessage.from != null && + fromMessage.to != null + ) { + throw PrismAgentError.invalidOfferCredentialMessageError() + } + + val fromDID = fromMessage.from!! + val toDID = fromMessage.to!! + val body = Json.decodeFromString(fromMessage.body) + + return OfferCredential( + id = fromMessage.id, + body = body, + attachments = fromMessage.attachments, + thid = fromMessage.thid, + from = fromDID, + to = toDID + ) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as OfferCredential + + if (id != other.id) return false + if (body != other.body) return false + if (!attachments.contentEquals(other.attachments)) return false + if (thid != other.thid) return false + if (from != other.from) return false + if (to != other.to) return false + if (type != other.type) return false + + return true + } + + override fun hashCode(): Int { + var result = id?.hashCode() ?: 0 + result = 31 * result + body.hashCode() + result = 31 * result + attachments.contentHashCode() + result = 31 * result + (thid?.hashCode() ?: 0) + result = 31 * result + from.hashCode() + result = 31 * result + to.hashCode() + result = 31 * result + type.hashCode() + return result + } + + @Serializable + data class Body( + val goalCode: String? = null, + val comment: String? = null, + val replacementId: String? = null, + val multipleAvailable: String? = null, + val credentialPreview: CredentialPreview, + val formats: Array + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as Body + + if (goalCode != other.goalCode) return false + if (comment != other.comment) return false + if (replacementId != other.replacementId) return false + if (multipleAvailable != other.multipleAvailable) return false + if (credentialPreview != other.credentialPreview) return false + if (!formats.contentEquals(other.formats)) return false + + return true + } + + override fun hashCode(): Int { + var result = goalCode?.hashCode() ?: 0 + result = 31 * result + (comment?.hashCode() ?: 0) + result = 31 * result + (replacementId?.hashCode() ?: 0) + result = 31 * result + (multipleAvailable?.hashCode() ?: 0) + result = 31 * result + credentialPreview.hashCode() + result = 31 * result + formats.contentHashCode() + return result + } + } +} + +inline fun OfferCredential.Companion.build( + fromDID: DID, + toDID: DID, + thid: String?, + credentialPreview: CredentialPreview, + credentials: Map = mapOf() +): OfferCredential { + val aux = credentials.map { (key, value) -> + val attachment = AttachmentDescriptor.build( + payload = value + ) + val format = CredentialFormat(attachId = attachment.id, format = key) + format to attachment + } + return OfferCredential( + body = OfferCredential.Body( + credentialPreview = credentialPreview, + formats = aux.map { it.first }.toTypedArray() + ), + attachments = aux.map { it.second }.toTypedArray(), + thid = thid, + from = fromDID, + to = toDID + ) +} diff --git a/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/ProposeCredential.kt b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/ProposeCredential.kt new file mode 100644 index 000000000..274a5c438 --- /dev/null +++ b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/ProposeCredential.kt @@ -0,0 +1,148 @@ +package io.iohk.atala.prism.walletsdk.prismagent.protocols.issueCredential + +import io.iohk.atala.prism.apollo.uuid.UUID +import io.iohk.atala.prism.walletsdk.domain.models.AttachmentDescriptor +import io.iohk.atala.prism.walletsdk.domain.models.DID +import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.domain.models.PrismAgentError +import io.iohk.atala.prism.walletsdk.prismagent.helpers.build +import io.iohk.atala.prism.walletsdk.prismagent.protocols.ProtocolType +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.js.JsExport + +@Serializable +@JsExport +data class ProposeCredential( + val id: String? = UUID.randomUUID4().toString(), + val body: Body, + val attachments: Array, + val thid: String?, + val from: DID, + val to: DID +) { + public val type: String = ProtocolType.DidcommProposeCredential.value + + fun makeMessage(): Message { + return Message( + id = id ?: UUID.randomUUID4().toString(), + piuri = type, + from = from, + to = to, + body = Json.encodeToString(body), + attachments = attachments, + thid = thid + ) + } + + companion object { + fun fromMessage(fromMessage: Message): ProposeCredential { + require( + fromMessage.piuri == ProtocolType.DidcommProposeCredential.value && + fromMessage.from != null && + fromMessage.to != null + ) { + throw PrismAgentError.invalidProposedCredentialMessageError() + } + + val fromDID = fromMessage.from!! + val toDID = fromMessage.to!! + val body = Json.decodeFromString(fromMessage.body) + + return ProposeCredential( + id = fromMessage.id, + body = body, + attachments = fromMessage.attachments, + thid = fromMessage.thid, + from = fromDID, + to = toDID + ) + } + } + + @Serializable + data class Body( + val goalCode: String? = null, + val comment: String? = null, + val credentialPreview: CredentialPreview, + val formats: Array + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as Body + + if (goalCode != other.goalCode) return false + if (comment != other.comment) return false + if (credentialPreview != other.credentialPreview) return false + if (!formats.contentEquals(other.formats)) return false + + return true + } + + override fun hashCode(): Int { + var result = goalCode?.hashCode() ?: 0 + result = 31 * result + (comment?.hashCode() ?: 0) + result = 31 * result + credentialPreview.hashCode() + result = 31 * result + formats.contentHashCode() + return result + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as ProposeCredential + + if (id != other.id) return false + if (body != other.body) return false + if (!attachments.contentEquals(other.attachments)) return false + if (thid != other.thid) return false + if (from != other.from) return false + if (to != other.to) return false + if (type != other.type) return false + + return true + } + + override fun hashCode(): Int { + var result = id?.hashCode() ?: 0 + result = 31 * result + body.hashCode() + result = 31 * result + attachments.contentHashCode() + result = 31 * result + (thid?.hashCode() ?: 0) + result = 31 * result + from.hashCode() + result = 31 * result + to.hashCode() + result = 31 * result + type.hashCode() + return result + } +} + +inline fun ProposeCredential.Companion.build( + fromDID: DID, + toDID: DID, + thid: String?, + credentialPreview: CredentialPreview, + credentials: Map = mapOf() +): ProposeCredential { + val aux = credentials.map { (key, value) -> + val attachment = AttachmentDescriptor.build( + payload = value + ) + val format = CredentialFormat(attachId = attachment.id, format = key) + format to attachment + } + return ProposeCredential( + body = ProposeCredential.Body( + credentialPreview = credentialPreview, + formats = aux.map { it.first }.toTypedArray() + ), + attachments = aux.map { it.second }.toTypedArray(), + thid = thid, + from = fromDID, + to = toDID + ) +} diff --git a/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/RequestCredential.kt b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/RequestCredential.kt new file mode 100644 index 000000000..7e22b7bfe --- /dev/null +++ b/prism-agent/src/commonMain/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/RequestCredential.kt @@ -0,0 +1,157 @@ +package io.iohk.atala.prism.walletsdk.prismagent.protocols.issueCredential + +import io.iohk.atala.prism.apollo.uuid.UUID +import io.iohk.atala.prism.walletsdk.domain.models.AttachmentDescriptor +import io.iohk.atala.prism.walletsdk.domain.models.DID +import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.domain.models.PrismAgentError +import io.iohk.atala.prism.walletsdk.prismagent.helpers.build +import io.iohk.atala.prism.walletsdk.prismagent.protocols.ProtocolType +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.js.JsExport + +@Serializable +@JsExport +data class RequestCredential( + val id: String? = UUID.randomUUID4().toString(), + val body: Body, + val attachments: Array, + val thid: String?, + val from: DID, + val to: DID +) { + val type: String = ProtocolType.DidcommRequestCredential.value + + fun makeMessage(): Message { + return Message( + id = id ?: UUID.randomUUID4().toString(), + piuri = type, + from = from, + to = to, + body = Json.encodeToString(body), + attachments = attachments, + thid = thid + ) + } + + companion object { + fun fromMessage(fromMessage: Message): RequestCredential { + require( + fromMessage.piuri == ProtocolType.DidcommRequestCredential.value && + fromMessage.from != null && + fromMessage.to != null + ) { + throw PrismAgentError.invalidRequestCredentialMessageError() + } + + val fromDID = fromMessage.from!! + val toDID = fromMessage.to!! + val body = Json.decodeFromString(fromMessage.body) + + return RequestCredential( + id = fromMessage.id, + body = body, + attachments = fromMessage.attachments, + thid = fromMessage.thid, + from = fromDID, + to = toDID + ) + } + + fun makeRequestFromOfferCredential(offer: OfferCredential): RequestCredential { + return RequestCredential( + body = Body( + goalCode = offer.body.goalCode, + comment = offer.body.comment, + formats = offer.body.formats + ), + attachments = offer.attachments, + thid = offer.thid, + from = offer.to, + to = offer.from + ) + } + } + + @Serializable + data class Body( + val goalCode: String? = null, + val comment: String? = null, + val formats: Array + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as Body + + if (goalCode != other.goalCode) return false + if (comment != other.comment) return false + if (!formats.contentEquals(other.formats)) return false + + return true + } + + override fun hashCode(): Int { + var result = goalCode?.hashCode() ?: 0 + result = 31 * result + (comment?.hashCode() ?: 0) + result = 31 * result + formats.contentHashCode() + return result + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as RequestCredential + + if (id != other.id) return false + if (body != other.body) return false + if (!attachments.contentEquals(other.attachments)) return false + if (thid != other.thid) return false + if (from != other.from) return false + if (to != other.to) return false + if (type != other.type) return false + + return true + } + + override fun hashCode(): Int { + var result = id?.hashCode() ?: 0 + result = 31 * result + body.hashCode() + result = 31 * result + attachments.contentHashCode() + result = 31 * result + (thid?.hashCode() ?: 0) + result = 31 * result + from.hashCode() + result = 31 * result + to.hashCode() + result = 31 * result + type.hashCode() + return result + } +} + +inline fun RequestCredential.Companion.build( + fromDID: DID, + toDID: DID, + thid: String?, + credentials: Map = mapOf() +): RequestCredential { + val aux = credentials.map { (key, value) -> + val attachment = AttachmentDescriptor.build( + payload = value + ) + val format = CredentialFormat(attachId = attachment.id, format = key) + format to attachment + } + return RequestCredential( + body = RequestCredential.Body( + formats = aux.map { it.first }.toTypedArray() + ), + attachments = aux.map { it.second }.toTypedArray(), + thid = thid, + from = fromDID, + to = toDID + ) +} diff --git a/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/helpers/DID.kt b/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/helpers/DID.kt new file mode 100644 index 000000000..ab6632365 --- /dev/null +++ b/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/helpers/DID.kt @@ -0,0 +1,20 @@ +package io.iohk.atala.prism.walletsdk.prismagent.helpers + +import io.iohk.atala.prism.walletsdk.domain.models.DID + +fun DID.Companion.fromMethodAndMethodId( + method: String?, + methodId: String? +): DID { + return DID( + method = method ?: "test", + methodId = methodId ?: "testableId" + ) +} + +fun DID.Companion.fromIndex(index: Int): DID { + return DID.fromMethodAndMethodId( + method = "test$index", + methodId = "testableId$index" + ) +} diff --git a/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredentialTest.kt b/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredentialTest.kt new file mode 100644 index 000000000..319632a20 --- /dev/null +++ b/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/IssueCredentialTest.kt @@ -0,0 +1,80 @@ +package io.iohk.atala.prism.walletsdk.prismagent.protocols.issueCredential + +import io.iohk.atala.prism.walletsdk.domain.models.DID +import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.domain.models.PrismAgentError +import io.iohk.atala.prism.walletsdk.prismagent.helpers.fromIndex +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class IssueCredentialTest { + + @Test + fun testCredentialFromMessage_whenValidIssueMessage_thenInitIssueCredential() { + val fromDID = DID.fromIndex(index = 0) + val toDID = DID.fromIndex(index = 1) + val validIssueCredential = IssueCredential( + body = IssueCredential.Body( + formats = arrayOf( + CredentialFormat( + attachId = "test1", + format = "test" + ) + ) + ), + attachments = arrayOf(), + thid = "1", + from = fromDID, + to = toDID + ) + val issueMessage = validIssueCredential.makeMessage() + val testIssueCredentialFormat = IssueCredential.fromMessage(issueMessage) + assertEquals(testIssueCredentialFormat, validIssueCredential) + } + + @Test + fun testWhenInvalidIssueMessageThenInitIssueCredential() { + val invalidIssueCredential = Message( + piuri = "InvalidType", + from = null, + to = null, + body = "" + ) + assertFailsWith { + IssueCredential.fromMessage(invalidIssueCredential) + } + } + + @Test + fun testWhenValidRequestMessageThenInitIssueCredential() { + val fromDID = DID.fromIndex(index = 0) + val toDID = DID.fromIndex(index = 1) + val validRequestCredential = RequestCredential( + body = RequestCredential.Body( + formats = arrayOf( + CredentialFormat( + attachId = "test1", + format = "test" + ) + ) + ), + attachments = arrayOf(), + thid = "1", + from = fromDID, + to = toDID + ) + val requestMessage = validRequestCredential.makeMessage() + val testIssueCredential = IssueCredential.makeIssueFromRequestCedential(requestMessage) + + assertEquals(validRequestCredential.from, testIssueCredential.to) + assertEquals(validRequestCredential.to, testIssueCredential.from) + assertEquals(validRequestCredential.attachments, testIssueCredential.attachments) + assertEquals(validRequestCredential.id, testIssueCredential.thid) + assertEquals(testIssueCredential.thid, requestMessage.id) + assertEquals(validRequestCredential.body.goalCode, testIssueCredential.body.goalCode) + assertEquals(validRequestCredential.body.comment, testIssueCredential.body.comment) + assertContentEquals(validRequestCredential.body.formats, testIssueCredential.body.formats) + } +} diff --git a/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/OfferCredentialTest.kt b/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/OfferCredentialTest.kt new file mode 100644 index 000000000..6163bd4bc --- /dev/null +++ b/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/OfferCredentialTest.kt @@ -0,0 +1,57 @@ +package io.iohk.atala.prism.walletsdk.prismagent.protocols.issueCredential + +import io.iohk.atala.prism.walletsdk.domain.models.DID +import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.domain.models.PrismAgentError +import io.iohk.atala.prism.walletsdk.prismagent.helpers.fromIndex +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class OfferCredentialTest { + + @Test + fun testWhenValidOfferMessageThenInitOfferCredential() { + val fromDID = DID.fromIndex(index = 0) + val toDID = DID.fromIndex(index = 1) + val validOfferCredential = OfferCredential( + body = OfferCredential.Body( + credentialPreview = CredentialPreview( + attributes = arrayOf( + CredentialPreview.Attribute( + name = "test1", + value = "test", + mimeType = "test.x" + ) + ) + ), + formats = arrayOf( + CredentialFormat( + attachId = "test1", + format = "test" + ) + ) + ), + attachments = arrayOf(), + thid = "1", + from = fromDID, + to = toDID + ) + val offerMessage = validOfferCredential.makeMessage() + val testOfferCredentialFormat = OfferCredential.fromMessage(offerMessage) + assertEquals(testOfferCredentialFormat, validOfferCredential) + } + + @Test + fun testWhenInvalidOfferMessageThenInitOfferCredential() { + val invalidOfferCredential = Message( + piuri = "InvalidType", + from = null, + to = null, + body = "" + ) + assertFailsWith { + OfferCredential.fromMessage(invalidOfferCredential) + } + } +} diff --git a/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/ProposeCredentialTest.kt b/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/ProposeCredentialTest.kt new file mode 100644 index 000000000..c5bc958b5 --- /dev/null +++ b/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/ProposeCredentialTest.kt @@ -0,0 +1,57 @@ +package io.iohk.atala.prism.walletsdk.prismagent.protocols.issueCredential + +import io.iohk.atala.prism.walletsdk.domain.models.DID +import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.domain.models.PrismAgentError +import io.iohk.atala.prism.walletsdk.prismagent.helpers.fromIndex +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class ProposeCredentialTest { + + @Test + fun testWhenValidProposeMessageThenInitProposeCredential() { + val fromDID = DID.fromIndex(index = 0) + val toDID = DID.fromIndex(index = 1) + val validProposeCredential = ProposeCredential( + body = ProposeCredential.Body( + credentialPreview = CredentialPreview( + attributes = arrayOf( + CredentialPreview.Attribute( + name = "test1", + value = "test", + mimeType = "test.x" + ) + ) + ), + formats = arrayOf( + CredentialFormat( + attachId = "test1", + format = "test" + ) + ) + ), + attachments = arrayOf(), + thid = "1", + from = fromDID, + to = toDID + ) + val proposeMessage = validProposeCredential.makeMessage() + val testOfferCredentialFormat = ProposeCredential.fromMessage(proposeMessage) + assertEquals(testOfferCredentialFormat, validProposeCredential) + } + + @Test + fun testWhenInvalidProposeMessageThenInitProposeCredential() { + val invalidProposeCredential = Message( + piuri = "InvalidType", + from = null, + to = null, + body = "" + ) + assertFailsWith { + ProposeCredential.fromMessage(invalidProposeCredential) + } + } +} diff --git a/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/RequestCredentialTest.kt b/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/RequestCredentialTest.kt new file mode 100644 index 000000000..1683433a9 --- /dev/null +++ b/prism-agent/src/commonTest/kotlin/io.iohk.atala.prism.walletsdk.prismagent/protocols/issueCredential/RequestCredentialTest.kt @@ -0,0 +1,50 @@ +package io.iohk.atala.prism.walletsdk.prismagent.protocols.issueCredential + +import io.iohk.atala.prism.walletsdk.domain.models.DID +import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.domain.models.PrismAgentError +import io.iohk.atala.prism.walletsdk.prismagent.helpers.fromIndex +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class RequestCredentialTest { + + @Test + fun testWhenValidRequestMessageThenInitRequestCredential() { + val fromDID = DID.fromIndex(index = 0) + val toDID = DID.fromIndex(index = 1) + val validRequestCredential = RequestCredential( + body = RequestCredential.Body( + goalCode = "test1", + comment = "test1", + formats = arrayOf( + CredentialFormat( + attachId = "test1", + format = "test" + ) + ) + ), + attachments = arrayOf(), + thid = "1", + from = fromDID, + to = toDID + ) + val requestMessage = validRequestCredential.makeMessage() + val testRequestCredentialFormat = RequestCredential.fromMessage(requestMessage) + assertEquals(testRequestCredentialFormat, validRequestCredential) + } + + @Test + fun testWhenInvalidRequestMessageThenInitRequestCredential() { + val invalidRequestCredential = Message( + piuri = "InvalidType", + from = null, + to = null, + body = "" + ) + assertFailsWith { + RequestCredential.fromMessage(invalidRequestCredential) + } + } +}