# State Modification Patterns

[← Back to Transactors: Understanding the Lifecycle of a Transaction](/core-dev-bootcamp/module03bis.md)

***

### Introduction

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`:

```cpp
class ReadView
{
public:
    // Read a ledger entry (returns nullptr if not found)
    virtual std::shared_ptr<SLE const> read(Keylet const& k) const = 0;

    // Check if an entry exists
    virtual bool exists(Keylet const& k) const = 0;

    // Get current fees
    virtual Fees const& fees() const = 0;

    // Get current rules (amendments)
    virtual Rules const& rules() const = 0;

    // Get ledger sequence
    virtual LedgerIndex seq() const = 0;
};
```

**Example usage in preclaim:**

```cpp
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();
    // ...
}
```

#### ApplyView

Read/write access to ledger state. Used in `doApply`:

```cpp
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;
};
```

#### 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

```cpp
// 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);
```

#### Reading an SLE

```cpp
// 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);
```

#### Modifying an SLE

```cpp
// 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);
```

#### Deleting an SLE

```cpp
// 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);
```

***

### Keylets

Keylets are typed wrappers around ledger entry keys. They combine a type and a key, ensuring type safety when accessing ledger entries.

```cpp
// Common keylet functions
keylet::account(AccountID const& id);           // Account root
keylet::check(AccountID const& id, std::uint32_t seq);  // Check
keylet::offer(AccountID const& id, std::uint32_t seq);  // Offer
keylet::line(AccountID const& a, AccountID const& b, Currency const& c);  // Trust line
keylet::ownerDir(AccountID const& id);          // Owner directory
keylet::escrow(AccountID const& src, std::uint32_t seq);  // Escrow
keylet::payChan(AccountID const& src, AccountID const& dst, std::uint32_t seq);  // Payment channel
```

**Example:**

```cpp
// 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);
```

***

### 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:

```cpp
// 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);
```

For objects that relate to two accounts (like Checks), add to both directories:

```cpp
// 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);
```

#### Removing from a Directory

When deleting a ledger object, remove it from all directories:

```cpp
// 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);
}
```

***

### 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:

```cpp
// Get the account entry
auto const sle = view().peek(keylet::account(account_));

// Increment owner count
adjustOwnerCount(view(), sle, 1, j_);
```

#### Decrementing Owner Count

When deleting an owned object:

```cpp
// Get the account entry
auto const sle = view().peek(keylet::account(owner));

// Decrement owner count
adjustOwnerCount(view(), sle, -1, j_);
```

#### The adjustOwnerCount Function

```cpp
void adjustOwnerCount(
    ApplyView& view,
    std::shared_ptr<SLE> const& sle,
    std::int32_t amount,  // +1 or -1
    beast::Journal j);
```

This function:

1. Gets the current owner count
2. Adds the adjustment
3. Updates the account SLE

***

### Reserve Checking

Before creating a new object, verify the account can afford the increased reserve:

```cpp
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...
}
```

**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:

1. All state changes are reverted
2. The fee is still charged
3. The sequence number is still consumed

This ensures that failed transactions never leave the ledger in an inconsistent state.

#### How Atomicity Works

1. **Staging**: Changes are made to a view layer, not the actual ledger
2. **Validation**: All invariants are checked
3. **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

```cpp
// Get XRP balance
STAmount accountHolds(
    ReadView const& view,
    AccountID const& account,
    Currency const& currency,
    AccountID const& issuer,
    FreezeHandling zeroIfFrozen,
    beast::Journal j);

// Get liquid XRP (available after reserves)
XRPAmount xrpLiquid(
    ReadView const& view,
    AccountID const& id,
    std::int32_t ownerCountAdj,  // Adjust for pending changes
    beast::Journal j);
```

#### Checking Freeze Status

```cpp
// 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);
```

#### Checking Expiration

```cpp
// Check if a time has passed (uses parent close time)
bool hasExpired(
    ReadView const& view,
    std::optional<std::uint32_t> const& exp);
```

***

### Best Practices

1. **Always check entry existence**: Use `peek()` or `read()` before accessing fields
2. **Use keylets for type safety**: Don't construct keys manually
3. **Update directories on create/delete**: Maintain bidirectional links
4. **Update owner count on create/delete**: Keep reserve calculations correct
5. **Check reserves before creating**: Prevent `tecINSUFFICIENT_RESERVE` failures
6. **Remove from directories before erasing**: Clean up all references
7. **Use `mPriorBalance` for reserve checks**: Allow fee payment from reserves

***

### Codebase References

| File                               | Description            |
| ---------------------------------- | ---------------------- |
| `include/xrpl/ledger/ReadView.h`   | ReadView interface     |
| `include/xrpl/ledger/ApplyView.h`  | ApplyView interface    |
| `include/xrpl/ledger/View.h`       | View utility functions |
| `src/xrpld/ledger/detail/View.cpp` | View implementation    |
| `include/xrpl/protocol/Indexes.h`  | Keylet definitions     |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.xrpl-commons.org/core-dev-bootcamp/module03bis/state-modification.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
