diff --git a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/methods/simulateTransaction.kt b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/methods/simulateTransaction.kt new file mode 100644 index 0000000..244d3c5 --- /dev/null +++ b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/methods/simulateTransaction.kt @@ -0,0 +1,124 @@ +package net.avianlabs.solana.methods + +import io.ktor.util.* +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.* +import net.avianlabs.solana.SolanaClient +import net.avianlabs.solana.client.Response +import net.avianlabs.solana.client.Response.RPC +import net.avianlabs.solana.domain.core.Commitment +import net.avianlabs.solana.domain.core.Transaction + +/** + * Simulate a transaction and return the result + * + * @param transaction Transaction to simulate + * @param commitment Commitment level to simulate the transaction at + * @param sigVerify if true the transaction signatures will be verified (conflicts with + * replaceRecentBlockhash) + * @param replaceRecentBlockhash if true the transaction recent blockhash will be replaced with the + * most recent blockhash. (conflicts with sigVerify) + * @param minContextSlot the minimum slot that the request can be evaluated at + * @param innerInstructions If true the response will include inner instructions. These inner + * instructions will be jsonParsed where possible, otherwise json. + * @param accounts Accounts configuration object + * + * @return Simulated transaction result + */ +public suspend fun SolanaClient.simulateTransaction( + transaction: Transaction, + commitment: Commitment? = null, + sigVerify: Boolean? = null, + replaceRecentBlockhash: Boolean? = null, + minContextSlot: ULong? = null, + innerInstructions: Boolean? = null, + accounts: List? = null, +): Response> = invoke( + method = "simulateTransaction", + params = buildJsonArray { + add(transaction.serialize().encodeBase64()) + addJsonObject { + put("encoding", "base64") + commitment?.let { put("commitment", it.value) } + sigVerify?.let { put("sigVerify", it) } + replaceRecentBlockhash?.let { put("replaceRecentBlockhash", it) } + minContextSlot?.let { put("minContextSlot", it.toString()) } + innerInstructions?.let { put("innerInstructions", it) } + accounts?.let { + putJsonObject("accounts") { + putJsonArray("addresses") { + it.forEach { add(it) } + } + put("encoding", "base58") + } + } + } + } +) + +@Serializable +public data class SimulateTransactionResponse( + /** + * Error if transaction failed, null if transaction succeeded. + */ + @Contextual val err: Any?, + /** + * Array of log messages the transaction instructions output during execution, null if simulation + * failed before the transaction was able to execute (for example due to an invalid blockhash or + * signature verification failure) + */ + val logs: List?, + /** + * Accounts requested if any + */ + val accounts: List?, + /** + * The number of compute budget units consumed during the processing of this transaction + */ + val unitsConsumed: ULong, + /** + * the most-recent return data generated by an instruction in the transaction + */ + val returnData: ReturnData?, + /** + * Defined only if innerInstructions was set to true + */ + @Contextual val innerInstructions: Any?, +) { + @Serializable + public data class AccountInfo( + /** + * Number of lamports assigned to this account + */ + val lamports: ULong, + /** + * Base-58 encoded Pubkey of the program this account has been assigned to + */ + val owner: String, + /** + * Data associated with the account, either as encoded binary data or JSON format + */ + @Contextual val data: Any, + /** + * Boolean indicating if the account contains a program (and is strictly read-only) + */ + val executable: Boolean, + /** + * The epoch at which this account will next owe rent + */ + val rentEpoch: ULong, + ) + + @Serializable + public data class ReturnData( + /** + * The program that generated the return data + */ + val programId: String, + /** + * The return data itself, as base-64 encoded binary data + */ + @Contextual val data: Any, + ) +} diff --git a/solana-kotlin/src/commonTest/kotlin/net/avianlabs/solana/domain/program/SystemProgramTest.kt b/solana-kotlin/src/commonTest/kotlin/net/avianlabs/solana/domain/program/SystemProgramTest.kt index 41fd5f8..1c991bd 100644 --- a/solana-kotlin/src/commonTest/kotlin/net/avianlabs/solana/domain/program/SystemProgramTest.kt +++ b/solana-kotlin/src/commonTest/kotlin/net/avianlabs/solana/domain/program/SystemProgramTest.kt @@ -1,5 +1,7 @@ package net.avianlabs.solana.domain.program +import io.ktor.client.* +import io.ktor.client.plugins.logging.* import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import net.avianlabs.solana.SolanaClient @@ -20,7 +22,13 @@ class SystemProgramTest { @Test @Ignore fun testCreateDurableNonceAccount() = runBlocking { - val client = SolanaClient(client = RpcKtorClient("http://localhost:8899")) + val client = + SolanaClient(client = RpcKtorClient("http://localhost:8899", httpClient = HttpClient { + install(Logging) { + level = LogLevel.ALL + logger = Logger.SIMPLE + } + })) val keypair = TweetNaCl.Signature.generateKey(Random.nextBytes(32)) println("Keypair: ${keypair.publicKey}") @@ -56,6 +64,10 @@ class SystemProgramTest { .sign(listOf(keypair, nonceAccount)) + val simulated = client.simulateTransaction(initTransaction) + + println("simulated: $simulated") + val initSignature = client.sendTransaction(initTransaction) println("Initialized nonce account: $initSignature")