Skip to content
Merged
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
15 changes: 15 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

---

## 0.1.7 - 2026-07-03

### Added

1. Added `Solace::Instructions::ComputeBudget::SetComputeUnitPriceInstruction` for setting a transaction's priority fee
2. Added `Solace::Instructions::ComputeBudget::SetComputeUnitLimitInstruction` for capping a transaction's compute units
3. Added `Solace::Composers::ComputeBudgetProgramSetComputeUnitPriceComposer`
4. Added `Solace::Composers::ComputeBudgetProgramSetComputeUnitLimitComposer`

### Changed

### Fixed

---

## 0.1.6 - 2026-06-22

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ more control. Every operation is reachable at more than one level.
messages (legacy and versioned), instructions, account context, and address lookup tables.
- **Building transactions** — instruction builders, composers, the transaction composer,
and program clients.
- **Programs** — the System program, SPL Token, Token-2022, and the Associated Token
Account program.
- **Programs** — the System program, SPL Token, Token-2022, the Associated Token
Account program, and Compute Budget.
- **Reference** — codecs, PDA derivation, Curve25519, constants, serialization, tokens, and
errors.

Expand Down
2 changes: 1 addition & 1 deletion gem/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
solace (0.1.6)
solace (0.1.7)
base58 (~> 0.2)
ffi (~> 1.15)
rbnacl (~> 7.0)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module Solace
module Composers
# Composer for creating a compute budget set compute unit limit instruction.
#
# This composer resolves and orders the required accounts for a `SetComputeUnitLimit`
# instruction, sets up their access permissions, and delegates construction to the
# appropriate instruction builder (`Instructions::ComputeBudget::SetComputeUnitLimitInstruction`).
#
# It is used for capping the compute units a transaction may consume.
#
# Required accounts:
# - **Program**: Compute Budget program (readonly, non-signer)
#
# @example Compose and build a set compute unit limit instruction
# composer = ComputeBudgetProgramSetComputeUnitLimitComposer.new(
# units: 200_000
# )
#
# @see Instructions::ComputeBudget::SetComputeUnitLimitInstruction
# @since 0.1.7
class ComputeBudgetProgramSetComputeUnitLimitComposer < Base
# Extracts the compute unit limit from the params
#
# @return [Integer] The compute unit limit
def units
params[:units]
end

# Returns the compute budget program id
#
# @return [String] The compute budget program id
def compute_budget_program
Constants::COMPUTE_BUDGET_PROGRAM_ID.to_s
end

# Setup accounts required for set compute unit limit instruction
# Called automatically during initialization
#
# @return [void]
def setup_accounts
account_context.add_readonly_nonsigner(compute_budget_program)
end

# Build instruction with resolved account indices
#
# @param account_context [Utils::AccountContext] The account context
# @return [Solace::Instruction]
def build_instruction(account_context)
Instructions::ComputeBudget::SetComputeUnitLimitInstruction.build(
units: units,
program_index: account_context.index_of(compute_budget_program)
)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module Solace
module Composers
# Composer for creating a compute budget set compute unit price instruction.
#
# This composer resolves and orders the required accounts for a `SetComputeUnitPrice`
# instruction, sets up their access permissions, and delegates construction to the
# appropriate instruction builder (`Instructions::ComputeBudget::SetComputeUnitPriceInstruction`).
#
# It is used for attaching a priority fee to a transaction.
#
# Required accounts:
# - **Program**: Compute Budget program (readonly, non-signer)
#
# @example Compose and build a set compute unit price instruction
# composer = ComputeBudgetProgramSetComputeUnitPriceComposer.new(
# micro_lamports: 50_000
# )
#
# @see Instructions::ComputeBudget::SetComputeUnitPriceInstruction
# @since 0.1.7
class ComputeBudgetProgramSetComputeUnitPriceComposer < Base
# Extracts the price per compute unit from the params
#
# @return [Integer] The price per compute unit (in micro-lamports)
def micro_lamports
params[:micro_lamports]
end

# Returns the compute budget program id
#
# @return [String] The compute budget program id
def compute_budget_program
Constants::COMPUTE_BUDGET_PROGRAM_ID.to_s
end

# Setup accounts required for set compute unit price instruction
# Called automatically during initialization
#
# @return [void]
def setup_accounts
account_context.add_readonly_nonsigner(compute_budget_program)
end

# Build instruction with resolved account indices
#
# @param account_context [Utils::AccountContext] The account context
# @return [Solace::Instruction]
def build_instruction(account_context)
Instructions::ComputeBudget::SetComputeUnitPriceInstruction.build(
micro_lamports: micro_lamports,
program_index: account_context.index_of(compute_budget_program)
)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

module Solace
module Instructions
module ComputeBudget
# Instruction for setting the compute unit limit.
#
# This instruction is used to set the maximum number of compute units a
# transaction may consume. Together with the compute unit price, it
# determines the priority fee the transaction pays.
#
# @example Build a SetComputeUnitLimit instruction
# instruction = Solace::Instructions::ComputeBudget::SetComputeUnitLimitInstruction.build(
# units: 200_000,
# program_index: 1
# )
#
# @since 0.1.7
class SetComputeUnitLimitInstruction
# @!attribute [Array<Integer>] INSTRUCTION_INDEX
# Instruction index for the Compute Budget Program's SetComputeUnitLimit instruction.
INSTRUCTION_INDEX = [2].freeze

# Builds a Solace::Instruction for setting the compute unit limit
#
# @param units [Integer] Maximum compute units the transaction may consume
# @param program_index [Integer] Index of the Compute Budget program in the transaction's accounts
# @return [Solace::Instruction]
def self.build(units:, program_index:)
Solace::Instruction.new.tap do |ix|
ix.program_index = program_index
ix.accounts = []
ix.data = data(units)
end
end

# Instruction data for a set compute unit limit instruction
#
# The BufferLayout is:
# - [Instruction Index (1 byte)]
# - [Compute unit limit (4 bytes little-endian u32)]
#
# @param units [Integer] Maximum compute units the transaction may consume
# @return [Array<Integer>] 1-byte instruction index + 4-byte limit
def self.data(units)
INSTRUCTION_INDEX + Solace::Utils::Codecs.encode_le_u32(units).bytes
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

module Solace
module Instructions
# The ComputeBudget module contains instruction builders for the Compute Budget Program.
#
# The Compute Budget program prices and provisions a transaction's execution. Its
# instructions take no accounts; each encodes a directive the runtime reads when
# scheduling and executing the transaction, such as the priority fee attached to it.
#
# This module contains classes that build the low-level instruction data required
# to interact with the Compute Budget Program.
#
# @see https://docs.solana.com/developing/programming-model/runtime#compute-budget
# @since 0.1.7
module ComputeBudget
# Instruction for setting the compute unit price.
#
# This instruction is used to set the price (in micro-lamports per compute unit)
# a transaction pays as a priority fee, which validators use to order it during
# congestion.
#
# @example Build a SetComputeUnitPrice instruction
# instruction = Solace::Instructions::ComputeBudget::SetComputeUnitPriceInstruction.build(
# micro_lamports: 50_000,
# program_index: 1
# )
#
# @since 0.1.7
class SetComputeUnitPriceInstruction
# @!attribute [Array<Integer>] INSTRUCTION_INDEX
# Instruction index for the Compute Budget Program's SetComputeUnitPrice instruction.
INSTRUCTION_INDEX = [3].freeze

# Builds a Solace::Instruction for setting the compute unit price
#
# @param micro_lamports [Integer] Price per compute unit (in micro-lamports)
# @param program_index [Integer] Index of the Compute Budget program in the transaction's accounts
# @return [Solace::Instruction]
def self.build(micro_lamports:, program_index:)
Solace::Instruction.new.tap do |ix|
ix.program_index = program_index
ix.accounts = []
ix.data = data(micro_lamports)
end
end

# Instruction data for a set compute unit price instruction
#
# The BufferLayout is:
# - [Instruction Index (1 byte)]
# - [Price (8 bytes little-endian u64)]
#
# @param micro_lamports [Integer] Price per compute unit (in micro-lamports)
# @return [Array<Integer>] 1-byte instruction index + 8-byte price
def self.data(micro_lamports)
INSTRUCTION_INDEX + Solace::Utils::Codecs.encode_le_u64(micro_lamports).bytes
end
end
end
end
end
2 changes: 1 addition & 1 deletion gem/lib/solace/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

module Solace
# Latest version of the Solace gem.
VERSION = '0.1.6'
VERSION = '0.1.7'
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# frozen_string_literal: true

require 'test_helper'
Comment thread
fulf marked this conversation as resolved.

describe Solace::Composers::ComputeBudgetProgramSetComputeUnitLimitComposer do
let(:bob) { Fixtures.load_keypair('bob') }
let(:anna) { Fixtures.load_keypair('anna') }

let(:connection) { Solace::Connection.new(commitment: 'processed') }
let(:transaction_composer) { Solace::TransactionComposer.new(connection: connection) }

let(:composer) do
Solace::Composers::ComputeBudgetProgramSetComputeUnitLimitComposer.new(
units: 20_000
)
end

let(:transfer_composer) do
Solace::Composers::SystemProgramTransferComposer.new(
to: anna,
from: bob,
lamports: 10_000
)
end

describe 'composed transaction' do
before(:all) do
# Add instructions and set fee payer
transaction_composer.add_instruction(composer)
transaction_composer.add_instruction(transfer_composer)
transaction_composer.set_fee_payer(bob)

# Compose and decode the transaction message
@decoded_message = Solace::Transaction.from(transaction_composer.compose_transaction.serialize).message
end

it 'includes the set compute unit limit instruction' do
instructions = @decoded_message.instructions.map { |ix| [@decoded_message.accounts[ix.program_index], ix.data] }

assert_includes instructions, [
Solace::Constants::COMPUTE_BUDGET_PROGRAM_ID,
[2] + [20_000].pack('L<').bytes
]
end
end

describe 'transaction consuming fewer compute units than the limit' do
before(:all) do
# Add instructions and set fee payer
transaction_composer.add_instruction(composer)
transaction_composer.add_instruction(transfer_composer)
transaction_composer.set_fee_payer(bob)

# Compose and sign transaction
tx = transaction_composer.compose_transaction
tx.sign(bob)

# Send transaction
@signature = connection.send_transaction(tx.serialize)
end

it 'is confirmed by the node' do
assert(connection.wait_for_confirmed_signature { @signature['result'] })
end
end

describe 'transaction exceeding the compute unit limit' do
# Requests fewer units than the transaction needs
let(:composer) do
Solace::Composers::ComputeBudgetProgramSetComputeUnitLimitComposer.new(
units: 100
)
end

before(:all) do
# Add instructions and set fee payer
transaction_composer.add_instruction(composer)
transaction_composer.add_instruction(transfer_composer)
transaction_composer.set_fee_payer(bob)

# Compose and sign transaction
@transaction = transaction_composer.compose_transaction
@transaction.sign(bob)
end

it 'is rejected by the node' do
error = assert_raises(Solace::Errors::RPCError) do
connection.send_transaction(@transaction.serialize)
end

assert_match(/exceeded/i, error.message)
end
end
end
Loading