Skip to content

Commit

Permalink
Add Anchor Serializer (#27)
Browse files Browse the repository at this point in the history
* add anchor serializer

* add tests
  • Loading branch information
Funkatronics authored Jun 12, 2024
1 parent de05483 commit c083872
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 1 deletion.
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T>(val discriminator: ByteArray, serializer: KSerializer<T>)
: KSerializer<T> {

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<T>(namespace: String, ixName: String,
serializer: KSerializer<T>)
: DiscriminatorSerializer<T>(buildDiscriminator(namespace, ixName), serializer) {
companion object {
private fun buildDiscriminator(namespace: String, ixName: String) =
Sha256.hash("$namespace:$ixName".encodeToByteArray()).sliceArray(0 until 8)
}
}

class AnchorInstructionSerializer<T>(namespace: String, ixName: String, serializer: KSerializer<T>)
: AnchorDiscriminatorSerializer<T>(namespace, ixName, serializer) {
constructor(ixName: String, serializer: KSerializer<T>) : this("global", ixName, serializer)
}

inline fun <reified A> AnchorInstructionSerializer(namespace: String, ixName: String) =
AnchorInstructionSerializer<A>(namespace, ixName, serializer())

inline fun <reified A> AnchorInstructionSerializer(ixName: String) =
AnchorInstructionSerializer<A>(ixName, serializer())
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit c083872

Please sign in to comment.