From c083872708e4f954708235205609c00ecd048b6c Mon Sep 17 00:00:00 2001 From: Marco Martinez Date: Wed, 12 Jun 2024 14:29:08 -0600 Subject: [PATCH] Add Anchor Serializer (#27) * add anchor serializer * add tests --- gradle/libs.versions.toml | 2 +- .../AnchorInstructionSerializer.kt | 45 ++++++++++++++ .../AnchorDiscriminatorSerializerTests.kt | 60 +++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 solana/src/commonMain/kotlin/com/solana/serialization/AnchorInstructionSerializer.kt create mode 100644 solana/src/commonTest/kotlin/com/solana/serialization/AnchorDiscriminatorSerializerTests.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d3ef6f0..5223ad7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } -multimult = { group = "io.github.funkatronics", name = "multimult", version = "0.2.2" } +multimult = { group = "io.github.funkatronics", name = "multimult", version = "0.2.3" } rpc-core = { group = "com.solanamobile", name = "rpc-core", version = "0.2.5" } [plugins] diff --git a/solana/src/commonMain/kotlin/com/solana/serialization/AnchorInstructionSerializer.kt b/solana/src/commonMain/kotlin/com/solana/serialization/AnchorInstructionSerializer.kt new file mode 100644 index 0000000..9954a5a --- /dev/null +++ b/solana/src/commonMain/kotlin/com/solana/serialization/AnchorInstructionSerializer.kt @@ -0,0 +1,45 @@ +package com.solana.serialization + +import com.funkatronics.hash.Sha256 +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.serializer + +open class DiscriminatorSerializer(val discriminator: ByteArray, serializer: KSerializer) + : KSerializer { + + private val accountSerializer = serializer + override val descriptor: SerialDescriptor = accountSerializer.descriptor + + override fun serialize(encoder: Encoder, value: T) { + discriminator.forEach { encoder.encodeByte(it) } + accountSerializer.serialize(encoder, value) + } + + override fun deserialize(decoder: Decoder): T { + ByteArray(discriminator.size).map { decoder.decodeByte() } + return accountSerializer.deserialize(decoder) + } +} + +open class AnchorDiscriminatorSerializer(namespace: String, ixName: String, + serializer: KSerializer) + : DiscriminatorSerializer(buildDiscriminator(namespace, ixName), serializer) { + companion object { + private fun buildDiscriminator(namespace: String, ixName: String) = + Sha256.hash("$namespace:$ixName".encodeToByteArray()).sliceArray(0 until 8) + } +} + +class AnchorInstructionSerializer(namespace: String, ixName: String, serializer: KSerializer) + : AnchorDiscriminatorSerializer(namespace, ixName, serializer) { + constructor(ixName: String, serializer: KSerializer) : this("global", ixName, serializer) + } + +inline fun AnchorInstructionSerializer(namespace: String, ixName: String) = + AnchorInstructionSerializer(namespace, ixName, serializer()) + +inline fun AnchorInstructionSerializer(ixName: String) = + AnchorInstructionSerializer(ixName, serializer()) \ No newline at end of file diff --git a/solana/src/commonTest/kotlin/com/solana/serialization/AnchorDiscriminatorSerializerTests.kt b/solana/src/commonTest/kotlin/com/solana/serialization/AnchorDiscriminatorSerializerTests.kt new file mode 100644 index 0000000..1192f85 --- /dev/null +++ b/solana/src/commonTest/kotlin/com/solana/serialization/AnchorDiscriminatorSerializerTests.kt @@ -0,0 +1,60 @@ +package com.solana.serialization + +import com.funkatronics.hash.Sha256 +import com.funkatronics.kborsh.Borsh +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToByteArray +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class AnchorDiscriminatorSerializerTests { + + @Test + fun `discriminator is first 8 bytes of identifier hash`() { + // given + val namespace = "test" + val ixName = "testInstruction" + val data = "data" + val expectedDiscriminator = Sha256.hash( + "$namespace:$ixName".encodeToByteArray() + ).sliceArray(0..7) + + // when + val serialized = Borsh.encodeToByteArray(AnchorInstructionSerializer(namespace, ixName), data) + + // then + assertContentEquals(expectedDiscriminator, serialized.sliceArray(0..7)) + } + + @Test + fun `data is serialized after 8 byte identifier hash`() { + // given + val ixName = "testInstruction" + val data = "data" + val expectedEncodedData = Borsh.encodeToByteArray(data) + + // when + val serialized = Borsh.encodeToByteArray(AnchorInstructionSerializer(ixName), data) + + // then + assertContentEquals(expectedEncodedData, serialized.sliceArray(8 until serialized.size)) + } + + @Test + fun `serialize and deserialize data struct`() { + // given + @Serializable data class TestData(val name: String, val number: Int, val boolean: Boolean) + val ixName = "testInstruction" + val data = TestData("testName", 12345678, true) + val expectedEncodedData = Borsh.encodeToByteArray(data) + + // when + val serialized = Borsh.encodeToByteArray(AnchorInstructionSerializer(ixName), data) + val deserialized: TestData = Borsh.decodeFromByteArray(AnchorInstructionSerializer(ixName), serialized) + + // then + assertContentEquals(expectedEncodedData, serialized.sliceArray(8 until serialized.size)) + assertEquals(data, deserialized) + } +} \ No newline at end of file