XRPL-Ruby
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-rubyOr 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.disconnectTransactions
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 LimitAmountXRPL::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 itEvery 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: truesigns as one member of that account's signer list. -
combine_batch_signerssorts 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
endexamples/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 thefixCleanup3_4_0amendment is enabled on the target network. Before that fix, a delegate holdingPaymentBurncan in certain circumstances also mint fungible tokens (trust line tokens or MPTs). Thefeaturemethod 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:
autofilltakes it fromAccount, 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
BatchSignersentry (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 rspecIntegration tests talk to the real Testnet and are skipped by default. Enable them explicitly:
XRPL_NETWORK=1 bundle exec rspec spec/integrationContributing
Bug reports and pull requests are welcome on GitHub at https://github.com/AlexanderBuzz/xrpl-ruby.
License
Released under the MIT License.