Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions solanaclient/src/commonMain/kotlin/com/solana/rpc/JsonParsed.kt
Original file line number Diff line number Diff line change
@@ -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<String, JsonArray>? = null,
val fee: ULong,
val computeUnitsConsumed: ULong,
val preBalances: List<ULong>,
val postBalances: List<ULong>,
val innerInstructions: List<Instruction>,
val preTokenBalances: List<TokenBalance>,
val postTokenBalances: List<TokenBalance>,
val logMessages: List<String>,
val rewards: List<JsonObject>,
)

@Serializable
data class Instruction(
val index: Int,
val instructions: List<InnerInstruction>
)

@Serializable
data class InnerInstruction(
val programIdIndex: Int,
val accounts: List<Int>,
@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<String>
)

@Serializable
data class JsonParsedMessage(
val accountKeys: List<JsonParsedAccountMeta>,
val instructions: List<JsonParsedInstruction>,
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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 <T> makeRequest(request: RpcRequest, serializer: DeserializationStrategy<T>) =
rpcDriver.makeRequest(request, serializer)

Expand Down Expand Up @@ -189,6 +209,26 @@ suspend inline fun <reified D> SolanaRpcClient.getAccountInfo(
requestId: String? = null
) = getAccountInfo<D>(serializer(), publicKey, commitment, minContextSlot, dataSlice, requestId)

suspend inline fun <reified D> SolanaRpcClient.getAnchorAccountInfo(
accountName: String,
publicKey: SolanaPublicKey,
commitment: Commitment? = null,
minContextSlot: Long? = null,
dataSlice: AccountRequest.DataSlice? = null,
requestId: String? = null
) = getAccountInfo<D>(
AnchorDiscriminatorSerializer(
"account",
accountName,
serializer()
),
publicKey,
commitment,
minContextSlot,
dataSlice,
requestId
)

suspend fun <D> SolanaRpcClient.getMultipleAccounts(
deserializer: KSerializer<D>,
publicKeys: List<SolanaPublicKey>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccountInfo<List<String>>?>? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
}
Loading