When a transaction modifies the XRP Ledger, it doesn't work directly with the permanent ledger state. Instead, it operates through views—abstraction layers that provide controlled access to ledger data with support for atomic commits and rollbacks.
Understanding how to correctly read, create, update, and delete ledger entries (SLEs) through views is essential for implementing transactors that maintain ledger integrity.
Ledger Views
The view system provides three levels of access:
ReadView
Read-only access to ledger state. Used in preclaim:
classReadView{public:// Read a ledger entry (returns nullptr if not found)virtualstd::shared_ptr<SLEconst>read(Keyletconst&k)const=0;// Check if an entry existsvirtualboolexists(Keyletconst&k)const=0;// Get current feesvirtual Fees const&fees()const=0;// Get current rules (amendments)virtual Rules const&rules()const=0;// Get ledger sequencevirtualLedgerIndexseq()const=0;};
Example usage in preclaim:
ApplyView
Read/write access to ledger state. Used in doApply:
OpenView
Used internally by the ledger for staging changes during consensus.
Serialized Ledger Entries (SLEs)
Ledger entries are represented as SLE objects (Serialized Ledger Entries). Each SLE has:
A type (AccountRoot, Check, Offer, TrustLine, etc.)
A key (256-bit unique identifier)
Fields specific to that type
Creating an SLE
Reading an SLE
Modifying an SLE
Deleting an SLE
Keylets
Keylets are typed wrappers around ledger entry keys. They combine a type and a key, ensuring type safety when accessing ledger entries.
Example:
Directory Management
Directories are linked lists of ledger entry keys, used to track which objects an account owns. Every account has an owner directory that lists all objects owned by that account.
Adding to a Directory
When creating a new ledger object, add it to the appropriate directories:
For objects that relate to two accounts (like Checks), add to both directories:
Removing from a Directory
When deleting a ledger object, remove it from all directories:
Owner Count Management
Each account tracks how many ledger objects it owns via the sfOwnerCount field. This count affects the account's reserve requirement.
Incrementing Owner Count
When creating a new owned object:
Decrementing Owner Count
When deleting an owned object:
The adjustOwnerCount Function
This function:
Gets the current owner count
Adds the adjustment
Updates the account SLE
Reserve Checking
Before creating a new object, verify the account can afford the increased reserve:
Why use mPriorBalance?
The reserve is checked against the balance before the transaction fee is deducted. This allows accounts to dip into their reserve to pay fees, which is important for cleaning up objects when an account is low on funds.
Atomic Operations
All changes made through views are staged and only committed if the transaction succeeds. If the transaction fails with a tec code:
All state changes are reverted
The fee is still charged
The sequence number is still consumed
This ensures that failed transactions never leave the ledger in an inconsistent state.
How Atomicity Works
Staging: Changes are made to a view layer, not the actual ledger
Validation: All invariants are checked
Commit or Rollback:
On tesSUCCESS: Changes are committed
On tec*: Changes are reverted, but fee/sequence applied
On other failures: Nothing is applied
Common Utility Functions
Reading Account Balances
Checking Freeze Status
Checking Expiration
Best Practices
Always check entry existence: Use peek() or read() before accessing fields
Use keylets for type safety: Don't construct keys manually
Update directories on create/delete: Maintain bidirectional links
Update owner count on create/delete: Keep reserve calculations correct
Check reserves before creating: Prevent tecINSUFFICIENT_RESERVE failures
Remove from directories before erasing: Clean up all references
Use mPriorBalance for reserve checks: Allow fee payment from reserves
TER CreateCheck::preclaim(PreclaimContext const& ctx)
{
// Read destination account (read-only)
auto const sleDst = ctx.view.read(keylet::account(dstId));
if (!sleDst)
return tecNO_DST;
// Read flags
auto const flags = sleDst->getFlags();
// ...
}
class ApplyView : public ReadView
{
public:
// Get modifiable reference to an entry
virtual std::shared_ptr<SLE> peek(Keylet const& k) = 0;
// Insert a new entry
virtual void insert(std::shared_ptr<SLE> const& sle) = 0;
// Mark an entry as updated
virtual void update(std::shared_ptr<SLE> const& sle) = 0;
// Delete an entry
virtual void erase(std::shared_ptr<SLE const> const& sle) = 0;
// Directory operations
virtual std::optional<std::uint64_t> dirInsert(
Keylet const& directory,
Keylet const& key,
std::function<void(SLE::ref)> const& describe) = 0;
virtual bool dirRemove(
Keylet const& directory,
std::uint64_t page,
uint256 const& key,
bool keepRoot) = 0;
};
// Create a new Check entry
Keylet const checkKeylet = keylet::check(account_, seq);
auto sleCheck = std::make_shared<SLE>(checkKeylet);
// Set required fields
sleCheck->setAccountID(sfAccount, account_);
sleCheck->setAccountID(sfDestination, dstAccountId);
sleCheck->setFieldU32(sfSequence, seq);
sleCheck->setFieldAmount(sfSendMax, ctx_.tx[sfSendMax]);
// Set optional fields
if (auto const srcTag = ctx_.tx[~sfSourceTag])
sleCheck->setFieldU32(sfSourceTag, *srcTag);
// Read-only (in preclaim)
auto const sle = ctx.view.read(keylet::account(accountId));
if (!sle)
return tecNO_ENTRY;
// Get field values
auto const flags = sle->getFlags();
auto const balance = sle->getFieldAmount(sfBalance);
auto const sequence = sle->getFieldU32(sfSequence);
// Get modifiable reference (in doApply)
auto sle = view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL;
// Modify fields
sle->setFieldU32(sfSequence, newSequence);
sle->setFieldAmount(sfBalance, newBalance);
// Mark as updated (implicit in most cases, but good practice)
view().update(sle);
// Get the entry
auto sle = view().peek(keylet::check(owner, seq));
if (!sle)
return tecNO_ENTRY;
// Remove from directories first
view().dirRemove(
keylet::ownerDir(owner),
sle->getFieldU64(sfOwnerNode),
sle->key(),
false);
// Decrement owner count
adjustOwnerCount(view(), sleAccount, -1, j);
// Delete the entry
view().erase(sle);
// Create a keylet for a check
Keylet const checkKeylet = keylet::check(account_, seq);
// Use it to create or access the entry
auto sleCheck = std::make_shared<SLE>(checkKeylet);
// or
auto sleCheck = view().peek(checkKeylet);
// Add to owner's directory
auto const page = view().dirInsert(
keylet::ownerDir(account_), // Directory to add to
sleCheck->key(), // Key of the new entry
describeOwnerDir(account_)); // Description callback
if (!page)
return tecDIR_FULL; // Directory has too many entries
// Store the page number in the entry for later removal
sleCheck->setFieldU64(sfOwnerNode, *page);
// Add to destination's directory
if (dstAccountId != account_)
{
auto const dstPage = view().dirInsert(
keylet::ownerDir(dstAccountId),
sleCheck->key(),
describeOwnerDir(dstAccountId));
if (!dstPage)
return tecDIR_FULL;
sleCheck->setFieldU64(sfDestinationNode, *dstPage);
}
// Add to source's directory
auto const srcPage = view().dirInsert(
keylet::ownerDir(account_),
sleCheck->key(),
describeOwnerDir(account_));
if (!srcPage)
return tecDIR_FULL;
sleCheck->setFieldU64(sfOwnerNode, *srcPage);
// Remove from owner's directory
view().dirRemove(
keylet::ownerDir(owner),
sle->getFieldU64(sfOwnerNode), // Page where it was stored
sle->key(), // Key of the entry
false); // Don't keep empty root
// Remove from destination's directory if applicable
if (sle->isFieldPresent(sfDestinationNode))
{
view().dirRemove(
keylet::ownerDir(destination),
sle->getFieldU64(sfDestinationNode),
sle->key(),
false);
}
// Get the account entry
auto const sle = view().peek(keylet::account(account_));
// Increment owner count
adjustOwnerCount(view(), sle, 1, j_);
// Get the account entry
auto const sle = view().peek(keylet::account(owner));
// Decrement owner count
adjustOwnerCount(view(), sle, -1, j_);
TER CreateCheck::doApply()
{
auto const sle = view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL;
// Calculate reserve with one additional object
STAmount const reserve{
view().fees().accountReserve(
sle->getFieldU32(sfOwnerCount) + 1)};
// Check against balance BEFORE fee deduction
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
// Proceed with creating the object...
}
// Check if an issuer has globally frozen
bool isGlobalFrozen(ReadView const& view, AccountID const& issuer);
// Check if a specific trust line is frozen
bool isFrozen(
ReadView const& view,
AccountID const& account,
Currency const& currency,
AccountID const& issuer);
// Check if a time has passed (uses parent close time)
bool hasExpired(
ReadView const& view,
std::optional<std::uint32_t> const& exp);