From 437e1d450a5787346f4150f2ad8a023abd216eea Mon Sep 17 00:00:00 2001 From: funkatronics Date: Fri, 7 Jun 2024 10:43:58 -0600 Subject: [PATCH 1/8] add wip client --- gradle.properties | 5 + gradle/libs.versions.toml | 1 + settings.gradle.kts | 1 + solana/build.gradle.kts | 75 ++++++++ .../kotlin/com/solana/rpc/SolanaRpcClient.kt | 162 ++++++++++++++++++ .../serializers/SolanaResponseSerializer.kt | 22 +++ 6 files changed, 266 insertions(+) create mode 100644 solana/build.gradle.kts create mode 100644 solana/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt create mode 100644 solana/src/commonMain/kotlin/com/solana/serializers/SolanaResponseSerializer.kt diff --git a/gradle.properties b/gradle.properties index ca40cd3..203df78 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,3 +30,8 @@ POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/solana-mobile/rpc-core.git POM_DEVELOPER_NAME=Solana Mobile Engineering POM_DEVELOPER_URL=https://solanamobile.com POM_DEVELOPER_EMAIL=eng@solanamobile.com + +# RPC URLs used for testing +testing.rpc.defaultUrl=https://api.devnet.solana.com +testing.rpc.localUrl=http://127.0.0.1:8899 +localValidator=false \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e5019b..05b2f16 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = 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.1" } +web3-solana = { group = "com.solanamobile", name = "web3-solana", version = "0.3.0-beta3" } [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1cdea4a..9272dd5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,4 +18,5 @@ rootProject.name = "Rpc Core" include(":rpccore") include(":okiodriver") include(":ktordriver") +include(":solana") diff --git a/solana/build.gradle.kts b/solana/build.gradle.kts new file mode 100644 index 0000000..93e4444 --- /dev/null +++ b/solana/build.gradle.kts @@ -0,0 +1,75 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.publish) +} + +val artifactIdPrefix: String by project +val moduleArtifactId = "$artifactIdPrefix-solana" + +kotlin { + jvm { + jvmToolchain(11) + withJava() + testRuns["test"].executionTask.configure { + useJUnitPlatform() + } + } + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + macosX64(), + macosArm64() + ).forEach { + it.binaries.framework { + baseName = moduleArtifactId + } + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(mapOf("path" to ":rpccore"))) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.web3.solana) + implementation(libs.multimult) + } + } + val commonTest by getting { + kotlin.srcDir(File("${buildDir}/generated/src/commonTest/kotlin")) + dependencies { + implementation(libs.kotlin.test) + implementation(project(mapOf("path" to ":ktordriver"))) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.kotlinx.serialization.json) + } + } + } +} + +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() + ) +} diff --git a/solana/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt b/solana/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt new file mode 100644 index 0000000..d5eb46b --- /dev/null +++ b/solana/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt @@ -0,0 +1,162 @@ +package com.solana.rpc + +import com.funkatronics.encoders.Base58 +import com.solana.networking.HttpNetworkDriver +import com.solana.networking.Rpc20Driver +import com.solana.publickey.SolanaPublicKey +import com.solana.rpccore.JsonRpc20Request +import com.solana.serializers.SolanaResponseSerializer +import com.solana.transaction.Transaction +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.* +import kotlin.math.pow +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource + +class SolanaRpcClient(val rpcDriver: Rpc20Driver) { + + constructor(url: String, networkDriver: HttpNetworkDriver): 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() + + val timeSource = TimeSource.Monotonic + + // wait for desired transaction status + while(getStatus()?.confirmationStatus != commitment) { + + // wait a bit before retrying + val mark = timeSource.markNow() + var inc = 0 + while(timeSource.markNow() - mark < 0.3.seconds && isActive) { inc++ } + + if (!isActive) break // breakout after timeout + } + + Result.success(signature) + } + + 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 diff --git a/solana/src/commonMain/kotlin/com/solana/serializers/SolanaResponseSerializer.kt b/solana/src/commonMain/kotlin/com/solana/serializers/SolanaResponseSerializer.kt new file mode 100644 index 0000000..c679a95 --- /dev/null +++ b/solana/src/commonMain/kotlin/com/solana/serializers/SolanaResponseSerializer.kt @@ -0,0 +1,22 @@ +package com.solana.serializers + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +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 +private class WrappedValue(val value: V?) \ No newline at end of file From 896ffc2cbeb86473fbda892bf008076ff69f22ef Mon Sep 17 00:00:00 2001 From: funkatronics Date: Fri, 7 Jun 2024 10:53:33 -0600 Subject: [PATCH 2/8] add first test --- .github/actions/install-solana/action.yml | 39 +++++++++++++++++++ .github/workflows/build.yml | 16 +++++++- ktordriver/build.gradle.kts | 2 +- .../kotlin/com/solana/rpc/RpcClientTests.kt | 26 +++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 .github/actions/install-solana/action.yml create mode 100644 solana/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt diff --git a/.github/actions/install-solana/action.yml b/.github/actions/install-solana/action.yml new file mode 100644 index 0000000..c21ebd1 --- /dev/null +++ b/.github/actions/install-solana/action.yml @@ -0,0 +1,39 @@ +name: Install Solana + +inputs: + solana_version: + description: Version of Solana to install + required: true + +runs: + using: "composite" + steps: + - name: Cache Solana Install + if: ${{ !env.ACT }} + id: cache-solana-install + uses: actions/cache@v2 + with: + path: "$HOME/.local/share/solana/install/releases/${{ inputs.solana_version }}" + key: ${{ runner.os }}-Solana-v${{ inputs.solana_version }} + + - name: Install Solana + if: ${{ !env.ACT }} && steps.cache-solana-install.cache-hit != 'true' + run: | + sh -c "$(curl -sSfL https://release.solana.com/v${{ inputs.solana_version }}/install)" + shell: bash + + - name: Set Active Solana Version + run: | + rm -f "$HOME/.local/share/solana/install/active_release" + ln -s "$HOME/.local/share/solana/install/releases/${{ inputs.solana_version }}/solana-release" "$HOME/.local/share/solana/install/active_release" + shell: bash + + - name: Add Solana bin to Path + run: | + echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + shell: bash + + - name: Verify Solana install + run: | + solana --version + shell: bash \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2581c78..e282e71 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,9 @@ on: jobs: build: runs-on: macos-latest + strategy: + matrix: + solana: ["1.18.14"] steps: - name: Checkout @@ -26,11 +29,22 @@ jobs: distribution: 'temurin' cache: gradle + - name: Install Solana + uses: ./.github/actions/install-solana + with: + solana_version: ${{ matrix.solana }} + + - name: Start local validator + run: solana-test-validator > /dev/null 2>&1 & + # Build - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Test - run: ./gradlew build + run: ./gradlew build -PlocalValidator=true + + - name: Stop local validator + run: kill %1 - name: Save Test Reports if: failure() diff --git a/ktordriver/build.gradle.kts b/ktordriver/build.gradle.kts index 139538d..8a32264 100644 --- a/ktordriver/build.gradle.kts +++ b/ktordriver/build.gradle.kts @@ -31,7 +31,7 @@ kotlin { dependencies { implementation(project(mapOf("path" to ":rpccore"))) implementation(libs.kotlinx.coroutines.core) - implementation(libs.ktor.client.core) + api(libs.ktor.client.core) implementation(libs.ktor.client.cio) } } diff --git a/solana/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt b/solana/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt new file mode 100644 index 0000000..ed72d19 --- /dev/null +++ b/solana/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt @@ -0,0 +1,26 @@ +package com.solana.rpc + +import com.solana.config.TestConfig +import com.solana.networking.KtorNetworkDriver +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class RpcClientTests { + + @Test + fun `getLatestBlockhash returns valid blockhash response`() = runTest { + // given + val rpcClient = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) + + // when + val response = rpcClient.getLatestBlockhash() + + // then + assertNull(response.error) + assertNotNull(response.result) + assertTrue { response.result!!.blockhash.isNotEmpty() } + } +} \ No newline at end of file From 2d13f5f829c04fdba0237e6eb2640a1ce779c562 Mon Sep 17 00:00:00 2001 From: funkatronics Date: Fri, 7 Jun 2024 11:16:32 -0600 Subject: [PATCH 3/8] more tests --- solana/build.gradle.kts | 3 +- .../kotlin/com/solana/rpc/SolanaRpcClient.kt | 32 +++--- .../kotlin/com/solana/rpc/RpcClientTests.kt | 104 +++++++++++++++++- 3 files changed, 120 insertions(+), 19 deletions(-) diff --git a/solana/build.gradle.kts b/solana/build.gradle.kts index 93e4444..ddfc17f 100644 --- a/solana/build.gradle.kts +++ b/solana/build.gradle.kts @@ -40,10 +40,11 @@ kotlin { val commonTest by getting { kotlin.srcDir(File("${buildDir}/generated/src/commonTest/kotlin")) dependencies { - implementation(libs.kotlin.test) implementation(project(mapOf("path" to ":ktordriver"))) + implementation(libs.kotlin.test) implementation(libs.kotlinx.coroutines.test) implementation(libs.kotlinx.serialization.json) + implementation(libs.crypto) } } } diff --git a/solana/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt b/solana/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt index d5eb46b..0dcac88 100644 --- a/solana/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt +++ b/solana/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt @@ -75,6 +75,7 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { Result.success(signature) } + //region Requests class AirdropRequest(address: SolanaPublicKey, lamports: Long, requestId: String = "1") : JsonRpc20Request( method = "requestAirdrop", @@ -108,12 +109,6 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { requestId ) - @Serializable - class BlockhashResponse( - val blockhash: String, - val lastValidBlockHeight: Long - ) - class SendTransactionRequest(transaction: Transaction, skipPreflight: Boolean = true, requestId: String = "1") : JsonRpc20Request( method = "sendTransaction", @@ -138,14 +133,6 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { 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", @@ -159,4 +146,21 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { }, requestId ) + //endregion + + //region Responses + @Serializable + class BlockhashResponse( + val blockhash: String, + val lastValidBlockHeight: Long + ) + + @Serializable + data class SignatureStatus( + val slot: Long, + val confirmations: Long?, + var err: JsonObject?, + var confirmationStatus: String? + ) + //endregion } \ No newline at end of file diff --git a/solana/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt b/solana/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt index ed72d19..78b1b22 100644 --- a/solana/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt +++ b/solana/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt @@ -2,11 +2,16 @@ package com.solana.rpc import com.solana.config.TestConfig import com.solana.networking.KtorNetworkDriver +import com.solana.publickey.SolanaPublicKey +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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue +import kotlinx.coroutines.withContext +import kotlin.test.* class RpcClientTests { @@ -23,4 +28,95 @@ class RpcClientTests { assertNotNull(response.result) assertTrue { response.result!!.blockhash.isNotEmpty() } } + + @Test + fun `getMinBalanceForRentExemption returns rent exempt balance`() = runTest { + // given + val rpcClient = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) + val size = 0L + + // when + val response = rpcClient.getMinBalanceForRentExemption(size) + + // then + assertNull(response.error) + assertNotNull(response.result) + assertTrue { response.result!! > 0 } + } + + @Test + fun `getSignatureStatus returns valid blockhash response`() = runTest { + // given + val keyPair = Ed25519.generateKeyPair() + val pubkey = SolanaPublicKey(keyPair.publicKey) + val rpc = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) + + // when + val airdropResponse = rpc.requestAirdrop(pubkey, 0.1f) + val response = rpc.getSignatureStatuses(listOf(airdropResponse.result!!)) + + // then + assertNull(response.error) + assertNotNull(response.result) + assertTrue { response.result!!.size == 1 } + } + + @Test + fun `getBalance returns account balance`() = runTest { + // given + val keyPair = Ed25519.generateKeyPair() + val pubkey = SolanaPublicKey(keyPair.publicKey) + val rpc = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) + val balance = 10000000L + + // when + val airdropResponse = rpc.requestAirdrop(pubkey, 0.01f) + + withContext(Dispatchers.Default.limitedParallelism(1)) { + rpc.confirmTransaction(airdropResponse.result!!) + } + + val response = rpc.getBalance(pubkey) + + // then + assertNull(response.error) + assertNotNull(response.result) + assertEquals(balance, response.result) + } + + @Test + fun `sendTransaction returns transaction signature`() = runTest { + // given + val keyPair = Ed25519.generateKeyPair() + val pubkey = SolanaPublicKey(keyPair.publicKey) + val rpc = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) + 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(buildMemoTransaction(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) + assertTrue { response.result!!.isNotEmpty() } + } + + private fun buildMemoTransaction(address: SolanaPublicKey, memo: String) = + TransactionInstruction(SolanaPublicKey.from("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"), + listOf(AccountMeta(address, true, true)), + memo.encodeToByteArray() + ) } \ No newline at end of file From f226b6b7da2eedb96361d8af44a50c954414586e Mon Sep 17 00:00:00 2001 From: funkatronics Date: Fri, 7 Jun 2024 11:43:30 -0600 Subject: [PATCH 4/8] maybe fix ci --- ktordriver/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktordriver/build.gradle.kts b/ktordriver/build.gradle.kts index 8a32264..4b0594a 100644 --- a/ktordriver/build.gradle.kts +++ b/ktordriver/build.gradle.kts @@ -32,7 +32,7 @@ kotlin { implementation(project(mapOf("path" to ":rpccore"))) implementation(libs.kotlinx.coroutines.core) api(libs.ktor.client.core) - implementation(libs.ktor.client.cio) + api(libs.ktor.client.cio) } } val commonTest by getting { From 974c4a99d2187ce535f46a161a2c9f5da2258bcc Mon Sep 17 00:00:00 2001 From: funkatronics Date: Fri, 7 Jun 2024 11:56:43 -0600 Subject: [PATCH 5/8] remove stop step --- .github/workflows/build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e282e71..23354d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,9 +43,6 @@ jobs: - name: Test run: ./gradlew build -PlocalValidator=true - - name: Stop local validator - run: kill %1 - - name: Save Test Reports if: failure() uses: actions/upload-artifact@v3 From 7f5e203594f44204899e2f0d189dd234f40ba38e Mon Sep 17 00:00:00 2001 From: funkatronics Date: Fri, 7 Jun 2024 12:45:42 -0600 Subject: [PATCH 6/8] more fiddling --- settings.gradle.kts | 2 +- {solana => solanaclient}/build.gradle.kts | 0 .../kotlin/com/solana/rpc/SolanaRpcClient.kt | 61 +++++++++++-------- .../serializers/SolanaResponseSerializer.kt | 0 .../kotlin/com/solana/rpc/RpcClientTests.kt | 2 +- 5 files changed, 36 insertions(+), 29 deletions(-) rename {solana => solanaclient}/build.gradle.kts (100%) rename {solana => solanaclient}/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt (69%) rename {solana => solanaclient}/src/commonMain/kotlin/com/solana/serializers/SolanaResponseSerializer.kt (100%) rename {solana => solanaclient}/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt (98%) diff --git a/settings.gradle.kts b/settings.gradle.kts index 9272dd5..e1e0baa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,5 +18,5 @@ rootProject.name = "Rpc Core" include(":rpccore") include(":okiodriver") include(":ktordriver") -include(":solana") +include(":solanaclient") diff --git a/solana/build.gradle.kts b/solanaclient/build.gradle.kts similarity index 100% rename from solana/build.gradle.kts rename to solanaclient/build.gradle.kts diff --git a/solana/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt b/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt similarity index 69% rename from solana/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt rename to solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt index 0dcac88..a983235 100644 --- a/solana/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt +++ b/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt @@ -15,6 +15,7 @@ import kotlinx.serialization.builtins.nullable import kotlinx.serialization.builtins.serializer import kotlinx.serialization.json.* import kotlin.math.pow +import kotlin.random.Random import kotlin.time.Duration.Companion.seconds import kotlin.time.TimeSource @@ -22,31 +23,31 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { constructor(url: String, networkDriver: HttpNetworkDriver): this(Rpc20Driver(url, networkDriver)) - suspend fun requestAirdrop(address: SolanaPublicKey, amountSol: Float) = + suspend fun requestAirdrop(address: SolanaPublicKey, amountSol: Float, requestId: String? = null) = rpcDriver.makeRequest( - AirdropRequest(address, (amountSol*10f.pow(9)).toLong()), + AirdropRequest(address, (amountSol*10f.pow(9)).toLong(), requestId), String.serializer() ) - suspend fun getBalance(address: SolanaPublicKey, commitment: String = "confirmed") = - rpcDriver.makeRequest(BalanceRequest(address, commitment), SolanaResponseSerializer(Long.serializer())) + suspend fun getBalance(address: SolanaPublicKey, commitment: String = "confirmed", requestId: String? = null) = + rpcDriver.makeRequest(BalanceRequest(address, commitment, requestId), SolanaResponseSerializer(Long.serializer())) - suspend fun getMinBalanceForRentExemption(size: Long, commitment: String? = null) = - rpcDriver.makeRequest(RentExemptBalanceRequest(size, commitment), Long.serializer()) + suspend fun getMinBalanceForRentExemption(size: Long, commitment: String? = null, requestId: String? = null) = + rpcDriver.makeRequest(RentExemptBalanceRequest(size, commitment, requestId), Long.serializer()) - suspend fun getLatestBlockhash() = - rpcDriver.makeRequest(LatestBlockhashRequest(), SolanaResponseSerializer(BlockhashResponse.serializer())) + suspend fun getLatestBlockhash(commitment: String? = null, minContextSlot: Long? = null, requestId: String? = null) = + rpcDriver.makeRequest(LatestBlockhashRequest(commitment, minContextSlot, requestId), SolanaResponseSerializer(BlockhashResponse.serializer())) - suspend fun sendTransaction(transaction: Transaction) = - rpcDriver.makeRequest(SendTransactionRequest(transaction), String.serializer()) + suspend fun sendTransaction(transaction: Transaction, skipPreflight: Boolean = false, requestId: String? = null) = + rpcDriver.makeRequest(SendTransactionRequest(transaction, skipPreflight, requestId), String.serializer()) suspend fun sendAndConfirmTransaction(transaction: Transaction) = sendTransaction(transaction).apply { result?.let { confirmTransaction(it) } } - suspend fun getSignatureStatuses(signatures: List) = - rpcDriver.makeRequest(SignatureStatusesRequest(signatures), + suspend fun getSignatureStatuses(signatures: List, searchTransactionHistory: Boolean = false, requestId: String? = null) = + rpcDriver.makeRequest(SignatureStatusesRequest(signatures, searchTransactionHistory, requestId), SolanaResponseSerializer(ListSerializer(SignatureStatus.serializer().nullable)) ) @@ -67,7 +68,7 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { // wait a bit before retrying val mark = timeSource.markNow() var inc = 0 - while(timeSource.markNow() - mark < 0.3.seconds && isActive) { inc++ } + while(mark.elapsedNow() < 0.3.seconds && isActive) { inc++ } if (!isActive) break // breakout after timeout } @@ -76,8 +77,11 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { } //region Requests - class AirdropRequest(address: SolanaPublicKey, lamports: Long, requestId: String = "1") - : JsonRpc20Request( + sealed class SolanaRpcRequest(method: String, params: JsonElement?, id: String? = null) + : JsonRpc20Request(method, params, id ?: "$method-${Random.nextInt(100000000, 999999999)}") + + class AirdropRequest(address: SolanaPublicKey, lamports: Long, requestId: String? = null) + : SolanaRpcRequest( method = "requestAirdrop", params = buildJsonArray { add(address.base58()) @@ -86,8 +90,8 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { id = requestId ) - class BalanceRequest(address: SolanaPublicKey, commitment: String = "confirmed", requestId: String = "1") - : JsonRpc20Request( + class BalanceRequest(address: SolanaPublicKey, commitment: String = "confirmed", requestId: String? = null) + : SolanaRpcRequest( method = "getBalance", params = buildJsonArray { add(address.base58()) @@ -98,19 +102,22 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { requestId ) - class LatestBlockhashRequest(commitment: String = "confirmed", requestId: String = "1") - : JsonRpc20Request( + class LatestBlockhashRequest(commitment: String? = null, minContextSlot: Long? = null, requestId: String? = null) + : SolanaRpcRequest( method = "getLatestBlockhash", params = buildJsonArray { - addJsonObject { - put("commitment", commitment) + if (commitment != null || minContextSlot!= null) { + addJsonObject { + commitment?.let { put("commitment", commitment) } + minContextSlot?.let { put("minContextSlot", minContextSlot) } + } } }, requestId ) - class SendTransactionRequest(transaction: Transaction, skipPreflight: Boolean = true, requestId: String = "1") - : JsonRpc20Request( + class SendTransactionRequest(transaction: Transaction, skipPreflight: Boolean = false, requestId: String? = null) + : SolanaRpcRequest( method = "sendTransaction", params = buildJsonArray { add(Base58.encodeToString(transaction.serialize())) @@ -121,8 +128,8 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { requestId ) - class SignatureStatusesRequest(transactionIds: List, searchTransactionHistory: Boolean = false, requestId: String = "1") - : JsonRpc20Request( + class SignatureStatusesRequest(transactionIds: List, searchTransactionHistory: Boolean = false, requestId: String? = null) + : SolanaRpcRequest( method = "getSignatureStatuses", params = buildJsonArray { addJsonArray { transactionIds.forEach { add(it) } } @@ -133,8 +140,8 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { requestId ) - class RentExemptBalanceRequest(size: Long, commitment: String? = null, requestId: String = "1") - : JsonRpc20Request( + class RentExemptBalanceRequest(size: Long, commitment: String? = null, requestId: String? = null) + : SolanaRpcRequest( method = "getMinimumBalanceForRentExemption", params = buildJsonArray { add(size) diff --git a/solana/src/commonMain/kotlin/com/solana/serializers/SolanaResponseSerializer.kt b/solanaclient/src/commonMain/kotlin/com/solana/serializers/SolanaResponseSerializer.kt similarity index 100% rename from solana/src/commonMain/kotlin/com/solana/serializers/SolanaResponseSerializer.kt rename to solanaclient/src/commonMain/kotlin/com/solana/serializers/SolanaResponseSerializer.kt diff --git a/solana/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt b/solanaclient/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt similarity index 98% rename from solana/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt rename to solanaclient/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt index 78b1b22..f30c25b 100644 --- a/solana/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt +++ b/solanaclient/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt @@ -104,7 +104,7 @@ class RpcClientTests { Transaction(listOf(sig), this) } - val response = rpc.sendTransaction(transaction) + val response = rpc.sendTransaction(transaction, skipPreflight = true) // then assertNull(airdropResponse.error) From 2af6dd9b5765359ca5c1acbbf9dd9f275e71d301 Mon Sep 17 00:00:00 2001 From: funkatronics Date: Fri, 7 Jun 2024 13:45:40 -0600 Subject: [PATCH 7/8] transaction options and better confirmation --- .../kotlin/com/solana/rpc/SolanaRpcClient.kt | 218 ++++++++++++------ .../com/solana/rpc/TransactionOptions.kt | 40 ++++ .../kotlin/com/solana/rpc/RpcClientTests.kt | 6 +- 3 files changed, 189 insertions(+), 75 deletions(-) create mode 100644 solanaclient/src/commonMain/kotlin/com/solana/rpc/TransactionOptions.kt diff --git a/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt b/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt index a983235..b2f0e37 100644 --- a/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt +++ b/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt @@ -5,83 +5,140 @@ import com.solana.networking.HttpNetworkDriver import com.solana.networking.Rpc20Driver import com.solana.publickey.SolanaPublicKey import com.solana.rpccore.JsonRpc20Request +import com.solana.rpccore.RpcRequest import com.solana.serializers.SolanaResponseSerializer import com.solana.transaction.Transaction +import kotlinx.coroutines.delay 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.json.* +import kotlinx.serialization.serializer import kotlin.math.pow import kotlin.random.Random -import kotlin.time.Duration.Companion.seconds -import kotlin.time.TimeSource -class SolanaRpcClient(val rpcDriver: Rpc20Driver) { +class SolanaRpcClient( + val rpcDriver: Rpc20Driver, + private val defaultTransactionOptions: TransactionOptions = TransactionOptions() +) { - constructor(url: String, networkDriver: HttpNetworkDriver): this(Rpc20Driver(url, networkDriver)) + constructor( + url: String, networkDriver: HttpNetworkDriver, + defaultTransactionOptions: TransactionOptions = TransactionOptions() + ) : this(Rpc20Driver(url, networkDriver), defaultTransactionOptions) + + suspend inline fun makeRequest(request: RpcRequest, serializer: KSerializer) = + rpcDriver.makeRequest(request, serializer) + + suspend inline fun makeRequest(request: RpcRequest) = + rpcDriver.makeRequest(request, serializer()) suspend fun requestAirdrop(address: SolanaPublicKey, amountSol: Float, requestId: String? = null) = - rpcDriver.makeRequest( - AirdropRequest(address, (amountSol*10f.pow(9)).toLong(), requestId), + makeRequest( + AirdropRequest(address, (amountSol * 10f.pow(9)).toLong(), requestId), String.serializer() ) - suspend fun getBalance(address: SolanaPublicKey, commitment: String = "confirmed", requestId: String? = null) = - rpcDriver.makeRequest(BalanceRequest(address, commitment, requestId), SolanaResponseSerializer(Long.serializer())) - - suspend fun getMinBalanceForRentExemption(size: Long, commitment: String? = null, requestId: String? = null) = - rpcDriver.makeRequest(RentExemptBalanceRequest(size, commitment, requestId), Long.serializer()) - - suspend fun getLatestBlockhash(commitment: String? = null, minContextSlot: Long? = null, requestId: String? = null) = - rpcDriver.makeRequest(LatestBlockhashRequest(commitment, minContextSlot, requestId), SolanaResponseSerializer(BlockhashResponse.serializer())) + suspend fun getBalance( + address: SolanaPublicKey, + commitment: Commitment = Commitment.CONFIRMED, + requestId: String? = null + ) = makeRequest( + BalanceRequest(address, commitment, requestId), + SolanaResponseSerializer(Long.serializer()) + ) - suspend fun sendTransaction(transaction: Transaction, skipPreflight: Boolean = false, requestId: String? = null) = - rpcDriver.makeRequest(SendTransactionRequest(transaction, skipPreflight, requestId), String.serializer()) + suspend fun getMinBalanceForRentExemption( + size: Long, + commitment: Commitment? = null, + requestId: String? = null + ) = makeRequest(RentExemptBalanceRequest(size, commitment, requestId), Long.serializer()) + + suspend fun getLatestBlockhash( + commitment: Commitment? = null, + minContextSlot: Long? = null, + requestId: String? = null + ) = makeRequest( + LatestBlockhashRequest(commitment, minContextSlot, requestId), + SolanaResponseSerializer(BlockhashResponse.serializer()) + ) - suspend fun sendAndConfirmTransaction(transaction: Transaction) = - sendTransaction(transaction).apply { - result?.let { confirmTransaction(it) } - } + suspend fun sendTransaction( + transaction: Transaction, + skipPreflight: Boolean = false, + requestId: String? = null + ) = makeRequest(SendTransactionRequest(transaction, skipPreflight, requestId), String.serializer()) + + suspend fun sendAndConfirmTransaction( + transaction: Transaction, + options: TransactionOptions = defaultTransactionOptions + ) = sendTransaction(transaction).apply { + result?.let { confirmTransaction(it, options) } + } - suspend fun getSignatureStatuses(signatures: List, searchTransactionHistory: Boolean = false, requestId: String? = null) = - rpcDriver.makeRequest(SignatureStatusesRequest(signatures, searchTransactionHistory, requestId), - SolanaResponseSerializer(ListSerializer(SignatureStatus.serializer().nullable)) - ) + suspend fun getSignatureStatuses( + signatures: List, + searchTransactionHistory: Boolean = false, + requestId: String? = null + ) = makeRequest( + SignatureStatusesRequest(signatures, searchTransactionHistory, requestId), + 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() - - val timeSource = TimeSource.Monotonic - - // wait for desired transaction status - while(getStatus()?.confirmationStatus != commitment) { - - // wait a bit before retrying - val mark = timeSource.markNow() - var inc = 0 - while(mark.elapsedNow() < 0.3.seconds && isActive) { inc++ } + transactionSignature: String, + options: TransactionOptions = defaultTransactionOptions + ): Result = + withTimeout(options.timeout) { + val requiredCommitment = options.commitment.ordinal + + suspend fun confirmationStatus() = + getSignatureStatuses(listOf(transactionSignature), false) + .result?.first() + + // wait for desired transaction status + var inc = 1L + while (true) { + val confirmationOrdinal = confirmationStatus().also { + it?.err?.let { error -> + return@withTimeout Result.failure(Error(error.toString())) + } + }?.confirmationStatus?.ordinal ?: -1 + + if (confirmationOrdinal >= requiredCommitment) { + return@withTimeout Result.success(true) + } else { + // Exponential delay before retrying. + delay(500 * inc) + } + // breakout after timeout + if (!isActive) break + inc++ + } - if (!isActive) break // breakout after timeout + return@withTimeout Result.success(isActive) } - Result.success(signature) - } - //region Requests - sealed class SolanaRpcRequest(method: String, params: JsonElement?, id: String? = null) - : JsonRpc20Request(method, params, id ?: "$method-${Random.nextInt(100000000, 999999999)}") + sealed class SolanaRpcRequest( + method: String, + params: JsonElement?, + id: String? = null + ) : JsonRpc20Request( + method, + params, + id ?: "$method-${Random.nextInt(100000000, 999999999)}" + ) - class AirdropRequest(address: SolanaPublicKey, lamports: Long, requestId: String? = null) - : SolanaRpcRequest( + class AirdropRequest( + address: SolanaPublicKey, + lamports: Long, + requestId: String? = null + ) : SolanaRpcRequest( method = "requestAirdrop", params = buildJsonArray { add(address.base58()) @@ -90,25 +147,31 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { id = requestId ) - class BalanceRequest(address: SolanaPublicKey, commitment: String = "confirmed", requestId: String? = null) - : SolanaRpcRequest( + class BalanceRequest( + address: SolanaPublicKey, + commitment: Commitment = Commitment.CONFIRMED, + requestId: String? = null + ) : SolanaRpcRequest( method = "getBalance", params = buildJsonArray { add(address.base58()) addJsonObject { - put("commitment", commitment) + put("commitment", commitment.value) } }, requestId ) - class LatestBlockhashRequest(commitment: String? = null, minContextSlot: Long? = null, requestId: String? = null) - : SolanaRpcRequest( + class LatestBlockhashRequest( + commitment: Commitment? = null, + minContextSlot: Long? = null, + requestId: String? = null + ) : SolanaRpcRequest( method = "getLatestBlockhash", params = buildJsonArray { - if (commitment != null || minContextSlot!= null) { + if (commitment != null || minContextSlot != null) { addJsonObject { - commitment?.let { put("commitment", commitment) } + commitment?.let { put("commitment", commitment.value) } minContextSlot?.let { put("minContextSlot", minContextSlot) } } } @@ -116,20 +179,26 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { requestId ) - class SendTransactionRequest(transaction: Transaction, skipPreflight: Boolean = false, requestId: String? = null) - : SolanaRpcRequest( - method = "sendTransaction", - params = buildJsonArray { - add(Base58.encodeToString(transaction.serialize())) - addJsonObject { - put("skipPreflight", skipPreflight) - } - }, - requestId - ) + class SendTransactionRequest( + transaction: Transaction, + skipPreflight: Boolean = false, + requestId: String? = null + ) : SolanaRpcRequest( + method = "sendTransaction", + params = buildJsonArray { + add(Base58.encodeToString(transaction.serialize())) + addJsonObject { + put("skipPreflight", skipPreflight) + } + }, + requestId + ) - class SignatureStatusesRequest(transactionIds: List, searchTransactionHistory: Boolean = false, requestId: String? = null) - : SolanaRpcRequest( + class SignatureStatusesRequest( + transactionIds: List, + searchTransactionHistory: Boolean = false, + requestId: String? = null + ) : SolanaRpcRequest( method = "getSignatureStatuses", params = buildJsonArray { addJsonArray { transactionIds.forEach { add(it) } } @@ -140,14 +209,17 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { requestId ) - class RentExemptBalanceRequest(size: Long, commitment: String? = null, requestId: String? = null) - : SolanaRpcRequest( + class RentExemptBalanceRequest( + size: Long, + commitment: Commitment? = null, + requestId: String? = null + ) : SolanaRpcRequest( method = "getMinimumBalanceForRentExemption", params = buildJsonArray { add(size) commitment?.let { addJsonObject { - put("commitment", commitment) + put("commitment", commitment.value) } } }, @@ -167,7 +239,7 @@ class SolanaRpcClient(val rpcDriver: Rpc20Driver) { val slot: Long, val confirmations: Long?, var err: JsonObject?, - var confirmationStatus: String? + var confirmationStatus: Commitment? ) //endregion } \ No newline at end of file diff --git a/solanaclient/src/commonMain/kotlin/com/solana/rpc/TransactionOptions.kt b/solanaclient/src/commonMain/kotlin/com/solana/rpc/TransactionOptions.kt new file mode 100644 index 0000000..286017d --- /dev/null +++ b/solanaclient/src/commonMain/kotlin/com/solana/rpc/TransactionOptions.kt @@ -0,0 +1,40 @@ +package com.solana.rpc + +import kotlinx.serialization.SerialName +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +enum class Encoding(private val enc: String) { + base64("base64"), + base58("base58"), + jsonParsed("jsonParsed"); + fun getEncoding(): String { + return enc + } +} + +enum class Commitment(val value: String) { + @SerialName("processed") + PROCESSED("processed"), + + @SerialName("confirmed") + CONFIRMED("confirmed"), + + @SerialName("finalized") + FINALIZED("finalized"), + + @SerialName("max") + MAX("max"); + + override fun toString(): String { + return value + } +} + +data class TransactionOptions( + val commitment: Commitment = Commitment.FINALIZED, + val encoding: Encoding = Encoding.base64, + val skipPreflight: Boolean = false, + val preflightCommitment: Commitment = commitment, + val timeout: Duration = 30.seconds +) \ No newline at end of file diff --git a/solanaclient/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt b/solanaclient/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt index f30c25b..530ca9a 100644 --- a/solanaclient/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt +++ b/solanaclient/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt @@ -66,14 +66,16 @@ class RpcClientTests { // given val keyPair = Ed25519.generateKeyPair() val pubkey = SolanaPublicKey(keyPair.publicKey) - val rpc = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) + val rpc = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver(), + TransactionOptions(commitment = Commitment.CONFIRMED)) val balance = 10000000L // when val airdropResponse = rpc.requestAirdrop(pubkey, 0.01f) withContext(Dispatchers.Default.limitedParallelism(1)) { - rpc.confirmTransaction(airdropResponse.result!!) + rpc.confirmTransaction(airdropResponse.result!!, + TransactionOptions(commitment = Commitment.CONFIRMED)) } val response = rpc.getBalance(pubkey) From 4dd1882c3198795bdb982e14ee03a0c29f3fcabf Mon Sep 17 00:00:00 2001 From: funkatronics Date: Fri, 7 Jun 2024 13:59:20 -0600 Subject: [PATCH 8/8] remove max confirmation (deprecated) --- .../commonMain/kotlin/com/solana/rpc/TransactionOptions.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/solanaclient/src/commonMain/kotlin/com/solana/rpc/TransactionOptions.kt b/solanaclient/src/commonMain/kotlin/com/solana/rpc/TransactionOptions.kt index 286017d..a7a20e3 100644 --- a/solanaclient/src/commonMain/kotlin/com/solana/rpc/TransactionOptions.kt +++ b/solanaclient/src/commonMain/kotlin/com/solana/rpc/TransactionOptions.kt @@ -21,10 +21,7 @@ enum class Commitment(val value: String) { CONFIRMED("confirmed"), @SerialName("finalized") - FINALIZED("finalized"), - - @SerialName("max") - MAX("max"); + FINALIZED("finalized"); override fun toString(): String { return value