SolRengine Programs

Solana program interaction for Rails. Parse Anchor IDL files to generate Ruby account models, instruction builders, and Stimulus controllers.

Installation

Add to your Gemfile:

gem "solrengine-programs"

Usage

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 decoding
  • app/services/piggy_bank/lock_instruction.rb — Instruction builder
  • app/services/piggy_bank/unlock_instruction.rb — Instruction builder
  • app/javascript/controllers/piggy_bank_controller.js — Stimulus controller
  • config/idl/piggy_bank.json — IDL copy

Query Program Accounts

class PiggyBank::Lock < Solrengine::Programs::Account
  program_id "ZaU8j7XCKSxmmkMvg7NnjrLNK6eiLZbHsJQAc2rFzEN"
   "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"

   :payer, signer: true, writable: true
   :dst
   :lock, signer: true, writable: true
   :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 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"

   :signer, signer: true, writable: true
   :poll_account, writable: true, pda: [
    { const: [112, 111, 108, 108] },      # b"poll"
    { arg: :poll_id, type: :u64 }
  ]
   :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 SOLANA_KEYPAIR environment variable for server-side transaction signing.

Dependencies

License

MIT