solrengine-programs
Solana program interaction for Rails. Parse Anchor IDL files into Ruby account models, instruction builders, PDA derivation, and Stimulus controllers.
Solana program interaction for Rails. Parse Anchor IDL files to generate Ruby account models, instruction builders, and Stimulus controllers — then query accounts and build transactions in plain Ruby.
Install
# Gemfile
gem "solrengine-programs"
Generate from Anchor IDL
rails generate solrengine:program PiggyBank path/to/piggy_bank.json
This creates:
app/models/piggy_bank/lock.rb— account model with Borsh decodingapp/services/piggy_bank/lock_instruction.rb— instruction builderapp/services/piggy_bank/unlock_instruction.rb— instruction builderapp/javascript/controllers/piggy_bank_controller.js— Stimulus controllerconfig/idl/piggy_bank.json— IDL copy
Query program accounts
class PiggyBank::Lock < Solrengine::Programs::Account
program_id "ZaU8j7XCKSxmmkMvg7NnjrLNK6eiLZbHsJQAc2rFzEN"
account_name "Lock"
borsh_field :dst, "pubkey"
borsh_field :exp, "u64"
def self.for_wallet(wallet_address)
query(filters: [
{ "memcmp" => { "offset" => 8, "bytes" => wallet_address } }
])
end
def expired?
exp < Time.now.to_i
end
end
# Query accounts
locks = PiggyBank::Lock.for_wallet("YourWalletAddress...")
locks.each do |lock|
puts "#{lock.pubkey}: #{lock.sol_balance} SOL, expires #{Time.at(lock.exp)}"
end
Build instructions (server-side)
class PiggyBank::LockInstruction < Solrengine::Programs::Instruction
program_id "ZaU8j7XCKSxmmkMvg7NnjrLNK6eiLZbHsJQAc2rFzEN"
instruction_name "lock"
argument :amt, "u64"
argument :exp, "u64"
account :payer, signer: true, writable: true
account :dst
account :lock, signer: true, writable: true
account :system_program, address: "11111111111111111111111111111111"
end
# Build and send a transaction
ix = PiggyBank::LockInstruction.new(
amt: 100_000_000,
exp: (Time.now + 5.minutes).to_i,
payer: payer_pubkey,
dst: destination_pubkey,
lock: lock_keypair_pubkey
)
builder = Solrengine::Programs::TransactionBuilder.new
builder.add_instruction(ix)
builder.add_signer(server_keypair)
signature = builder.sign_and_send
PDA derivation
The generator reads pda.seeds from the Anchor IDL and emits instruction builders that derive addresses automatically — no manual address math required.
class Voting::InitializeCandidateInstruction < Solrengine::Programs::Instruction
program_id "2F1Z4eTmFqbjAnNWaDXXScoBYLMFn1gTasVy2mfPTeJx"
instruction_name "initialize_candidate"
argument :poll_id, "u64"
argument :candidate, "string"
account :signer, signer: true, writable: true
account :poll_account, writable: true, pda: [
{ const: [112, 111, 108, 108] }, # b"poll"
{ arg: :poll_id, type: :u64 }
]
account :candidate_account, writable: true, pda: [
{ arg: :poll_id, type: :u64 },
{ arg: :candidate, type: :string }
]
end
ix = Voting::InitializeCandidateInstruction.new(
poll_id: 1,
candidate: "alpha",
signer: payer_pubkey
)
# poll_account and candidate_account addresses are derived automatically
ix.to_instruction
You can also derive addresses manually:
address, bump = Solrengine::Programs::Pda.find_program_address(
["vault", Solrengine::Programs::Pda.to_seed(user_pubkey, :pubkey)],
program_id
)
Error mapping
idl = Solrengine::Programs::IdlParser.parse_file("config/idl/piggy_bank.json")
mapper = Solrengine::Programs::ErrorMapper.new(idl.errors)
begin
builder.sign_and_send
rescue Solrengine::Programs::TransactionError => e
mapper.raise_if_program_error!(e.rpc_error)
# Raises: ProgramError "LockNotExpired (6002): Lock has not expired yet"
end
Configuration
# config/initializers/solrengine_programs.rb
Solrengine::Programs.configure do |config|
config.keypair_format = :base58 # or :json_array
end
Set the SOLANA_KEYPAIR environment variable for server-side transaction signing.
Dependencies
- solrengine-rpc — Solana RPC client
- borsh — Borsh binary serialization
- ed25519 — transaction signing
- base58 — address encoding
License
MIT