Project

xrpl-ruby

0.0
The project is in a healthy, maintained state
This gem provides a Ruby interface to interact with the XRP Ledger (XRPL) blockchain, allowing developers to easily integrate XRPL functionality into their Ruby applications.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 2.0
~> 13.0
~> 3.0

Runtime

~> 1.3
~> 1.2.0
 Project Readme

XRPL-Ruby

CI codecov Gem Version Downloads Ruby License: MIT

A pure-Ruby library to interact with the XRP Ledger (XRPL) blockchain.

Features

  • Key and wallet management (secp256k1 and ed25519)
  • Address codec and binary codec, conformant with rippled 3.4.0 and the ripple-binary-codec reference fixtures
  • Transaction and ledger entry models generated from the ledger's own definitions
  • WebSocket client for the XRP Ledger public API
  • Testnet faucet helper to create and fund wallets
  • Transaction lifecycle: autofill with per-type fees, sign, submit, and reliable "submit and wait"
  • Payment channel claims, signed and verified locally

Requirements

  • Ruby 3.2 or later

Installation

Install the gem:

gem install xrpl-ruby

Or add it to your Gemfile:

gem 'xrpl-ruby'

Quick start

require 'xrpl-ruby'

# 1. Connect to the Testnet (blocks until the connection is ready)
client = XRPL::Client.new(:testnet)
client.connect!

# 2. Create and fund a wallet using the Testnet faucet
wallet = XRPL.fund_wallet(client)[:wallet]
puts wallet.classic_address

# 3. Look up the account on the ledger
info = client.account_info_response(
  account: wallet.classic_address,
  ledger_index: 'validated'
)
puts info.dig('result', 'account_data', 'Balance')

# 4. Send 1 XRP (1,000,000 drops) and wait for validation
receiver = XRPL.fund_wallet(client)[:wallet]
payment = {
  'TransactionType' => 'Payment',
  'Account'         => wallet.classic_address,
  'Destination'     => receiver.classic_address,
  'Amount'          => '1000000'
}
result = client.submit_and_wait(payment, wallet: wallet)
puts result.dig('result', 'meta', 'TransactionResult') # => "tesSUCCESS"

client.disconnect

Transactions

A transaction can always be a plain Hash, as above. The transaction classes are an addition on top of that: they are generated from the field formats in definitions.json, so they know which fields a type accepts, which of them are required, and what its flags are called.

payment = XRPL::Transaction::Payment.new(
  account:     wallet.classic_address,
  destination: receiver.classic_address,
  amount:      '1000000',
  flags:       XRPL::Transaction::Payment::TF_PARTIAL_PAYMENT
)

payment.validate!   # raises before a round trip is spent
client.submit_and_wait(payment, wallet: wallet)

Fields are written in snake_case and stored under the ledger's own names, so #to_h produces exactly what the binary codec expects:

payment.to_h
# => {"TransactionType"=>"Payment", "Account"=>"r...", "Destination"=>"r...",
#     "Amount"=>"1000000", "Flags"=>131072}

A field the type does not define is rejected when it is set, rather than by the server several seconds later:

payment.limit_amount = {}   # NoMethodError
XRPL::Transaction::Payment.new(limit_amount: {})
# => XRPL::Transaction::ValidationError: Payment has no field LimitAmount

XRPL::Transaction.from(hash) builds the matching class from a transaction hash, which is useful for anything read back off the ledger. Note that validate! follows rippled's formats: it checks what the ledger requires for serialisation, which is not always what a transaction needs to be meaningful.

Flags can be asked about by any of their names:

payment.flag?(:tf_partial_payment)   # => true
payment.flag_names                   # => ["tfPartialPayment"]

Fees

autofill sets the fee the transaction type actually needs, following the same rules as xrpl.js: an EscrowFinish pays for the size of its Fulfillment, AccountDelete, AMMCreate and VaultCreate cost the owner reserve, a Batch pays for its signatures and its inner transactions, and multisigning adds one base fee per signature. Ordinary fees are capped at 2 XRP; pass max_fee_drops: to Client.new to change that. The rules are available on their own as XRPL::Fee.calculate.

Ledger entries

The objects that make up the ledger's state - AccountRoot, RippleState, Offer, Escrow and the rest - have models too, generated from LEDGER_ENTRY_FORMATS the same way:

node  = client.request_with_retry('ledger_entry', account_root: address).dig('result', 'node')
entry = XRPL::LedgerEntry.from(node)

entry.class                       # => XRPL::LedgerEntry::AccountRoot
entry.balance                     # => "370000000"
entry.flag?(:lsf_default_ripple)  # => true
entry.flag_names                  # => ["lsfDefaultRipple", "lsfDisableMaster"]
entry.index                       # the entry's hash, as rippled reports it

Every flag rippled defines is a constant on its type, e.g. XRPL::LedgerEntry::AccountRoot::LSF_DEPOSIT_AUTH.

Batch

A Batch (XLS-56, enabled on Mainnet since 2026-10-09) applies up to eight inner transactions together. autofill prepares them: the tfInnerBatchTxn flag, Fee "0", an empty SigningPubKey and their sequences. It also refuses a Batch the ledger would refuse. A Batch of one account is signed like any other transaction:

batch = client.autofill({
  'TransactionType' => 'Batch',
  'Account' => alice.classic_address,
  'Flags' => XRPL::Batch::FLAGS['Batch']['tfAllOrNothing'],
  'RawTransactions' => [
    { 'RawTransaction' => { 'TransactionType' => 'Payment', 'Account' => alice.classic_address,
                            'Destination' => bob.classic_address, 'Amount' => '1000000' } },
    { 'RawTransaction' => { 'TransactionType' => 'Payment', 'Account' => alice.classic_address,
                            'Destination' => carol.classic_address, 'Amount' => '1000000' } }
  ]
})
client.submit_and_wait(batch, wallet: alice, autofill: false)

When inner transactions come from other accounts, each of those accounts signs the finished Batch. The outer account signs it last. The signatures cover the inner transaction IDs, so the Batch has to be complete before anyone signs, and that includes its fee. batch_signatures: tells autofill how many signatures will be collected, one per other account:

batch = client.autofill(swap, batch_signatures: 1)       # Alice pays Bob, Bob pays Alice
signed = Wallet::Wallet.combine_batch_signers([bob.sign_batch(batch)])
client.submit_and_wait(signed, wallet: alice, autofill: false)
  • sign_batch(batch, batch_account:) signs for another account whose regular key the wallet holds.
  • multisign: true signs as one member of that account's signer list.
  • combine_batch_signers sorts the entries the way rippled requires and drops the outer account's entry. It also merges signatures from one signer list into a single entry.
  • For an inner transaction with a Delegate, the delegate signs.

An outer tesSUCCESS does not mean the inner transactions succeeded. Under tfOnlyOne, tfUntilFailure or tfIndependent some of them can fail while the Batch itself succeeds. Look up each one by its ID and check its own result:

BinaryCodec.inner_transaction_ids(signed).each do |id|
  result = client.tx_response(transaction: id).dig('result', 'meta', 'TransactionResult')
  # an inner transaction that was not applied is not found at all
end

examples/batch.rb runs both flows against the Testnet.

Delegation

An account can let another account send certain transactions on its behalf (XLS-75, enabled on Mainnet since 2026-10-08). A DelegateSet grants the permissions. A permission is either a transaction type, such as "Payment", or one of the granular permissions, such as "TrustlineAuthorize" or "PaymentMint". The codec translates both names.

Do not delegate "PaymentBurn" until the fixCleanup3_4_0 amendment is enabled on the target network. Before that fix, a delegate holding PaymentBurn can in certain circumstances also mint fungible tokens (trust line tokens or MPTs). The feature method of a node reports whether it is enabled there; Known Amendments has the Mainnet status. Source: Permission Values.

client.submit_and_wait({
  'TransactionType' => 'DelegateSet',
  'Account' => isaac.classic_address,
  'Authorize' => alice.classic_address,
  'Permissions' => [{ 'Permission' => { 'PermissionValue' => 'Payment' } }]
}, wallet: isaac)

The delegate then sends the transaction with Account set to the delegating account and Delegate set to itself, and signs it with its own wallet:

client.submit_and_wait({
  'TransactionType' => 'Payment',
  'Account' => isaac.classic_address,
  'Delegate' => alice.classic_address,
  'Destination' => bob.classic_address,
  'Amount' => '1000000'
}, wallet: alice)
  • Sequence: autofill takes it from Account, as rippled expects.
  • Signature and fee: both belong to the delegate. Isaac pays the amount, Alice pays the fee.
  • Without the permission: the transaction comes back as terNO_DELEGATE_PERMISSION.
  • Inside a Batch: for a delegated inner transaction, the delegate signs the BatchSigners entry (see above).

Payment channel claims

A claim is signed off-ledger and redeemed later with a PaymentChannelClaim transaction. Amounts are in drops:

signature = wallet.sign_payment_channel_claim(channel_id, '1000000')

# On the receiving side, against the channel's PublicKey:
Wallet::Wallet.verify_payment_channel_claim(channel_id, '1000000', signature, public_key)

The client is silent by default. To see diagnostic output, pass a logger:

require 'logger'
client = XRPL::Client.new(:testnet, logger: Logger.new($stdout))

More runnable examples are in the examples/ directory.

Running the tests

bundle install
bundle exec rspec

Integration tests talk to the real Testnet and are skipped by default. Enable them explicitly:

XRPL_NETWORK=1 bundle exec rspec spec/integration

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/AlexanderBuzz/xrpl-ruby.

License

Released under the MIT License.