Skip to content

Commit

Permalink
add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Funkatronics committed Jun 6, 2024
1 parent 2f69763 commit 401ce9c
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 1 deletion.
5 changes: 5 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@ POM_SCM_DEV_CONNECTION=scm:git:ssh://[email protected]/solana-mobile/web3-core
POM_DEVELOPER_NAME=Solana Mobile Engineering
POM_DEVELOPER_URL=https://solanamobile.com
POM_DEVELOPER_EMAIL=[email protected]

# RPC URLs used for testing
testing.rpc.defaultUrl=https://api.devnet.solana.com
testing.rpc.localUrl=http://127.0.0.1:8899
localValidator=false
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[versions]
kotlinxCoroutines= "1.7.3"
kotlinxCoroutines = "1.7.3"
ktor = "2.3.11"

# Plugin versions
androidGradlePlugin = "8.0.2"
Expand All @@ -14,6 +15,8 @@ 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" }
multimult = { group = "io.github.funkatronics", name = "multimult", version = "0.2.1" }
rpc-core = { group = "com.solanamobile", name = "rpc-core", version = "0.2.5" }

Expand Down
24 changes: 24 additions & 0 deletions solana/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ kotlin {
}
}
val commonTest by getting {
kotlin.srcDir(File("${buildDir}/generated/src/commonTest/kotlin"))
dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.crypto)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.rpc.core)
}
}
Expand All @@ -49,3 +52,24 @@ kotlin {
mavenPublishing {
coordinates(group as String, moduleArtifactId, version as String)
}

afterEvaluate {
val defaultRpcUrl = properties["testing.rpc.defaultUrl"]
var rpcUrl = properties["rpcUrl"] ?: defaultRpcUrl

val useLocalValidator = project.properties["localValidator"] == "true"
val localRpcUrl = project.properties["testing.rpc.localUrl"]
if (useLocalValidator && localRpcUrl != null) rpcUrl = localRpcUrl

val dir = "${buildDir}/generated/src/commonTest/kotlin/com/solana/config"
mkdir(dir)
File(dir, "TestConfig.kt").writeText(
"""
package com.solana.config
internal object TestConfig {
const val RPC_URL = "$rpcUrl"
}
""".trimIndent()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.solana.programs

import com.solana.config.TestConfig
import com.solana.publickey.SolanaPublicKey
import com.solana.transaction.Message
import com.solana.transaction.Transaction
import com.solana.util.RpcClient
import diglol.crypto.Ed25519
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull

class MemoProgramTests {

@Test
fun `publishMemo builds valid transaction`() = runTest {
// given
val keyPair = Ed25519.generateKeyPair()
val pubkey = SolanaPublicKey(keyPair.publicKey)
val rpc = RpcClient(TestConfig.RPC_URL)
val message = "hello solana!"

// when
val airdropResponse = rpc.requestAirdrop(pubkey, 0.1f)
val blockhashResponse = rpc.getLatestBlockhash()

val transaction = Message.Builder()
.setRecentBlockhash(blockhashResponse.result!!.blockhash)
.addInstruction(MemoProgram.publishMemo(pubkey, message))
.build().run {
val sig = Ed25519.sign(keyPair, serialize())
Transaction(listOf(sig), this)
}

val response = rpc.sendTransaction(transaction)

// then
assertNull(airdropResponse.error)
assertNotNull(airdropResponse.result)
assertNull(response.error)
assertNotNull(response.result)
}
}
104 changes: 104 additions & 0 deletions solana/src/commonTest/kotlin/com/solana/programs/SystemProgramTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.solana.programs

import com.solana.config.TestConfig
import com.solana.publickey.SolanaPublicKey
import com.solana.transaction.Message
import com.solana.transaction.Transaction
import com.solana.util.RpcClient
import diglol.crypto.Ed25519
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull

class SystemProgramTests {

@Test
fun `createAccount successfully creates account`() = runTest {
// given
val payerKeyPair = Ed25519.generateKeyPair()
val newAccountKeyPair = Ed25519.generateKeyPair()
val payerPubkey = SolanaPublicKey(payerKeyPair.publicKey)
val newAccountPubkey = SolanaPublicKey(newAccountKeyPair.publicKey)
val rpc = RpcClient(TestConfig.RPC_URL)

// when
val airdropResponse = rpc.requestAirdrop(payerPubkey, 0.1f)

val rentExemptBalanceResponse = rpc.getMinBalanceForRentExemption(0)

val blockhashResponse = rpc.getLatestBlockhash()

val transaction = Message.Builder()
.setRecentBlockhash(blockhashResponse.result!!.blockhash)
.addInstruction(SystemProgram.createAccount(payerPubkey,
newAccountPubkey,
rentExemptBalanceResponse.result!!,
0L,
SystemProgram.PROGRAM_ID
))
.build().run {
val payerSig = Ed25519.sign(payerKeyPair, serialize())
val newAccountSig = Ed25519.sign(newAccountKeyPair, serialize())
Transaction(listOf(payerSig, newAccountSig), this)
}

rpc.sendTransaction(transaction)

// TODO: add transaction confirmation
runBlocking {
delay(300)
}

val response = rpc.getBalance(newAccountPubkey)

// then
assertNull(airdropResponse.error)
assertNotNull(airdropResponse.result)
assertNull(response.error)
assertNotNull(response.result)
assertEquals(rentExemptBalanceResponse.result!!, response.result)
}

@Test
fun `transfer successfully transfers funds`() = runTest {
// given
val payerKeyPair = Ed25519.generateKeyPair()
val receiverKeyPair = Ed25519.generateKeyPair()
val payerPubkey = SolanaPublicKey(payerKeyPair.publicKey)
val receiverPubkey = SolanaPublicKey(receiverKeyPair.publicKey)
val rpc = RpcClient(TestConfig.RPC_URL)
val balance = 10000000L // lamports

// when
val airdropResponse = rpc.requestAirdrop(payerPubkey, 0.1f)
val blockhashResponse = rpc.getLatestBlockhash()

val transaction = Message.Builder()
.setRecentBlockhash(blockhashResponse.result!!.blockhash)
.addInstruction(SystemProgram.transfer(payerPubkey, receiverPubkey, balance))
.build().run {
val sig = Ed25519.sign(payerKeyPair, serialize())
Transaction(listOf(sig), this)
}

rpc.sendTransaction(transaction)

// TODO: add transaction confirmation
runBlocking {
delay(300)
}

val response = rpc.getBalance(receiverPubkey)

// then
assertNull(airdropResponse.error)
assertNotNull(airdropResponse.result)
assertNull(response.error)
assertNotNull(response.result)
assertEquals(balance, response.result!!)
}
}
152 changes: 152 additions & 0 deletions solana/src/commonTest/kotlin/com/solana/util/RpcClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package com.solana.util

import com.funkatronics.encoders.Base58
import com.solana.networking.HttpNetworkDriver
import com.solana.networking.HttpRequest
import com.solana.networking.Rpc20Driver
import com.solana.publickey.SolanaPublicKey
import com.solana.rpccore.JsonRpc20Request
import com.solana.transaction.Transaction
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.core.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import kotlin.math.pow

class RpcClient(val rpcDriver: Rpc20Driver) {

constructor(url: String, networkDriver: HttpNetworkDriver = KtorHttpDriver()): this(Rpc20Driver(url, networkDriver))

suspend fun requestAirdrop(address: SolanaPublicKey, amountSol: Float) =
rpcDriver.makeRequest(
AirdropRequest(address, (amountSol*10f.pow(9)).toLong()),
String.serializer()
)

suspend fun getBalance(address: SolanaPublicKey, commitment: String = "confirmed") =
rpcDriver.makeRequest(BalanceRequest(address, commitment), SolanaResponseSerializer(Long.serializer()))

suspend fun getMinBalanceForRentExemption(size: Long, commitment: String? = null) =
rpcDriver.makeRequest(RentExemptBalanceRequest(size, commitment), Long.serializer())

suspend fun getLatestBlockhash() =
rpcDriver.makeRequest(LatestBlockhashRequest(), SolanaResponseSerializer(BlockhashResponse.serializer()))

suspend fun sendTransaction(transaction: Transaction) =
rpcDriver.makeRequest(SendTransactionRequest(transaction), String.serializer())

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
class WrappedValue<V>(val value: V?)

class KtorHttpDriver : HttpNetworkDriver {
override suspend fun makeHttpRequest(request: HttpRequest): String =
HttpClient().use { client ->
client.request(request.url) {
method = HttpMethod.parse(request.method)
request.properties.forEach { (k, v) ->
header(k, v)
}
setBody(request.body)
}.bodyAsText().apply {
println(this)
}
}
}

class AirdropRequest(address: SolanaPublicKey, lamports: Long, requestId: String = "1")
: JsonRpc20Request(
method = "requestAirdrop",
params = buildJsonArray {
add(address.base58())
add(lamports)
},
id = requestId
)

class BalanceRequest(address: SolanaPublicKey, commitment: String = "confirmed", requestId: String = "1")
: JsonRpc20Request(
method = "getBalance",
params = buildJsonArray {
add(address.base58())
addJsonObject {
put("commitment", commitment)
}
},
requestId
)

class LatestBlockhashRequest(commitment: String = "confirmed", requestId: String = "1")
: JsonRpc20Request(
method = "getLatestBlockhash",
params = buildJsonArray {
addJsonObject {
put("commitment", commitment)
}
},
requestId
)

@Serializable
class BlockhashResponse(
val blockhash: String,
val lastValidBlockHeight: Long
)

class SendTransactionRequest(transaction: Transaction, skipPreflight: Boolean = true, requestId: String = "1")
: JsonRpc20Request(
method = "sendTransaction",
params = buildJsonArray {
add(Base58.encodeToString(transaction.serialize()))
addJsonObject {
put("skipPreflight", skipPreflight)
}
},
requestId
)

class SignatureStatusesRequest(transactionIds: List<String>, searchTransactionHistory: Boolean = false, requestId: String = "1")
: JsonRpc20Request(
method = "sendTransaction",
params = buildJsonArray {
addJsonArray { transactionIds.forEach { add(it) } }
addJsonObject {
put("searchTransactionHistory", searchTransactionHistory)
}
},
requestId
)

class RentExemptBalanceRequest(size: Long, commitment: String? = null, requestId: String = "1")
: JsonRpc20Request(
method = "getMinimumBalanceForRentExemption",
params = buildJsonArray {
add(size)
commitment?.let {
addJsonObject {
put("commitment", commitment)
}
}
},
requestId
)
}

0 comments on commit 401ce9c

Please sign in to comment.