solana-ruby-kit

A Ruby port of @anza-xyz/kit — the official Solana TypeScript SDK — translated into idiomatic Ruby with Sorbet static types.

Every module maps 1-to-1 to a TypeScript package. All methods are synchronous (Ruby's RbNaCl is synchronous; TypeScript's Web Crypto API is not). This port now tracks the anza-xyz/kit version.

Requirements

  • Ruby >= 3.2
  • libsodium (required by rbnacl)
# macOS
brew install libsodium

# Debian / Ubuntu
apt-get install libsodium-dev

Installation

# Gemfile
gem 'solana-ruby-kit'
bundle install

Examples

Full worked examples live on the wiki:

The sections below cover configuration and the per-module API reference.

Rails

The gem includes a Railtie that auto-configures when Rails is present. Add it to your Gemfile as usual, then run the install generator:

rails generate solana:ruby:kit:install

This creates config/initializers/ruby_kit.rb:

Solana::Ruby::Kit.configure do |config|
  config.rpc_url    = 'https://api.mainnet-beta.solana.com'
  config.ws_url     = 'wss://api.mainnet-beta.solana.com'
  config.commitment = :confirmed
  config.timeout    = 30
end

Or configure via config/application.rb:

config.ruby_kit.rpc_url    = ENV['SOLANA_RPC_URL']
config.ruby_kit.commitment = :finalized

Get a pre-configured client anywhere in your app:

rpc = Solana::Ruby::Kit.rpc_client

Configuration

Option Default Description
rpc_url https://api.mainnet-beta.solana.com JSON-RPC endpoint
ws_url nil WebSocket endpoint for subscriptions
commitment :confirmed Default commitment level
timeout 30 HTTP read timeout in seconds

Modules

Solana::Ruby::Kit::Addresses@solana/addresses

Validate and work with base58-encoded Solana addresses.

Addr = Solana::Ruby::Kit::Addresses

# Validate
Addr.address?('11111111111111111111111111111111')  # => true

# Wrap into a typed Address value object (raises on invalid input)
addr = Addr.address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')

# Encode / decode raw bytes
bytes = Addr.decode_address(addr)   # => 32-byte binary String
str   = Addr.encode_address(bytes)  # => base58 String

# Program Derived Addresses (PDAs)
program = Addr.address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')
pda = Addr.get_program_derived_address(
  program_address: program,
  seeds:           ['my-seed', [1, 2, 3]]
)
puts pda.address  # => Address
puts pda.bump     # => Integer (0-255)

Solana::Ruby::Kit::Keys@solana/keys

Ed25519 key generation, signing, and verification via libsodium.

Keys = Solana::Ruby::Kit::Keys

# Generate
kp = Keys.generate_key_pair
kp.signing_key  # => RbNaCl::SigningKey
kp.verify_key   # => RbNaCl::VerifyKey

# From 64 raw bytes (seed || public key)
kp = Keys.create_key_pair_from_bytes(File.binread('wallet.bin'))

# From 32-byte private seed only
kp = Keys.create_key_pair_from_private_key_bytes(seed_bytes)

# Sign / verify
sig = Keys.sign_bytes(kp.signing_key, data)
ok  = Keys.verify_signature(kp.verify_key, sig, data)

# Base58-encode a signature
Keys.encode_signature(sig)   # => Signature string
Keys.decode_signature(str)   # => SignatureBytes

Solana::Ruby::Kit::Signers@solana/signers

High-level signer abstraction that wraps a key pair and exposes an address.

Signers = Solana::Ruby::Kit::Signers

# Random new signer
signer = Signers.generate_key_pair_signer
signer.address    # => Addresses::Address
signer.to_s       # => base58 string

# From existing key pair
signer = Signers.create_signer_from_key_pair(kp)

# From raw bytes
signer = Signers.create_key_pair_signer_from_bytes(bytes_64)
signer = Signers.create_key_pair_signer_from_private_key_bytes(seed_32)

# Sign arbitrary data
sig = signer.sign(message_bytes)

# Sign a batch of messages for multiple signers
map = Signers.sign_message_bytes_with_signers([signer1, signer2], bytes)
# => { "addr1" => SignatureBytes, "addr2" => SignatureBytes }

Solana::Ruby::Kit::TransactionMessages@solana/transaction-messages

Immutable transaction message builder. Every method returns a new struct; originals are unmodified.

TxMsg = Solana::Ruby::Kit::TransactionMessages

msg = TxMsg.create_transaction_message(version: 0)

# Set fee payer
msg = TxMsg.set_fee_payer(signer.address, msg)

# Set blockhash lifetime
constraint = TxMsg::BlockhashLifetimeConstraint.new(
  blockhash: '4vJ9...', last_valid_block_height: 123_456
)
msg = TxMsg.set_blockhash_lifetime(constraint, msg)

# Append / prepend instructions
msg = TxMsg.append_instructions(msg, [instruction])
msg = TxMsg.prepend_instructions(msg, [priority_fee_ix])

# Compute unit limit (SetComputeUnitLimit instruction from the Compute Budget program)
msg = TxMsg.set_transaction_message_compute_unit_limit(200_000, msg)
TxMsg.get_transaction_message_compute_unit_limit(msg)  # => 200_000

# Loaded accounts data size limit
msg = TxMsg.set_transaction_message_loaded_accounts_data_size_limit(64_000, msg)
TxMsg.get_transaction_message_loaded_accounts_data_size_limit(msg)  # => 64_000

# Durable nonce lifetime
nonce_constraint = TxMsg::DurableNonceLifetimeConstraint.new(
  nonce:                 'abc...',
  nonce_account_address: nonce_addr
)
msg = TxMsg.set_durable_nonce_lifetime(nonce_constraint, msg)

Solana::Ruby::Kit::Transactions@solana/transactions

Compile, sign, and inspect transactions.

Txns = Solana::Ruby::Kit::Transactions

# Compile a TransactionMessage into wire bytes + an empty signatures map.
# message_bytes are the bytes that each required signer must sign.
# A lifetime constraint (blockhash or durable nonce) is optional at compile
# time — omitting it writes 32 zero bytes into the blockhash field, which
# must be replaced before the transaction is valid for submission.
transaction = Txns.compile_transaction_message(message)

# Partially sign (one or more keys, not necessarily all signers)
tx = Txns.partially_sign_transaction([kp.signing_key], transaction)

# Fully sign (raises unless all signers have signed)
signed_tx = Txns.sign_transaction([kp.signing_key], transaction)

# Encode the fully signed transaction for submission via sendTransaction.
# Prepends compact-u16 signature count + 64-byte signatures to message bytes.
wire_bytes  = Txns.wire_encode_transaction(signed_tx)
wire_base64 = Base64.strict_encode64(wire_bytes)

# Get the transaction signature (fee payer's signature, base58)
sig = Txns.get_signature_from_transaction(signed_tx)

# Check completeness and size
Txns.fully_signed_transaction?(tx)      # => true / false
Txns.assert_fully_signed_transaction!(tx)

Txns.within_size_limit?(tx)             # => true if wire size <= 1232 bytes
Txns.assert_within_size_limit!(tx)

# Sendable = fully signed AND within size limit
Txns.sendable_transaction?(signed_tx)   # => true / false
Txns.assert_sendable_transaction!(signed_tx)

Solana::Ruby::Kit::Rpc@solana/rpc

Synchronous JSON-RPC client backed by Net::HTTP.

rpc = Solana::Ruby::Kit::Rpc::Client.new(
  Solana::Ruby::Kit::RpcTypes.devnet,
  timeout:      10,
  open_timeout: 5
)

rpc.get_slot                                  # => Integer
rpc.get_block_height                          # => Integer
rpc.get_balance(address)                      # resp.value => lamports
rpc.get_latest_blockhash                      # resp.value.blockhash, .last_valid_block_height
rpc.(address, encoding: 'base64')
rpc.get_multiple_accounts([addr1, addr2])
rpc.get_program_accounts(program_address)
rpc.get_signature_statuses([sig_str])
rpc.is_blockhash_valid(blockhash, commitment: :confirmed)
rpc.get_minimum_balance_for_rent_exemption(data_length)
rpc.get_transaction(signature, encoding: 'base64')
rpc.()
rpc.get_token_accounts_by_owner(owner, mint: mint_address)
rpc.get_epoch_info
rpc.get_epoch_schedule
rpc.get_block_time(slot)
rpc.get_inflation_reward([address])
rpc.get_signatures_for_address(address)
rpc.get_vote_accounts
rpc.simulate_transaction(encoded_tx)
rpc.send_transaction(encoded_tx)
rpc.request_airdrop(address, lamports)        # devnet / testnet only

Errors:

rescue Solana::Ruby::Kit::Rpc::RpcError => e
  puts e.code     # JSON-RPC error code
  puts e.message  # JSON-RPC error message
rescue Solana::Ruby::Kit::Rpc::HttpTransportError => e
  puts e.status_code  # HTTP status code

Solana::Ruby::Kit::RpcTypes@solana/rpc-types

Cluster URL helpers and typed wrappers.

RpcTypes = Solana::Ruby::Kit::RpcTypes

RpcTypes.mainnet                             # default mainnet URL
RpcTypes.mainnet('https://my-rpc.com')       # custom mainnet URL
RpcTypes.devnet                              # devnet
RpcTypes.testnet                             # testnet
RpcTypes.cluster_url('http://localhost:8899') # custom / localnet

Solana::Ruby::Kit::Options@solana/options

Rust-style Option<T> for Solana's on-chain option codec pattern.

Opts = Solana::Ruby::Kit::Options

some = Opts.some(42)          # => Some(42)
none = Opts.none              # => None

Opts.some?(some)              # => true
Opts.none?(none)              # => true
Opts.option?(some)            # => true

Opts.unwrap_option(some)      # => 42
Opts.unwrap_option(none)      # => nil
Opts.unwrap_option(none, -> { 0 })  # => 0

Opts.wrap_nullable(nil)       # => None
Opts.wrap_nullable(42)        # => Some(42)

# Recursively unwrap nested structures
Opts.unwrap_option_recursively({ a: some, b: [none, some] })
# => { a: 42, b: [nil, 42] }

Solana::Ruby::Kit::Functional@solana/functional

Functional pipeline composition.

Kit = Solana::Ruby::Kit

result = Kit::Functional.pipe(
  Kit::TransactionMessages.create_transaction_message(version: 0),
  ->(tx) { Kit::TransactionMessages.set_fee_payer(fee_payer, tx) },
  ->(tx) { Kit::TransactionMessages.set_blockhash_lifetime(constraint, tx) },
  ->(tx) { Kit::TransactionMessages.append_instructions(tx, [ix]) }
)

Solana::Ruby::Kit::Codecs@solana/codecs

Binary encoder/decoder framework for Solana on-chain data.

Codecs = Solana::Ruby::Kit::Codecs

# Numbers
u8  = Codecs.u8
u16 = Codecs.u16_le   # little-endian (default for Solana)
u32 = Codecs.u32_le
u64 = Codecs.u64_le
i8  = Codecs.i8
f32 = Codecs.f32_le

u16.encode(1000)       # => "\xe8\x03"
u16.decode("\xe8\x03") # => 1000

# Strings
utf8  = Codecs.utf8
bytes = Codecs.bytes_codec

# Data structures
struct_codec = Codecs.struct_codec([
  ['amount', u64],
  ['mint',   bytes]
])

Solana::Ruby::Kit::RpcSubscriptions@solana/rpc-subscriptions

WebSocket-based subscription client.

ws = Solana::Ruby::Kit::RpcSubscriptions::Client.new(
  'wss://api.devnet.solana.com'
)

sub = ws.(address, commitment: :confirmed)
sub.on_message { |notification| puts notification }
sub.on_error   { |err|          puts err }

ws.(sub.id)
ws.close

Solana::Ruby::Kit::Sysvars@solana/sysvars

Well-known sysvar addresses and decoded account data.

Sysvars = Solana::Ruby::Kit::Sysvars

Sysvars::Addresses::CLOCK_ADDRESS          # => Address
Sysvars::Addresses::RENT_ADDRESS
Sysvars::Addresses::EPOCH_SCHEDULE_ADDRESS

# Fetch and decode via an RPC client
clock = Sysvars.fetch_sysvar_clock(rpc)
clock.slot              # => Integer
clock.epoch             # => Integer
clock.unix_timestamp    # => Integer

rent = Sysvars.fetch_sysvar_rent(rpc)
rent.lamports_per_byte_year  # => Integer
rent.exemption_threshold     # => Float

Solana::Ruby::Kit::Programs@solana/programs

Program error helpers and well-known program interfaces.

Programs = Solana::Ruby::Kit::Programs

# Inspect custom program errors in transaction simulation results
Programs.program_error?(err)                   # => true / false
Programs.program_error?(err, expected_code: 1) # match a specific code
Programs.get_program_error_code(err)           # => Integer or nil

Programs::StakeProgram

Create and delegate stake accounts.

Stake = Solana::Ruby::Kit::Programs::StakeProgram

# Well-known addresses
Stake::PROGRAM_ID       # Stake11111111111111111111111111111111111111
Stake::STAKE_CONFIG_ID  # StakeConfig11111111111111111111111111111111
Stake::STAKE_ACCOUNT_SPACE  # => 200 (bytes required for a stake account)

# Build two instructions that allocate and initialise a new stake account.
# The caller appends both to a transaction message.
create_ixs = Stake.(
  from:          fee_payer_address,   # funding wallet (writable, signer)
  stake_account: stake_keypair.address, # new stake account address (writable, signer)
  authorized:    owner_address,       # becomes both staker and withdrawer
  lamports:      2_282_880            # enough for rent + some stake
)

# Build one instruction that delegates an initialised stake account.
delegate_ix = Stake.delegate_instruction(
  stake_account: stake_keypair.address,
  vote_account:  validator_vote_address,
  authorized:    owner_address         # must sign the transaction
)

Programs::AssociatedTokenAccount

Create SPL token accounts at their canonical (Associated Token Account) address.

ATA = Solana::Ruby::Kit::Programs::AssociatedTokenAccount

# Well-known program IDs
ATA::PROGRAM_ID          # ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL
ATA::TOKEN_PROGRAM_ID    # TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
ATA::TOKEN_2022_PROGRAM_ID
ATA::SYSTEM_PROGRAM_ID

# Derive the ATA address (no RPC call required)
pda = ATA.get_associated_token_address(
  wallet:           owner_address,
  mint:             mint_address,
  token_program_id: ATA::TOKEN_PROGRAM_ID   # default; omit for SPL Token
)
pda.address  # => Addresses::Address (the ATA)
pda.bump     # => Integer

# Build the createAssociatedTokenAccount instruction
ix = ATA.create_instruction(
  payer:            fee_payer_address,  # pays rent
  wallet:           owner_address,      # will own the ATA
  mint:             mint_address,
  token_program_id: ATA::TOKEN_PROGRAM_ID,
  idempotent:       true   # use CreateIdempotent — safe to call if ATA exists
)

Solana::Ruby::Kit::OffchainMessages@solana/signers, @solana/offchain-messages

Sign and verify off-chain messages (Phantom wallet standard).

OffChain = Solana::Ruby::Kit::OffchainMessages

msg = OffChain::Message.new(
  version: 1,                     # 0 = legacy printable ASCII, 1 = extended UTF-8
  domain:  'example.com',
  message: 'Hello, Solana!'
)

encoded = OffChain.encode_offchain_message(msg)
decoded = OffChain.decode_offchain_message(encoded)

signature = OffChain.sign_offchain_message(signer, msg)
OffChain.verify_offchain_message_signature(verify_key_bytes, signature, msg)  # => true

Checking that a signer signed what you asked

A wallet returns the message bytes it signed alongside its signature. Verifying that signature only proves the signer produced it over those bytes — not that those bytes are the message you asked for. Compare the two before you verify the signature, so a signer that signed the wrong thing is reported as a content mismatch rather than as a confusing cryptographic failure.

Addresses = Solana::Ruby::Kit::Addresses

expected = OffChain::MessageV1.new(
  content:              'Transfer 1 SOL to Alice',
  required_signatories: [OffChain::Signatory.new(address: Addresses::Address.new(my_address))]
)

# Raises SolanaError if the content or the required signatories differ.
# Required signatories are compared ignoring order; content is compared exactly.
OffChain.assert_offchain_message_v1_equal(received, expected)

The error context reports content lengths in UTF-8 bytes rather than the content itself, so message text never reaches your logs or error reporting.

Note: MessageV1 mirrors the newer @solana/offchain-messages v1 shape (UTF-8 content plus required_signatories) and is a distinct type from Message above, which models the older @solana/signers domain form. The v1 wire codec is not yet translated, so received must come from your own decoding for now.

Solana::Ruby::Kit::ResourceLimitEstimation@solana/kit

Estimate and set compute unit limits and loaded accounts data size limits by simulating the transaction before sending.

RLE = Solana::Ruby::Kit::ResourceLimitEstimation

# 1. Fill provisory (0) limits as placeholders during message construction.
#    This reserves space in the transaction for the limit instructions so the
#    size estimate used by compile_transaction_message is accurate.
msg = RLE.fill_transaction_message_provisory_resource_limits(msg)

# 2. After construction, estimate actual resource usage via simulation and
#    stamp the real values onto the message.
estimator = RLE.estimate_resource_limits_factory(rpc: rpc)
# estimate returns { compute_unit_limit: Integer, loaded_accounts_data_size_limit?: Integer }
estimate = estimator.call(msg)

# 3. Or combine steps 2 + set in one call.
setter  = RLE.estimate_and_set_resource_limits_factory(estimator)
msg     = setter.call(msg)

Solana::Ruby::Kit::TransactionConfirmation@solana/transaction-confirmation

Poll for transaction confirmation with timeout.

Confirm = Solana::Ruby::Kit::TransactionConfirmation

Confirm.wait_for_confirmation(
  rpc,
  sig,
  commitment:   :confirmed,
  timeout_secs: 60
)

Solana::Ruby::Kit::InstructionPlans@solana/instruction-plans

Plan and execute instruction sequences that may span multiple transactions. The planner packs instructions into messages respecting the 1232-byte transaction size limit; the executor walks the resulting plan tree and sends each transaction.

require 'base64'
Plans = Solana::Ruby::Kit::InstructionPlans
Kit   = Solana::Ruby::Kit

# ── 1. Build an instruction plan ─────────────────────────────────────────────
# Instructions are auto-wrapped in SingleInstructionPlan.
# Use parallel_instruction_plan for instructions that can run concurrently.
plan = Plans.sequential_instruction_plan([ix1, ix2, ix3])

# ── 2. Create a planner ───────────────────────────────────────────────────────
# create_transaction_message is called whenever a new (empty) transaction is
# needed. It must return a message with a fee payer and blockhash already set.
#
# max_instructions_per_transaction caps how many top-level instructions the
# planner packs into a single message. Must be a positive integer no greater
# than 64 (the transaction format's hard limit); defaults to 16, leaving
# headroom for inner (CPI) instructions the planner can't see ahead of time.
planner = Plans.create_transaction_planner(
  create_transaction_message: -> {
    Kit::Functional.pipe(
      Kit::TransactionMessages.create_transaction_message(version: :legacy),
      ->(tx) { Kit::TransactionMessages.set_fee_payer(signer.address, tx) },
      ->(tx) { Kit::TransactionMessages.set_blockhash_lifetime(constraint, tx) }
    )
  },
  max_instructions_per_transaction: 12  # optional; omit for the default of 16
)

# plan! distributes instructions across as few transactions as possible.
# The cap can also be overridden per call:
transaction_plan = planner.call(plan, max_instructions_per_transaction: 8)

# ── 3. Create an executor and run it ─────────────────────────────────────────
# execute_transaction_message receives a fresh, mutable context Hash and each
# fully-packed TransactionMessage, and returns the context a successful result
# should carry. The two serve different outcomes: what you *store* on the context
# reaches a failed or canceled result, what you *return* reaches a successful one
# (merged over what was stored, the returned value winning).
# If it raises, the executor cancels all remaining messages and re-raises — and the
# context as it stood at that moment is preserved on the failed result.
executor = Plans.create_transaction_plan_executor(
  execute_transaction_message: ->(context, message) {
    transaction = Kit::Transactions.compile_transaction_message(message)
    signed      = Kit::Transactions.sign_transaction([signer.key_pair.signing_key], transaction)
    context[:transaction] = signed  # recorded now, so it survives a failure below
    wire = Base64.strict_encode64(Kit::Transactions.wire_encode_transaction(signed))
    rpc.send_transaction(wire)
    { transaction: signed }
  }
)

result = executor.call(transaction_plan)
# result is a TransactionPlanResult tree mirroring the transaction_plan structure.
# Each leaf is a SingleTransactionPlanResult with status :successful, :failed, or :canceled.
# Every status carries a #context; a :successful one also exposes #transaction.

# A one-argument lambda returning { transaction:, context: } — the shape this method
# required before v7.1.0 — is still accepted and adapted onto the flow above.

# ── 4. Plan types ─────────────────────────────────────────────────────────────
Plans.single_instruction_plan(ix)                      # wrap one instruction
Plans.sequential_instruction_plan([ix1, ix2])          # ordered, divisible
Plans.non_divisible_sequential_instruction_plan([ix1, ix2])  # must be atomic
Plans.parallel_instruction_plan([ix1, ix2])            # any order / same tx

# MessagePacker plans for instructions of variable size (e.g. large data writes)
Plans.get_linear_message_packer_instruction_plan(
  total_length:    data.bytesize,
  get_instruction: ->(offset, length) { build_write_ix(offset, data[offset, length]) }
)

Solana::Ruby::Kit::WalletStandard@solana/wallet-standard

Server-side handling of the Wallet Standard signTransaction interface. A browser wallet (Phantom, Backpack, Solflare, …) signs a transaction and returns wire bytes; Rails decodes those bytes and verifies every Ed25519 signature without broadcasting — because Solana addresses are Ed25519 public keys, no additional key lookup is required.

Full worked examples: Verify a Wallet-Signed Transaction in Rails and Build a Transaction for Browser Signing.

Wallet Standard feature constants

Use these when building frontend metadata or documenting required wallet capabilities:

WS = Solana::Ruby::Kit::WalletStandard

WS::SIGN_TRANSACTION          # => 'solana:signTransaction'
WS::SIGN_AND_SEND_TRANSACTION # => 'solana:signAndSendTransaction'
WS::SIGN_MESSAGE              # => 'solana:signMessage'
WS::CONNECT                   # => 'standard:connect'

Solana::Ruby::Kit::TransactionIntrospection@solana/transaction-introspection

Decode a confirmed getTransaction response and walk its outer and inner (CPI) instructions — useful for indexers, explorers, or auditing what a transaction actually did on-chain.

TI  = Solana::Ruby::Kit::TransactionIntrospection
rpc = Solana::Ruby::Kit.rpc_client

rpc_tx = rpc.get_transaction(
  signature,
  encoding:                           'base64',
  max_supported_transaction_version: 0
)

# Decodes 'base64', 'base58', or 'json' getTransaction responses.
# `transaction` is only present for 'base64'/'base58' (a re-encodable
# Transactions::Transaction); 'json' responses carry no wire bytes to round-trip.
decoded = TI.decode_transaction_from_rpc_response(rpc_tx)

# Outer instructions only, with account indices resolved to full AccountMetas.
outer = TI.get_instructions_from_compiled_transaction_message(
  decoded.compiled_message,
  decoded.loaded_addresses
)

# Every instruction — outer and inner (CPI) — in the order an explorer
# displays them, each tagged with a `trace` describing its position.
instructions = TI.walk_instructions(
  compiled_message: decoded.compiled_message,
  loaded_addresses: decoded.loaded_addresses,
  meta:             rpc_tx['meta']
)

instructions.each do |traced|
  ix = traced.instruction
  puts "#{traced.trace[:kind]} ix -> #{ix.program_address}"
end

Note 'jsonParsed' responses are not supported — their instructions are pre-parsed by the server and lack raw bytes, so they can't be resolved here; fetch with 'json' or 'base64' instead. Wire-format ('base64'/'base58') decoding only handles legacy and v0 transactions — Ruby's transaction compiler doesn't produce v1 transactions yet either, so there is nothing to decompile for that version.

Error handling

All errors inherit from Solana::Ruby::Kit::SolanaError.

rescue Solana::Ruby::Kit::SolanaError => e
  puts e.code     # => :SOLANA_ERROR__ADDRESSES__INVALID_BASE58_ENCODED_ADDRESS
  puts e.message  # => human-readable description
  puts e.context  # => Hash of structured context values
end

Error codes match the TypeScript @solana/errors package constants.

Type checking with Sorbet

Every public method has a sig block. To enable static type checking in your project:

bundle exec srb init
bundle exec srb tc

Development

bundle install
bundle exec rspec            # run tests
bundle exec srb tc           # type-check
bundle exec tapioca gems     # regenerate gem RBI files (first time or after gem updates)

License

MIT