diff --git a/solanaclient/src/commonMain/kotlin/com/solana/rpc/JsonParsed.kt b/solanaclient/src/commonMain/kotlin/com/solana/rpc/JsonParsed.kt new file mode 100644 index 0000000..7089dd4 --- /dev/null +++ b/solanaclient/src/commonMain/kotlin/com/solana/rpc/JsonParsed.kt @@ -0,0 +1,84 @@ +package com.solana.rpc + +import com.funkatronics.encoders.Base58 +import com.solana.publickey.SolanaPublicKey +import com.solana.transaction.Blockhash +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +@Serializable +data class TransactionMetadata( + val err: Map? = null, + val fee: ULong, + val computeUnitsConsumed: ULong, + val preBalances: List, + val postBalances: List, + val innerInstructions: List, + val preTokenBalances: List, + val postTokenBalances: List, + val logMessages: List, + val rewards: List, +) + +@Serializable +data class Instruction( + val index: Int, + val instructions: List +) + +@Serializable +data class InnerInstruction( + val programIdIndex: Int, + val accounts: List, + @SerialName("data") val base58EncodedData: String +) { + val rawData = Base58.decode(base58EncodedData) +} + +@Serializable +data class TokenBalance( + val accountIndex: Int, + val mint: SolanaPublicKey, + val owner: SolanaPublicKey? = null, + val programId: SolanaPublicKey? = null, + val uiTokenAmount: UiTokenAmount +) + +@Serializable +data class UiTokenAmount( + val amount: ULong, + val decimals: Byte, + @SerialName("uiAmountString") val uiAmount: String +) + +@Serializable +data class JsonParsedTransaction( + val message: JsonParsedMessage, + val signatures: List +) + +@Serializable +data class JsonParsedMessage( + val accountKeys: List, + val instructions: List, + val recentBlockhash: Blockhash +) + +@Serializable +data class JsonParsedInstruction( + val program: String? = null, + val programId: SolanaPublicKey, + val stackHeight: ULong? = null, + val parsed: JsonElement? = null, +) + +@Serializable +data class JsonParsedAccountMeta( + @SerialName("pubkey") val publicKey: SolanaPublicKey, + val source: String, + @SerialName("signer") val isSigner: Boolean, + @SerialName("writable") val isWritable: Boolean, +) \ No newline at end of file diff --git a/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt b/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt index 5d2c4bb..d919062 100644 --- a/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt +++ b/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcClient.kt @@ -4,7 +4,9 @@ import com.solana.networking.HttpNetworkDriver import com.solana.networking.Rpc20Driver import com.solana.publickey.SolanaPublicKey import com.solana.rpccore.RpcRequest +import com.solana.serialization.AnchorDiscriminatorSerializer import com.solana.serializers.SolanaResponseDeserializer +import com.solana.transaction.Message import com.solana.transaction.Transaction import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -116,6 +118,15 @@ class SolanaRpcClient( SolanaResponseDeserializer(SimulationResult.serializer()) ) + suspend fun getFeeForMessage( + message: Message, + commitment: Commitment = Commitment.PROCESSED, + minContextSlot: Long? = null, + ) = makeRequest( + GetFeeForMessageRequest(message, commitment, minContextSlot), + SolanaResponseDeserializer(ULong.serializer()) + ) + suspend fun sendTransaction( transaction: Transaction, options: TransactionOptions = defaultTransactionOptions, @@ -162,6 +173,15 @@ class SolanaRpcClient( return@withTimeout Result.success(isActive) } + suspend fun getTransaction( + transactionSignature: String, + commitment: Commitment = Commitment.FINALIZED, + maxSupportedTransactionVersion: Long = 0 + ) = makeRequest( + GetTransactionRequest(transactionSignature, commitment, maxSupportedTransactionVersion), + TransactionDetails.serializer().nullable + ) + internal suspend inline fun makeRequest(request: RpcRequest, serializer: DeserializationStrategy) = rpcDriver.makeRequest(request, serializer) @@ -189,6 +209,26 @@ suspend inline fun SolanaRpcClient.getAccountInfo( requestId: String? = null ) = getAccountInfo(serializer(), publicKey, commitment, minContextSlot, dataSlice, requestId) +suspend inline fun SolanaRpcClient.getAnchorAccountInfo( + accountName: String, + publicKey: SolanaPublicKey, + commitment: Commitment? = null, + minContextSlot: Long? = null, + dataSlice: AccountRequest.DataSlice? = null, + requestId: String? = null +) = getAccountInfo( + AnchorDiscriminatorSerializer( + "account", + accountName, + serializer() + ), + publicKey, + commitment, + minContextSlot, + dataSlice, + requestId +) + suspend fun SolanaRpcClient.getMultipleAccounts( deserializer: KSerializer, publicKeys: List, diff --git a/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcRequest.kt b/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcRequest.kt index 9b776c4..9881848 100644 --- a/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcRequest.kt +++ b/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcRequest.kt @@ -5,6 +5,7 @@ import com.funkatronics.encoders.Base64 import com.funkatronics.hash.Sha256 import com.solana.publickey.SolanaPublicKey import com.solana.rpccore.JsonRpc20Request +import com.solana.transaction.Message import com.solana.transaction.Transaction import kotlinx.serialization.Serializable import kotlinx.serialization.json.* @@ -232,6 +233,37 @@ class RentExemptBalanceRequest( requestId ) +class GetTransactionRequest( + signature: String, + commitment: Commitment = Commitment.FINALIZED, + maxSupportedTransactionVersion: Long = 0, + requestId: String? = null +) : SolanaRpcRequest( + method = "getTransaction", + params = { add(signature) }, + configuration = { + put("commitment", commitment.serialName()) + put("maxSupportedTransactionVersion", maxSupportedTransactionVersion) + put("encoding", "jsonParsed") + }, + requestId +) + +class GetFeeForMessageRequest( + message: Message, + commitment: Commitment = Commitment.PROCESSED, + minContextSlot: Long? = null, + requestId: String? = null +) : SolanaRpcRequest( + method = "getFeeForMessage", + params = { add(message.serialize().run { Base64.encodeToString(this) }) }, + configuration = { + put("commitment", commitment.serialName()) + put("minContextSlot", minContextSlot) + }, + requestId +) + class SimulateTransactionRequest( transaction: Transaction, commitment: Commitment? = null, diff --git a/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcResponse.kt b/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcResponse.kt index 5ba6d8c..3f77766 100644 --- a/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcResponse.kt +++ b/solanaclient/src/commonMain/kotlin/com/solana/rpc/SolanaRpcResponse.kt @@ -24,6 +24,15 @@ data class SignatureStatus( var confirmationStatus: Commitment? ) +@Serializable +data class TransactionDetails( + val blockTime: Long, + val meta: TransactionMetadata, + val slot: ULong, + val transaction: JsonParsedTransaction, + val version: JsonElement? = null +) + @Serializable data class SimulationResult( val accounts: List>?>? = null, diff --git a/solanaclient/src/commonTest/kotlin/com/solana/rpc/GetFeeForMessageTests.kt b/solanaclient/src/commonTest/kotlin/com/solana/rpc/GetFeeForMessageTests.kt new file mode 100644 index 0000000..18f3055 --- /dev/null +++ b/solanaclient/src/commonTest/kotlin/com/solana/rpc/GetFeeForMessageTests.kt @@ -0,0 +1,42 @@ +package com.solana.rpc + +import com.solana.config.TestConfig +import com.solana.networking.KtorNetworkDriver +import com.solana.publickey.SolanaPublicKey +import com.solana.transaction.Message +import diglol.crypto.Ed25519 +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class GetFeeForMessageTests { + + @Test + fun `getFeeForMessage with basic options returns non-zero value`() = runTest { + // given + val keyPair = Ed25519.generateKeyPair() + val pubkey = SolanaPublicKey(keyPair.publicKey) + val rpc = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) + + // when + val blockhashResponse = rpc.getLatestBlockhash() + + val message = Message.Builder() + .setRecentBlockhash(blockhashResponse.result!!.blockhash) + .addInstruction(buildMemoTransaction(pubkey, "hello solana!")) + .build() + + val response = rpc.getFeeForMessage( + message, + commitment = Commitment.CONFIRMED, + minContextSlot = null, + ) + + // then + assertNull(response.error) + assertNotNull(response.result) + assertTrue { response.result!! > 0u } + } +} \ No newline at end of file diff --git a/solanaclient/src/commonTest/kotlin/com/solana/rpc/GetTransactionTest.kt b/solanaclient/src/commonTest/kotlin/com/solana/rpc/GetTransactionTest.kt new file mode 100644 index 0000000..8af7c2c --- /dev/null +++ b/solanaclient/src/commonTest/kotlin/com/solana/rpc/GetTransactionTest.kt @@ -0,0 +1,125 @@ +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 kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class GetTransactionTest { + + @Test + fun `GetTransaction returns transaction details`() = runTest { + // given + val keyPair = Ed25519.generateKeyPair() + val pubkey = SolanaPublicKey(keyPair.publicKey) + val programId = SolanaPublicKey.from("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") + val rpc = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) + val expectedParsedInstruction = JsonParsedInstruction( + program="spl-memo", + programId = programId, + stackHeight = 1u, + parsed = JsonPrimitive("hello world") + ) + + // when + rpc.requestAirdrop(pubkey, 0.1f) + val blockhashResponse = rpc.getLatestBlockhash() + val transaction = Message.Builder() + .setRecentBlockhash(blockhashResponse.result!!.blockhash) + .addInstruction(buildMemoTransaction(pubkey, "hello world")) + .build().run { + val sig = Ed25519.sign(keyPair, serialize()) + Transaction(listOf(sig), this) + } + + val txResponse = withContext(Dispatchers.Default.limitedParallelism(1)) { + rpc.sendAndConfirmTransaction(transaction, TransactionOptions( + commitment = Commitment.CONFIRMED, + skipPreflight = true + )) + } + val response = rpc.getTransaction(txResponse.result!!, Commitment.CONFIRMED) + + println(response.result) + + // then + assertNull(response.error) + assertNotNull(response.result) + assertNotNull(response.result!!.meta) + assertNull(response.result!!.meta.err) + response.result!!.transaction.message.instructions.first().apply { + assertEquals(expectedParsedInstruction.program, this.program) + assertEquals(expectedParsedInstruction.programId, this.programId) + assertEquals(expectedParsedInstruction.parsed, this.parsed) + assertEquals(expectedParsedInstruction.stackHeight, this.stackHeight ?: 1u) + } + } + + @Test + fun `GetTransaction returns transaction error`() = runTest { + // given + val keyPair = Ed25519.generateKeyPair() + val pubkey = SolanaPublicKey(keyPair.publicKey) + val programId = SolanaPublicKey.from("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") + val rpc = SolanaRpcClient(TestConfig.RPC_URL, KtorNetworkDriver()) + val expectedParsedInstruction = JsonParsedInstruction( + program="spl-memo", + programId = programId, + stackHeight = 1u, + parsed = JsonPrimitive("hello world") + ) + + // when + rpc.requestAirdrop(pubkey, 0.1f) + val blockhashResponse = rpc.getLatestBlockhash() + val transaction = Message.Builder() + .setRecentBlockhash(blockhashResponse.result!!.blockhash) + .addInstruction(TransactionInstruction( + SolanaPublicKey.from("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"), + listOf( + AccountMeta(pubkey, true, true), + AccountMeta(programId, false, false) + ), + "hello world".encodeToByteArray() + ) + ) + .build().run { + val sig = Ed25519.sign(keyPair, serialize()) + Transaction(listOf(sig), this) + } + + val txResponse = withContext(Dispatchers.Default.limitedParallelism(1)) { + rpc.sendAndConfirmTransaction(transaction, TransactionOptions( + commitment = Commitment.CONFIRMED, + skipPreflight = true + )) + } + val response = rpc.getTransaction(txResponse.result!!, Commitment.CONFIRMED) + + println(response.result) + + // then + assertNull(response.error) + assertNotNull(response.result) + assertNotNull(response.result!!.meta) + assertNotNull(response.result!!.meta.err) + response.result!!.transaction.message.instructions.first().apply { + assertEquals(expectedParsedInstruction.program, this.program) + assertEquals(expectedParsedInstruction.programId, this.programId) + assertEquals(expectedParsedInstruction.parsed, this.parsed) + assertEquals(expectedParsedInstruction.stackHeight, this.stackHeight ?: 1u) + } + } +} \ 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 c19d41e..cc1b251 100644 --- a/solanaclient/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt +++ b/solanaclient/src/commonTest/kotlin/com/solana/rpc/RpcClientTests.kt @@ -437,10 +437,4 @@ class RpcClientTests { 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 diff --git a/solanaclient/src/commonTest/kotlin/com/solana/rpc/SimulateTransactionTests.kt b/solanaclient/src/commonTest/kotlin/com/solana/rpc/SimulateTransactionTests.kt index 4d7bddf..59fa08c 100644 --- a/solanaclient/src/commonTest/kotlin/com/solana/rpc/SimulateTransactionTests.kt +++ b/solanaclient/src/commonTest/kotlin/com/solana/rpc/SimulateTransactionTests.kt @@ -10,7 +10,6 @@ import com.solana.transaction.TransactionInstruction import com.solana.transaction.toUnsignedTransaction import diglol.crypto.Ed25519 import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext import kotlin.test.Test @@ -191,10 +190,4 @@ class SimulateTransactionTests { assertNotNull(response.result!!.logs) assertNull(response.result!!.err) } - - private fun buildMemoTransaction(address: SolanaPublicKey, memo: String) = - TransactionInstruction(SolanaPublicKey.from("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"), - listOf(AccountMeta(address, true, true)), - memo.encodeToByteArray() - ) } \ No newline at end of file diff --git a/solanaclient/src/commonTest/kotlin/com/solana/rpc/Util.kt b/solanaclient/src/commonTest/kotlin/com/solana/rpc/Util.kt new file mode 100644 index 0000000..884bec0 --- /dev/null +++ b/solanaclient/src/commonTest/kotlin/com/solana/rpc/Util.kt @@ -0,0 +1,11 @@ +package com.solana.rpc + +import com.solana.publickey.SolanaPublicKey +import com.solana.transaction.AccountMeta +import com.solana.transaction.TransactionInstruction + +fun buildMemoTransaction(address: SolanaPublicKey, memo: String) = + TransactionInstruction(SolanaPublicKey.from("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"), + listOf(AccountMeta(address, true, true)), + memo.encodeToByteArray() + ) \ No newline at end of file