← Back to Transactors: Understanding the Lifecycle of a Transaction
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:
Creates a new Check ledger object
Links the Check to both sender's and recipient's owner directories
Increments the sender's owner count
Specifies a maximum amount that can be cashed
Transaction Fields:
The sender creating the check
The recipient who can cash the check
Maximum amount that can be cashed
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.
Check 1: Self-Send Prevention
A check to yourself is redundant—you can just keep the money:
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:
isLegalNet() checks that:
The amount isn't negative
The amount doesn't overflow
signum() <= 0 ensures the amount is positive (not zero).
Check 3: Currency Validation
The currency code must be valid:
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:
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.
Check 1: Destination Account Existence
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:
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:
Check 4: Destination Tag Requirement
Some accounts require a destination tag:
Check 5: Freeze Status
For non-XRP amounts, check that the asset isn't frozen:
Global Freeze: The issuer has frozen all holdings of this currency.
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.
Check 6: Expiration
Don't create a check that's already expired:
hasExpired() compares the expiration against the parent ledger's close time.
Phase 3: doApply
doApply modifies the ledger state.
Step 1: Verify Account Exists
This should never fail—if we got this far, the account exists. tefINTERNAL indicates a bug.
Step 2: Check 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
The check's key is derived from the creator's account and the transaction sequence (or ticket number).
Step 4: Set Required Fields
Step 5: Set Optional Fields
The [~sfField] pattern returns std::optional, allowing conditional field setting.
Step 6: Insert into Ledger
This adds the new SLE to the view's modified set.
Step 7: Add to Destination Directory
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
The check is also added to the source's directory—they need to track checks they've created.
Step 9: Update Owner Count
Increment the source's owner count. This affects their reserve requirement.
Complete Transaction Flow Diagram
Ledger State Changes
Before CheckCreate:
After CheckCreate (fee = 12 drops):
Key Patterns Demonstrated
Stateless validation in preflight: No ledger access
Amendment checking: Using ctx.view.rules().enabled()
Freeze checking: Both global and trust line freezes
Reserve management: Check before creating objects
Directory management: Add to both source and destination
Owner count management: Increment when creating objects
Optional field handling: Using [~sfField] pattern
Appropriate error codes: tem* for format, tec* for state
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
Trace a failed CheckCreate: Walk through what happens when:
Destination doesn't exist
Compare with CancelCheck: How does deletion differ from creation?
Implement logging: Add detailed trace logs to follow execution
Codebase References
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
src/xrpld/app/tx/detail/CancelCheck.cpp
CancelCheck for comparison
include/xrpl/protocol/Indexes.h
Keylet definitions including keylet::check