# Case Study: CheckCreate Transactor

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

***

### Introduction

The CheckCreate transaction demonstrates all the essential patterns for implementing a transactor in rippled. Checks are deferred payment instructions—similar to paper checks—that allow a sender to authorize a payment that the recipient can later cash.

This case study provides a detailed, code-level walkthrough of the CheckCreate implementation, examining every validation step and state modification. By understanding this transaction, you'll have a template for implementing any new transaction type.

***

### What CheckCreate Does

A CheckCreate transaction:

1. Creates a new Check ledger object
2. Links the Check to both sender's and recipient's owner directories
3. Increments the sender's owner count
4. Specifies a maximum amount that can be cashed

**Transaction Fields:**

| Field            | Required | Description                          |
| ---------------- | -------- | ------------------------------------ |
| `Account`        | Yes      | The sender creating the check        |
| `Destination`    | Yes      | The recipient who can cash the check |
| `SendMax`        | Yes      | Maximum amount that can be cashed    |
| `Expiration`     | No       | When the check expires               |
| `DestinationTag` | No       | Tag for the destination              |
| `SourceTag`      | No       | Tag for the source                   |
| `InvoiceID`      | No       | Arbitrary reference ID               |

***

### Source Files

* **Header**: `src/xrpld/app/tx/detail/CreateCheck.h`
* **Implementation**: `src/xrpld/app/tx/detail/CreateCheck.cpp`

***

### Phase 1: Preflight

Preflight performs stateless validation on the transaction content.

```cpp
NotTEC
CreateCheck::preflight(PreflightContext const& ctx)
{
```

#### Check 1: Self-Send Prevention

A check to yourself is redundant—you can just keep the money:

```cpp
    if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
    {
        JLOG(ctx.j.warn()) << "Malformed transaction: Check to self.";
        return temREDUNDANT;
    }
```

**Why `temREDUNDANT`?** This is a permanent format error. The transaction can never be valid with these field values.

#### Check 2: SendMax Validation

The amount must be positive and well-formed:

```cpp
    {
        STAmount const sendMax{ctx.tx.getFieldAmount(sfSendMax)};
        if (!isLegalNet(sendMax) || sendMax.signum() <= 0)
        {
            JLOG(ctx.j.warn()) << "Malformed transaction: bad sendMax amount: "
                               << sendMax.getFullText();
            return temBAD_AMOUNT;
        }
```

`isLegalNet()` checks that:

* The amount isn't negative
* The amount doesn't overflow
* The precision is valid

`signum() <= 0` ensures the amount is positive (not zero).

#### Check 3: Currency Validation

The currency code must be valid:

```cpp
        if (badCurrency() == sendMax.getCurrency())
        {
            JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency.";
            return temBAD_CURRENCY;
        }
    }
```

`badCurrency()` returns a special invalid currency code. This catches malformed currency specifications.

#### Check 4: Expiration Validation

If an expiration is provided, it must not be zero:

```cpp
    if (auto const optExpiry = ctx.tx[~sfExpiration])
    {
        if (*optExpiry == 0)
        {
            JLOG(ctx.j.warn()) << "Malformed transaction: bad expiration";
            return temBAD_EXPIRATION;
        }
    }

    return tesSUCCESS;
}
```

**Note:** The `~` operator returns `std::optional<T>`, allowing us to check if the field is present.

***

### Phase 2: Preclaim

Preclaim validates against the current ledger state.

```cpp
TER
CreateCheck::preclaim(PreclaimContext const& ctx)
{
```

#### Check 1: Destination Account Existence

```cpp
    AccountID const dstId{ctx.tx[sfDestination]};
    auto const sleDst = ctx.view.read(keylet::account(dstId));
    if (!sleDst)
    {
        JLOG(ctx.j.warn()) << "Destination account does not exist.";
        return tecNO_DST;
    }
```

**Why `tecNO_DST`?** The destination doesn't exist *now*, but it could be created before this transaction is applied. However, the transaction will fail if applied without the destination.

#### Check 2: DisallowIncoming Permission

Accounts can opt out of receiving certain objects:

```cpp
    auto const flags = sleDst->getFlags();

    if (ctx.view.rules().enabled(featureDisallowIncoming) &&
        (flags & lsfDisallowIncomingCheck))
        return tecNO_PERMISSION;
```

This is amendment-gated (`featureDisallowIncoming`), so we only check if the amendment is enabled.

#### Check 3: Pseudo-Account Prevention

Pseudo-accounts (like AMM pools) cannot cash checks:

```cpp
    if (isPseudoAccount(sleDst))
        return tecNO_PERMISSION;
```

#### Check 4: Destination Tag Requirement

Some accounts require a destination tag:

```cpp
    if ((flags & lsfRequireDestTag) && !ctx.tx.isFieldPresent(sfDestinationTag))
    {
        JLOG(ctx.j.warn()) << "Malformed transaction: DestinationTag required.";
        return tecDST_TAG_NEEDED;
    }
```

#### Check 5: Freeze Status

For non-XRP amounts, check that the asset isn't frozen:

```cpp
    {
        STAmount const sendMax{ctx.tx[sfSendMax]};
        if (!sendMax.native())
        {
            // Check global freeze
            AccountID const& issuerId{sendMax.getIssuer()};
            if (isGlobalFrozen(ctx.view, issuerId))
            {
                JLOG(ctx.j.warn()) << "Creating a check for frozen asset";
                return tecFROZEN;
            }
```

**Global Freeze**: The issuer has frozen all holdings of this currency.

```cpp
            // Check source trust line freeze
            AccountID const srcId{ctx.tx.getAccountID(sfAccount)};
            if (issuerId != srcId)
            {
                auto const sleTrust = ctx.view.read(
                    keylet::line(srcId, issuerId, sendMax.getCurrency()));
                if (sleTrust &&
                    sleTrust->isFlag(
                        (issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze))
                {
                    JLOG(ctx.j.warn())
                        << "Creating a check for frozen trustline.";
                    return tecFROZEN;
                }
            }
```

**Trust Line Freeze**: The issuer has frozen the source's specific trust line.

The `(issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze` pattern is due to how trust lines store flags—the "high" account's flags use different bits than the "low" account's flags.

```cpp
            // Check destination trust line freeze
            if (issuerId != dstId)
            {
                auto const sleTrust = ctx.view.read(
                    keylet::line(issuerId, dstId, sendMax.getCurrency()));
                if (sleTrust &&
                    sleTrust->isFlag(
                        (dstId > issuerId) ? lsfHighFreeze : lsfLowFreeze))
                {
                    JLOG(ctx.j.warn())
                        << "Creating a check for destination frozen trustline.";
                    return tecFROZEN;
                }
            }
        }
    }
```

#### Check 6: Expiration

Don't create a check that's already expired:

```cpp
    if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
    {
        JLOG(ctx.j.warn()) << "Creating a check that has already expired.";
        return tecEXPIRED;
    }
    return tesSUCCESS;
}
```

`hasExpired()` compares the expiration against the parent ledger's close time.

***

### Phase 3: doApply

doApply modifies the ledger state.

```cpp
TER
CreateCheck::doApply()
{
```

#### Step 1: Verify Account Exists

```cpp
    auto const sle = view().peek(keylet::account(account_));
    if (!sle)
        return tefINTERNAL;
```

This should never fail—if we got this far, the account exists. `tefINTERNAL` indicates a bug.

#### Step 2: Check Reserve

```cpp
    {
        STAmount const reserve{
            view().fees().accountReserve(sle->getFieldU32(sfOwnerCount) + 1)};

        if (mPriorBalance < reserve)
            return tecINSUFFICIENT_RESERVE;
    }
```

Calculate what the reserve will be with one more owned object. Use `mPriorBalance` (before fee) to allow dipping into reserve for fees.

#### Step 3: Create the Check SLE

```cpp
    std::uint32_t const seq = ctx_.tx.getSeqValue();
    Keylet const checkKeylet = keylet::check(account_, seq);
    auto sleCheck = std::make_shared<SLE>(checkKeylet);
```

The check's key is derived from the creator's account and the transaction sequence (or ticket number).

#### Step 4: Set Required Fields

```cpp
    sleCheck->setAccountID(sfAccount, account_);
    AccountID const dstAccountId = ctx_.tx[sfDestination];
    sleCheck->setAccountID(sfDestination, dstAccountId);
    sleCheck->setFieldU32(sfSequence, seq);
    sleCheck->setFieldAmount(sfSendMax, ctx_.tx[sfSendMax]);
```

#### Step 5: Set Optional Fields

```cpp
    if (auto const srcTag = ctx_.tx[~sfSourceTag])
        sleCheck->setFieldU32(sfSourceTag, *srcTag);
    if (auto const dstTag = ctx_.tx[~sfDestinationTag])
        sleCheck->setFieldU32(sfDestinationTag, *dstTag);
    if (auto const invoiceId = ctx_.tx[~sfInvoiceID])
        sleCheck->setFieldH256(sfInvoiceID, *invoiceId);
    if (auto const expiry = ctx_.tx[~sfExpiration])
        sleCheck->setFieldU32(sfExpiration, *expiry);
```

The `[~sfField]` pattern returns `std::optional`, allowing conditional field setting.

#### Step 6: Insert into Ledger

```cpp
    view().insert(sleCheck);
```

This adds the new SLE to the view's modified set.

#### Step 7: Add to Destination Directory

```cpp
    auto viewJ = ctx_.app.journal("View");

    if (dstAccountId != account_)
    {
        auto const page = view().dirInsert(
            keylet::ownerDir(dstAccountId),
            checkKeylet,
            describeOwnerDir(dstAccountId));

        JLOG(j_.trace()) << "Adding Check to destination directory "
                         << to_string(checkKeylet.key) << ": "
                         << (page ? "success" : "failure");

        if (!page)
            return tecDIR_FULL;

        sleCheck->setFieldU64(sfDestinationNode, *page);
    }
```

The check is added to the destination's owner directory so they can find checks payable to them. The page number is stored in the Check for later removal.

#### Step 8: Add to Source Directory

```cpp
    {
        auto const page = view().dirInsert(
            keylet::ownerDir(account_),
            checkKeylet,
            describeOwnerDir(account_));

        JLOG(j_.trace()) << "Adding Check to owner directory "
                         << to_string(checkKeylet.key) << ": "
                         << (page ? "success" : "failure");

        if (!page)
            return tecDIR_FULL;

        sleCheck->setFieldU64(sfOwnerNode, *page);
    }
```

The check is also added to the source's directory—they need to track checks they've created.

#### Step 9: Update Owner Count

```cpp
    adjustOwnerCount(view(), sle, 1, viewJ);
    return tesSUCCESS;
}
```

Increment the source's owner count. This affects their reserve requirement.

***

### Complete Transaction Flow Diagram

```
┌─────────────────────────────────────────────────────────────────────┐
│                         CheckCreate Transaction                      │
│                                                                     │
│  Account: rAlice                                                    │
│  Destination: rBob                                                  │
│  SendMax: 100 XRP                                                   │
│  Expiration: 750000000                                              │
└─────────────────────────────────┬───────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│  PREFLIGHT                                                          │
│                                                                     │
│  ✓ rAlice ≠ rBob (not self-send)                                   │
│  ✓ 100 XRP is positive and legal                                   │
│  ✓ XRP is a valid currency                                          │
│  ✓ 750000000 ≠ 0 (valid expiration)                                │
│                                                                     │
│  Result: tesSUCCESS                                                 │
└─────────────────────────────────┬───────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│  PRECLAIM                                                           │
│                                                                     │
│  ✓ rBob exists in ledger                                           │
│  ✓ rBob doesn't have lsfDisallowIncomingCheck                      │
│  ✓ rBob is not a pseudo-account                                    │
│  ✓ rBob doesn't require destination tag (or tag provided)          │
│  ✓ XRP cannot be frozen (skip freeze checks)                       │
│  ✓ 750000000 > parent close time (not expired)                     │
│                                                                     │
│  Result: tesSUCCESS                                                 │
└─────────────────────────────────┬───────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│  DOAPPLY                                                            │
│                                                                     │
│  1. Read rAlice's AccountRoot (peek)                               │
│  2. Calculate reserve: 1 XRP + (ownerCount + 1) * 0.2 XRP          │
│  3. Verify mPriorBalance >= reserve                                │
│  4. Create Check SLE with keylet::check(rAlice, seq)               │
│  5. Set fields: Account, Destination, Sequence, SendMax            │
│  6. Set optional fields if present                                  │
│  7. Insert Check into ledger                                        │
│  8. Add Check to rBob's owner directory → get page number          │
│  9. Add Check to rAlice's owner directory → get page number        │
│  10. adjustOwnerCount(rAlice, +1)                                   │
│                                                                     │
│  Result: tesSUCCESS                                                 │
└─────────────────────────────────────────────────────────────────────┘
```

***

### Ledger State Changes

Before CheckCreate:

```
┌─────────────────────────────────┐
│  AccountRoot: rAlice            │
│  Balance: 1000 XRP              │
│  OwnerCount: 2                  │
│  Sequence: 42                   │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│  AccountRoot: rBob              │
│  Balance: 500 XRP               │
│  OwnerCount: 1                  │
└─────────────────────────────────┘

Owner Directories: (no checks)
```

After CheckCreate (fee = 12 drops):

```
┌─────────────────────────────────┐
│  AccountRoot: rAlice            │
│  Balance: 999.999988 XRP        │  ← Fee deducted
│  OwnerCount: 3                  │  ← Incremented
│  Sequence: 43                   │  ← Incremented
└─────────────────────────────────┘

┌─────────────────────────────────┐
│  AccountRoot: rBob              │
│  Balance: 500 XRP               │  ← Unchanged
│  OwnerCount: 1                  │  ← Unchanged (not owner)
└─────────────────────────────────┘

┌─────────────────────────────────┐
│  Check: (new)                   │
│  Account: rAlice                │
│  Destination: rBob              │
│  SendMax: 100 XRP               │
│  Sequence: 42                   │
│  Expiration: 750000000          │
│  OwnerNode: <page in Alice dir> │
│  DestinationNode: <page in Bob> │
└─────────────────────────────────┘

Owner Directories:
  rAlice: [..., Check(42)]
  rBob: [..., Check(42)]
```

***

### Key Patterns Demonstrated

1. **Stateless validation in preflight**: No ledger access
2. **Amendment checking**: Using `ctx.view.rules().enabled()`
3. **Freeze checking**: Both global and trust line freezes
4. **Reserve management**: Check before creating objects
5. **Directory management**: Add to both source and destination
6. **Owner count management**: Increment when creating objects
7. **Optional field handling**: Using `[~sfField]` pattern
8. **Appropriate error codes**: `tem*` for format, `tec*` for state

***

### Related Transactors

Understanding CheckCreate helps with these related transactions:

* **CashCheck**: Cashing a check (balance transfer + check deletion)
* **CancelCheck**: Canceling a check (check deletion only)
* **EscrowCreate**: Similar pattern for creating escrow objects
* **OfferCreate**: Similar pattern for creating offer objects

***

### Exercises

1. **Trace a failed CheckCreate**: Walk through what happens when:
   * Destination doesn't exist
   * Trust line is frozen
   * Insufficient reserve
2. **Compare with CancelCheck**: How does deletion differ from creation?
3. **Implement logging**: Add detailed trace logs to follow execution

***

### Codebase References

| File                                      | Description                                  |
| ----------------------------------------- | -------------------------------------------- |
| `src/xrpld/app/tx/detail/CreateCheck.cpp` | CheckCreate implementation                   |
| `src/xrpld/app/tx/detail/CreateCheck.h`   | CheckCreate class definition                 |
| `src/xrpld/app/tx/detail/CashCheck.cpp`   | CashCheck for comparison                     |
| `src/xrpld/app/tx/detail/CancelCheck.cpp` | CancelCheck for comparison                   |
| `include/xrpl/protocol/Indexes.h`         | Keylet definitions including `keylet::check` |


---

# 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/case-study-checkcreate.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.
