Skip to content

Repository files navigation

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"
  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 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 SOLANA_KEYPAIR environment variable for server-side transaction signing.

Dependencies

License

MIT

About

Anchor IDL parsing and Borsh encoding for Solana program interaction in Rails

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages