> For the complete documentation index, see [llms.txt](https://docs.xrpl-commons.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.xrpl-commons.org/core-dev-bootcamp/module03bis/case-study-checkcreate.md).

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