diff --git a/.gitignore b/.gitignore index 1209939..de77d8e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ /build */build /.gradle +/.kotlin # IDE Files /.idea diff --git a/build.gradle.kts b/build.gradle.kts index acbd6e8..5a66e90 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,13 @@ plugins { alias(libs.plugins.publish) apply false } +configurations.all { + resolutionStrategy.dependencySubstitution { + substitute(module("com.solanamobile:web3-core")).using(project(":core")) + substitute(module("com.solanamobile:web3-solana")).using(project(":solana")) + } +} + //tasks.register("clean", Delete::class) { // delete(rootProject.buildDir) //} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index d51190e..113adf3 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -8,8 +8,8 @@ val artifactIdPrefix: String by project val moduleArtifactId = "$artifactIdPrefix-core" kotlin { + jvmToolchain(11) jvm { - jvmToolchain(11) withJava() testRuns["test"].executionTask.configure { useJUnitPlatform() diff --git a/gradle.properties b/gradle.properties index 31e67ff..1181831 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,10 @@ kotlin.code.style=official kotlin.js.compiler=ir +#MPP +kotlin.mpp.enableCInteropCommonization=true +kotlin.mpp.androidSourceSetLayoutVersion=2 + # Group name and project version, used when publishing. For official releases, the version should be # provided to Gradle via '-P version="1.0"'. group=com.solanamobile diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 25bdd0d..2335a1a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,11 @@ [versions] kotlinxCoroutines = "1.7.3" ktor = "2.3.11" +rpcCore = "0.2.7" # Plugin versions androidGradlePlugin = "8.0.2" -kotlin = "1.9.21" +kotlin = "2.0.20-Beta2" kotlinSerialization = "1.6.2" vanniktechMavenPublish = "0.25.3" @@ -18,7 +19,9 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor 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.3" } -rpc-core = { group = "com.solanamobile", name = "rpc-core", version = "0.2.5" } +rpc-core = { group = "com.solanamobile", name = "rpc-core", version.ref = "rpcCore" } +rpc-ktordriver = { group = "com.solanamobile", name = "rpc-ktordriver", version.ref = "rpcCore" } +rpc-solana = { group = "com.solanamobile", name = "rpc-solana", version.ref = "rpcCore" } salkt = { group = "io.github.funkatronics", name = "salkt", version = "0.1.0" } [plugins] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fae0804..0d18421 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/solana/build.gradle.kts b/solana/build.gradle.kts index b9dffe1..6c67ce4 100644 --- a/solana/build.gradle.kts +++ b/solana/build.gradle.kts @@ -6,10 +6,12 @@ plugins { val artifactIdPrefix: String by project val moduleArtifactId = "$artifactIdPrefix-solana" +val buildDir = layout.buildDirectory.asFile.get() +val generatedDir = "${buildDir}/generated/src/commonTest/kotlin" kotlin { + jvmToolchain(11) jvm { - jvmToolchain(11) withJava() testRuns["test"].executionTask.configure { useJUnitPlatform() @@ -37,7 +39,7 @@ kotlin { } } val commonTest by getting { - kotlin.srcDir(File("${buildDir}/generated/src/commonTest/kotlin")) + kotlin.srcDir(File(generatedDir)) dependencies { implementation(libs.kotlin.test) implementation(libs.kotlinx.coroutines.test) @@ -45,6 +47,8 @@ kotlin { implementation(libs.ktor.client.core) implementation(libs.ktor.client.cio) implementation(libs.rpc.core) + implementation(libs.rpc.ktordriver) + implementation(libs.rpc.solana) } } } @@ -62,7 +66,7 @@ afterEvaluate { val localRpcUrl = project.properties["testing.rpc.localUrl"] if (useLocalValidator && localRpcUrl != null) rpcUrl = localRpcUrl - val dir = "${buildDir}/generated/src/commonTest/kotlin/com/solana/config" + val dir = "${generatedDir}/com/solana/config" mkdir(dir) File(dir, "TestConfig.kt").writeText( """ diff --git a/solana/src/commonMain/kotlin/com/solana/programs/MemoProgram.kt b/solana/src/commonMain/kotlin/com/solana/programs/MemoProgram.kt index e5275a6..9cbe1c5 100644 --- a/solana/src/commonMain/kotlin/com/solana/programs/MemoProgram.kt +++ b/solana/src/commonMain/kotlin/com/solana/programs/MemoProgram.kt @@ -3,9 +3,13 @@ package com.solana.programs import com.solana.publickey.SolanaPublicKey import com.solana.transaction.AccountMeta import com.solana.transaction.TransactionInstruction +import kotlin.experimental.ExperimentalObjCName import kotlin.jvm.JvmStatic +import kotlin.native.ObjCName -object MemoProgram : Program { +@OptIn(ExperimentalObjCName::class) +@ObjCName("MemoProgram") +object MemoProgram { @JvmStatic val PROGRAM_ID = SolanaPublicKey.from("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") diff --git a/solana/src/commonMain/kotlin/com/solana/programs/SystemProgram.kt b/solana/src/commonMain/kotlin/com/solana/programs/SystemProgram.kt index 37b4f5c..db8ea5f 100644 --- a/solana/src/commonMain/kotlin/com/solana/programs/SystemProgram.kt +++ b/solana/src/commonMain/kotlin/com/solana/programs/SystemProgram.kt @@ -5,9 +5,13 @@ import com.solana.publickey.SolanaPublicKey import com.solana.transaction.AccountMeta import com.solana.transaction.TransactionInstruction import kotlinx.serialization.builtins.ByteArraySerializer +import kotlin.experimental.ExperimentalObjCName import kotlin.jvm.JvmStatic +import kotlin.native.ObjCName -object SystemProgram : Program { +@OptIn(ExperimentalObjCName::class) +@ObjCName("SystemProgram") +object SystemProgram { @JvmStatic val PROGRAM_ID = SolanaPublicKey.from("11111111111111111111111111111111") diff --git a/solana/src/commonTest/kotlin/com/solana/programs/MemoProgramTests.kt b/solana/src/commonTest/kotlin/com/solana/programs/MemoProgramTests.kt index 9092425..6929941 100644 --- a/solana/src/commonTest/kotlin/com/solana/programs/MemoProgramTests.kt +++ b/solana/src/commonTest/kotlin/com/solana/programs/MemoProgramTests.kt @@ -1,10 +1,12 @@ package com.solana.programs import com.solana.config.TestConfig +import com.solana.networking.KtorNetworkDriver import com.solana.publickey.SolanaPublicKey +import com.solana.rpc.SolanaRpcClient +import com.solana.rpc.TransactionOptions 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 @@ -18,7 +20,7 @@ class MemoProgramTests { // given val keyPair = Ed25519.generateKeyPair() val pubkey = SolanaPublicKey(keyPair.publicKey) - val rpc = RpcClient(TestConfig.RPC_URL) + val rpc = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) val message = "hello solana!" // when @@ -33,7 +35,7 @@ class MemoProgramTests { Transaction(listOf(sig), this) } - val response = rpc.sendTransaction(transaction) + val response = rpc.sendTransaction(transaction, TransactionOptions(skipPreflight = true)) // then assertNull(airdropResponse.error) diff --git a/solana/src/commonTest/kotlin/com/solana/programs/SystemProgramTests.kt b/solana/src/commonTest/kotlin/com/solana/programs/SystemProgramTests.kt index cd7b761..1f7fc99 100644 --- a/solana/src/commonTest/kotlin/com/solana/programs/SystemProgramTests.kt +++ b/solana/src/commonTest/kotlin/com/solana/programs/SystemProgramTests.kt @@ -1,14 +1,15 @@ package com.solana.programs import com.solana.config.TestConfig +import com.solana.networking.KtorNetworkDriver import com.solana.publickey.SolanaPublicKey +import com.solana.rpc.Commitment +import com.solana.rpc.SolanaRpcClient +import com.solana.rpc.TransactionOptions import com.solana.transaction.Message import com.solana.transaction.Transaction -import com.solana.util.RpcClient import diglol.crypto.Ed25519 import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext import kotlin.test.Test @@ -25,7 +26,7 @@ class SystemProgramTests { val newAccountKeyPair = Ed25519.generateKeyPair() val payerPubkey = SolanaPublicKey(payerKeyPair.publicKey) val newAccountPubkey = SolanaPublicKey(newAccountKeyPair.publicKey) - val rpc = RpcClient(TestConfig.RPC_URL) + val rpc = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) // when val airdropResponse = rpc.requestAirdrop(payerPubkey, 0.1f) @@ -49,7 +50,10 @@ class SystemProgramTests { } withContext(Dispatchers.Default.limitedParallelism(1)) { - rpc.sendAndConfirmTransaction(transaction) + rpc.sendAndConfirmTransaction(transaction, TransactionOptions( + commitment = Commitment.CONFIRMED, + skipPreflight = true + )) } val response = rpc.getBalance(newAccountPubkey) @@ -69,7 +73,7 @@ class SystemProgramTests { val receiverKeyPair = Ed25519.generateKeyPair() val payerPubkey = SolanaPublicKey(payerKeyPair.publicKey) val receiverPubkey = SolanaPublicKey(receiverKeyPair.publicKey) - val rpc = RpcClient(TestConfig.RPC_URL) + val rpc = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) val balance = 10000000L // lamports // when @@ -85,7 +89,10 @@ class SystemProgramTests { } withContext(Dispatchers.Default.limitedParallelism(1)) { - rpc.sendAndConfirmTransaction(transaction) + rpc.sendAndConfirmTransaction(transaction, TransactionOptions( + commitment = Commitment.CONFIRMED, + skipPreflight = true + )) } val response = rpc.getBalance(receiverPubkey) diff --git a/solana/src/commonTest/kotlin/com/solana/util/RpcClient.kt b/solana/src/commonTest/kotlin/com/solana/util/RpcClient.kt deleted file mode 100644 index 42741bc..0000000 --- a/solana/src/commonTest/kotlin/com/solana/util/RpcClient.kt +++ /dev/null @@ -1,197 +0,0 @@ -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.util.date.* -import io.ktor.utils.io.core.* -import kotlinx.coroutines.isActive -import kotlinx.coroutines.withTimeout -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.nullable -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()) - - suspend fun sendAndConfirmTransaction(transaction: Transaction) = - sendTransaction(transaction).apply { - result?.let { confirmTransaction(it) } - } - - suspend fun getSignatureStatuses(signatures: List) = - rpcDriver.makeRequest(SignatureStatusesRequest(signatures), - SolanaResponseSerializer(ListSerializer(SignatureStatus.serializer().nullable))) - - suspend fun confirmTransaction( - signature: String, - commitment: String = "confirmed", - timeout: Long = 15000 - ): Result = withTimeout(timeout) { - suspend fun getStatus() = - getSignatureStatuses(listOf(signature)) - .result?.first() - - // wait for desired transaction status - while(getStatus()?.confirmationStatus != commitment) { - - // wait a bit before retrying - val millis = getTimeMillis() - var inc = 0 - while(getTimeMillis() - millis < 300 && isActive) { inc++ } - - if (!isActive) break // breakout after timeout - } - - Result.success(signature) - } - - class SolanaResponseSerializer(dataSerializer: KSerializer) - : KSerializer { - 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(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, searchTransactionHistory: Boolean = false, requestId: String = "1") - : JsonRpc20Request( - method = "getSignatureStatuses", - params = buildJsonArray { - addJsonArray { transactionIds.forEach { add(it) } } - addJsonObject { - put("searchTransactionHistory", searchTransactionHistory) - } - }, - requestId - ) - - @Serializable - data class SignatureStatus( - val slot: Long, - val confirmations: Long?, - var err: JsonObject?, - var confirmationStatus: String? - ) - - class RentExemptBalanceRequest(size: Long, commitment: String? = null, requestId: String = "1") - : JsonRpc20Request( - method = "getMinimumBalanceForRentExemption", - params = buildJsonArray { - add(size) - commitment?.let { - addJsonObject { - put("commitment", commitment) - } - } - }, - requestId - ) -} \ No newline at end of file