From 7b9e7913ad1fe7435e44ce6a15ef13d4158bbe88 Mon Sep 17 00:00:00 2001 From: funkatronics Date: Thu, 8 Jan 2026 11:31:27 -0700 Subject: [PATCH] add compute budget program + tests --- .../solana/programs/ComputeBudgetProgram.kt | 89 ++++++++++++ .../programs/ComputeBudgetProgramTests.kt | 132 ++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 solana/src/commonMain/kotlin/com/solana/programs/ComputeBudgetProgram.kt create mode 100644 solana/src/commonTest/kotlin/com/solana/programs/ComputeBudgetProgramTests.kt diff --git a/solana/src/commonMain/kotlin/com/solana/programs/ComputeBudgetProgram.kt b/solana/src/commonMain/kotlin/com/solana/programs/ComputeBudgetProgram.kt new file mode 100644 index 0000000..a691b0d --- /dev/null +++ b/solana/src/commonMain/kotlin/com/solana/programs/ComputeBudgetProgram.kt @@ -0,0 +1,89 @@ +package com.solana.programs + +import com.funkatronics.kborsh.BorshEncoder +import com.solana.publickey.SolanaPublicKey +import com.solana.transaction.TransactionInstruction +import kotlin.jvm.JvmStatic + +object ComputeBudgetProgram : Program { + @JvmStatic + val PROGRAM_ID = SolanaPublicKey.from("ComputeBudget111111111111111111111111111111") + + private const val PROGRAM_INDEX_REQUEST_HEAP_FRAME = 1.toByte() + private const val PROGRAM_INDEX_SET_COMPUTE_UNIT_LIMIT = 2.toByte() + private const val PROGRAM_INDEX_SET_COMPUTE_UNIT_PRICE = 3.toByte() + private const val PROGRAM_INDEX_SET_LOADED_ACCOUNTS_DATA_SIZE_LIMIT = 4.toByte() + + /** + * Request a specific transaction-wide program heap region size in bytes. The value + * requested must be a multiple of 1024. This new heap region size applies to each + * program executed in the transaction, including all calls to CPIs. + * + * @param bytes the heap region size, in bytes + */ + @JvmStatic + fun requestHeapFrame( + bytes: UInt + ): TransactionInstruction = + TransactionInstruction(PROGRAM_ID, + listOf(), + BorshEncoder().apply { + encodeByte(PROGRAM_INDEX_REQUEST_HEAP_FRAME) + encodeInt(bytes.toInt()) + }.borshEncodedBytes + ) + + /** + * Set a specific compute unit limit that the transaction is allowed to consume. + * + * @param units the maximum compute units the that the transaction is allowed to consume + */ + @JvmStatic + fun setComputeUnitLimit( + units: UInt + ): TransactionInstruction = + TransactionInstruction(PROGRAM_ID, + listOf(), + BorshEncoder().apply { + encodeByte(PROGRAM_INDEX_SET_COMPUTE_UNIT_LIMIT) + encodeInt(units.toInt()) + }.borshEncodedBytes + ) + + /** + * Set a compute unit price in “micro-lamports” to pay a higher transaction fee for higher + * transaction prioritization. + * + * @param uLamports the micro-lamport unit price for the transaction + */ + @JvmStatic + fun setComputeUnitPrice( + uLamports: ULong + ): TransactionInstruction = + TransactionInstruction(PROGRAM_ID, + listOf(), + BorshEncoder().apply { + encodeByte(PROGRAM_INDEX_SET_COMPUTE_UNIT_PRICE) + encodeLong(uLamports.toLong()) + }.borshEncodedBytes + ) + + /** + * Set a specific transaction-wide account data size limit, in bytes, is allowed to load. + * + * @param bytes the account data size limit, in bytes + */ + @JvmStatic + fun setLoadedAccountsDataSizeLimit( + bytes: UInt + ): TransactionInstruction = + TransactionInstruction(PROGRAM_ID, + listOf(), + BorshEncoder().apply { + encodeByte(PROGRAM_INDEX_SET_LOADED_ACCOUNTS_DATA_SIZE_LIMIT) + encodeLong(bytes.toLong()) + }.borshEncodedBytes + ) + + override val programId = PROGRAM_ID +} \ No newline at end of file diff --git a/solana/src/commonTest/kotlin/com/solana/programs/ComputeBudgetProgramTests.kt b/solana/src/commonTest/kotlin/com/solana/programs/ComputeBudgetProgramTests.kt new file mode 100644 index 0000000..9a83ea8 --- /dev/null +++ b/solana/src/commonTest/kotlin/com/solana/programs/ComputeBudgetProgramTests.kt @@ -0,0 +1,132 @@ +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 diglol.crypto.Ed25519 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class ComputeBudgetProgramTests { + + @Test + fun `set Compute Limit and Price builds valid transaction`() = 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(MemoProgram.publishMemo(pubkey, message)) + .addInstruction(ComputeBudgetProgram.setComputeUnitLimit(25000u)) + .addInstruction(ComputeBudgetProgram.setComputeUnitPrice(10000u)) + .build().run { + val sig = Ed25519.sign(keyPair, serialize()) + Transaction(listOf(sig), this) + } + + val response = withContext(Dispatchers.Default.limitedParallelism(1)) { + rpc.sendAndConfirmTransaction( + transaction, TransactionOptions( + commitment = Commitment.CONFIRMED, + skipPreflight = true + ) + ) + } + + // then + assertNull(airdropResponse.error) + assertNotNull(airdropResponse.result) + assertNull(response.error) + assertNotNull(response.result) + } + + @Test + fun `request heap frame builds valid transaction`() = 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(MemoProgram.publishMemo(pubkey, message)) + .addInstruction(ComputeBudgetProgram.requestHeapFrame(40u*1024u)) + .build().run { + val sig = Ed25519.sign(keyPair, serialize()) + Transaction(listOf(sig), this) + } + + val response = withContext(Dispatchers.Default.limitedParallelism(1)) { + rpc.sendAndConfirmTransaction( + transaction, TransactionOptions( + commitment = Commitment.CONFIRMED, + skipPreflight = true + ) + ) + } + + // then + assertNull(airdropResponse.error) + assertNotNull(airdropResponse.result) + assertNull(response.error) + assertNotNull(response.result) + } + + @Test + fun `set Loaded Accounts data size limit builds valid transaction`() = 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(MemoProgram.publishMemo(pubkey, message)) + .addInstruction(ComputeBudgetProgram.setLoadedAccountsDataSizeLimit(300000u)) + .build().run { + val sig = Ed25519.sign(keyPair, serialize()) + Transaction(listOf(sig), this) + } + + val response = withContext(Dispatchers.Default.limitedParallelism(1)) { + rpc.sendAndConfirmTransaction( + transaction, TransactionOptions( + commitment = Commitment.CONFIRMED, + skipPreflight = true + ) + ) + } + + // then + assertNull(airdropResponse.error) + assertNotNull(airdropResponse.result) + assertNull(response.error) + assertNotNull(response.result) + } +} \ No newline at end of file