Skip to content

Commit

Permalink
getAccountInfo + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Funkatronics committed Jun 14, 2024
1 parent 8071edc commit 913cfa1
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 24 deletions.
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ vanniktechMavenPublish = "0.25.3"

[libraries]
crypto = { group = "com.diglol.crypto", name = "crypto", version = "0.1.5" }
kborsh = { group = "io.github.funkatronics", name = "kborsh", version = "0.1.0" }
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinSerialization" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
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" }
ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" }
multimult = { group = "io.github.funkatronics", name = "multimult", version = "0.2.2" }
web3-solana = { group = "com.solanamobile", name = "web3-solana", version = "0.3.0-beta3" }
multimult = { group = "io.github.funkatronics", name = "multimult", version = "0.2.3" }
web3-solana = { group = "com.solanamobile", name = "web3-solana", version = "0.3.0-beta4" }

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
Expand Down
2 changes: 2 additions & 0 deletions solanaclient/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ kotlin {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.web3.solana)
implementation(libs.kborsh)
implementation(libs.multimult)
}
}
Expand All @@ -44,6 +45,7 @@ kotlin {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.mock)
implementation(libs.crypto)
}
}
Expand Down
24 changes: 24 additions & 0 deletions solanaclient/src/commonMain/kotlin/com/solana/rpc/AccountInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.solana.rpc

import com.solana.publickey.SolanaPublicKey
import com.solana.serializers.BorshAsBase64JsonArraySerializer
import com.solana.serializers.SolanaResponseSerializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable

@Serializable
data class AccountInfo<D>(
val data: D?,
val executable: Boolean,
val lamports: ULong,
val owner: SolanaPublicKey,
val rentEpoch: ULong,
val size: ULong? = null
)

fun <A> SolanaAccountSerializer(serializer: KSerializer<A>) =
SolanaResponseSerializer(
AccountInfo.serializer(
BorshAsBase64JsonArraySerializer(serializer)
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ class SolanaRpcClient(
String.serializer()
)

suspend fun <D> getAccountInfo(
serializer: KSerializer<D>,
publicKey: SolanaPublicKey,
commitment: Commitment? = null,
minContextSlot: Long? = null,
requestId: String? = null
) = makeRequest(
AccountInfoRequest(publicKey, commitment, minContextSlot, requestId),
SolanaAccountSerializer(serializer)
)

suspend inline fun <reified D> getAccountInfo(
publicKey: SolanaPublicKey,
commitment: Commitment? = null,
minContextSlot: Long? = null,
requestId: String? = null
) = getAccountInfo<D>(serializer(), publicKey, commitment, minContextSlot, requestId)

suspend fun getBalance(
address: SolanaPublicKey,
commitment: Commitment = Commitment.CONFIRMED,
Expand Down Expand Up @@ -135,6 +153,25 @@ class SolanaRpcClient(
id ?: "$method-${Random.nextInt(100000000, 999999999)}"
)

class AccountInfoRequest(
publicKey: SolanaPublicKey,
commitment: Commitment? = null,
minContextSlot: Long? = null,
requestId: String? = null
) : SolanaRpcRequest(
method = "getAccountInfo",
params = buildJsonArray {
add(publicKey.base58())
addJsonObject {
put("encoding", Encoding.base64.getEncoding())
commitment?.let { put("commitment", commitment.value) }
minContextSlot?.let { put("minContextSlot", minContextSlot) }
// TODO: support data slicing
}
},
requestId
)

class AirdropRequest(
address: SolanaPublicKey,
lamports: Long,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.solana.serializers

import com.funkatronics.encoders.Base64
import com.funkatronics.kborsh.BorshDecoder
import com.funkatronics.kborsh.BorshEncoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

class SolanaResponseSerializer<R>(dataSerializer: KSerializer<R>)
: KSerializer<R?> {
private val serializer = WrappedValue.serializer(dataSerializer)
override val descriptor: SerialDescriptor = serializer.descriptor

override fun serialize(encoder: Encoder, value: R?) =
encoder.encodeSerializableValue(serializer, WrappedValue(value))

override fun deserialize(decoder: Decoder): R? =
decoder.decodeSerializableValue(serializer).value
}

@Serializable
private class WrappedValue<V>(val value: V?)

internal object ByteArrayAsBase64JsonArraySerializer: KSerializer<ByteArray> {
private val delegateSerializer = ListSerializer(String.serializer())
override val descriptor: SerialDescriptor = delegateSerializer.descriptor

override fun serialize(encoder: Encoder, value: ByteArray) =
encoder.encodeSerializableValue(delegateSerializer, listOf(
Base64.encodeToString(value), "base64"
))

override fun deserialize(decoder: Decoder): ByteArray {
decoder.decodeSerializableValue(delegateSerializer).apply {
if (contains("base64")) first { it != "base64" }.apply {
return Base64.decode(this)
}
else throw(SerializationException("Not Base64"))
}
}
}

internal class BorshAsBase64JsonArraySerializer<T>(private val dataSerializer: KSerializer<T>): KSerializer<T?> {
private val delegateSerializer = ByteArrayAsBase64JsonArraySerializer
override val descriptor: SerialDescriptor = dataSerializer.descriptor

override fun serialize(encoder: Encoder, value: T?) =
encoder.encodeSerializableValue(delegateSerializer,
value?.let {
BorshEncoder().apply {
encodeSerializableValue(dataSerializer, value)
}.borshEncodedBytes
} ?: byteArrayOf()
)

override fun deserialize(decoder: Decoder): T? =
decoder.decodeSerializableValue(delegateSerializer).run {
if (this.isEmpty()) return null
BorshDecoder(this).decodeSerializableValue(dataSerializer)
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,90 @@
package com.solana.rpc

import com.funkatronics.encoders.Base64
import com.funkatronics.kborsh.Borsh
import com.solana.config.TestConfig
import com.solana.networking.KtorNetworkDriver
import com.solana.publickey.SolanaPublicKey
import com.solana.serialization.ByteStringSerializer
import com.solana.transaction.AccountMeta
import com.solana.transaction.Message
import com.solana.transaction.Transaction
import com.solana.transaction.TransactionInstruction
import diglol.crypto.Ed25519
import io.ktor.client.*
import io.ktor.client.engine.mock.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToByteArray
import kotlin.test.*

class RpcClientTests {

@Test
fun `getAccountInfo returns AccountInfo object`() = runTest {
// given
val expectedAccountData = "system_program"
val rpcClient = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver())

// when
val response = rpcClient.getAccountInfo(
ByteStringSerializer(expectedAccountData.length),
SolanaPublicKey.from("11111111111111111111111111111111")
)

// then
assertNull(response.error)
assertNotNull(response.result)
assertEquals(expectedAccountData, response.result!!.data!!.decodeToString())
}

@Test
fun `getAccountInfo deserializes account data struct`() = runTest {
// given
@Serializable
data class TestAccountData(val name: String, val number: Int, val bool: Boolean)
val testData = TestAccountData("accountInfoTest", 123456789, false)
val testDataBorsh = Borsh.encodeToByteArray(testData)
val ownerPubkey = SolanaPublicKey.from("11111111111111111111111111111111")
val mockedResponse = """
{
"jsonrpc":"2.0",
"result":{
"context":{"apiVersion":"apiVer","slot":123456789},
"value":{
"data":[
"${Base64.encodeToString(testDataBorsh)}",
"base64"
]
"executable":true,
"lamports":1,
"owner":"11111111111111111111111111111111",
"rentEpoch":1234567890,
"space":${testDataBorsh.size}
}
},
"id":"requestId"
}
""".trimIndent()

val rpcClient = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver(
HttpClient(MockEngine {
respond(mockedResponse)
})
))

// when
val response = rpcClient.getAccountInfo<TestAccountData>(ownerPubkey)

// then
assertNull(response.error)
assertNotNull(response.result)
assertEquals(response.result!!.owner, ownerPubkey)
assertEquals(testData, response.result!!.data)
}

@Test
fun `getLatestBlockhash returns valid blockhash response`() = runTest {
// given
Expand Down

0 comments on commit 913cfa1

Please sign in to comment.