Inspect exactly how an XRP Ledger transaction changed ledger state.
xrpl-state-diff is a small, developer-focused Rust tool that converts an
XRPL transaction and its metadata into a human-readable and machine-readable
ledger state diff.
Instead of manually reading AffectedNodes, the tool answers a simpler question:
What changed because of this transaction?
Payment · tesSUCCESS
Ledger 106,187,291 · 043D9639…26894 · rLBSbJgw…V8kYj
STATE CHANGES
────────────────────────────────────────────────────────────────
AccountRoot · modified (3386568A…0633B)
r4hck56M…CXG4C
Balance 71,712.473095 XRP → 71,712.473097 XRP
AccountRoot · modified (7EFA8F6F…57635)
rLBSbJgw…V8kYj
Balance 29.00298 XRP → 29.002967 XRP
Sequence 101187494 → 101187495
FEE
0.000011 XRP
SUMMARY
2 modified · 0 created · 0 deleted
Fee: 0.000011 XRP
The XRP Ledger exposes detailed transaction metadata and ledger state, but raw
metadata is optimized for machines rather than humans. A transaction can modify
several different ledger objects. Understanding those changes currently
requires manually interpreting AffectedNodes, PreviousFields, FinalFields,
CreatedNode, ModifiedNode, and DeletedNode.
xrpl-state-diff makes that state transition explicit — a deterministic,
field-level before/after view of every ledger object a transaction touched.
- Inspect an XRPL transaction by hash
- Fetch transaction metadata from an XRPL JSON-RPC server
- Decode
AffectedNodes(CreatedNode,ModifiedNode,DeletedNode) - Field-level before/after changes, including unchanged-field mode
- Normalized XRP / IOU amounts with float-free decimal arithmetic
- Decodes hex-encoded currency codes (e.g.
4F70756C656E6365…→Opulence) - Compact summaries for array fields (e.g.
10 NFTs → 11 NFTs) - Concise terminal output (auto-colored,
NO_COLORaware) - Deterministic machine-readable JSON for pipelines and tests
- Minimal HTMX web interface (axum, server-rendered, no SPA)
- Configurable XRPL endpoints (flag,
XRPL_ENDPOINTenv, or default) - Offline fixture-driven test suite (
cargo testneeds no network)
Requires a stable Rust toolchain (1.75+).
cargo build --release
./target/release/xrpl-state-diff --helpOr install directly:
cargo install --path .# human-readable diff
xrpl-state-diff tx 043D9639920D08BCDBE212CABBA685C62310096F5AF7956C4A312EAEF5826894
# deterministic JSON
xrpl-state-diff tx 043D9639920D08BCDBE212CABBA685C62310096F5AF7956C4A312EAEF5826894 --json
# also list fields that did not change
xrpl-state-diff tx <hash> --show-unchanged
# point at a different node
xrpl-state-diff tx <hash> --endpoint https://s2.ripple.com:51234
# or: export XRPL_ENDPOINT=https://my-private-node.example:51234
# disable colors
xrpl-state-diff tx <hash> --no-colorxrpl-state-diff web --bind 127.0.0.1:8080
# open http://127.0.0.1:8080Paste a transaction hash into the form, or click one of the real example transactions. Results are fetched server-side and swapped in via HTMX; no frontend build step, no JavaScript framework.
# simple XRP payment
xrpl-state-diff tx 043D9639920D08BCDBE212CABBA685C62310096F5AF7956C4A312EAEF5826894
# payment that funds a brand-new account (CreatedNode)
xrpl-state-diff tx 018A90722173F75E9E7F1A9AE4A6D716149F89B43E5E1C878ACB8A39768234DA
# cross-currency payment (RippleState changes)
xrpl-state-diff tx 164CA8806FD55BE41BE6976126B033171D6B2FC26A94A340B6697CA0C27C9CEF
# OfferCreate with a hex-encoded currency ("Opulence")
xrpl-state-diff tx 0BE21941603B16EF6A9BE477B1BB490A0CD054684DA50F17C4762C8761875002
# OfferCancel that deletes an offer
xrpl-state-diff tx 3511CDEDCE001D71108801B34DB6D43429E5C1083B119AE17FC42907A5C1BBE0
# NFTokenMint (NFTokenPage with 10 → 11 tokens)
xrpl-state-diff tx 8A463BD654D254DF5E1D511E5893C4D833DF294C75B2FD88F4F4FB14E681D635
# AccountDelete
xrpl-state-diff tx 8DB4A3EDBD5B42F49338E337CDA3564508F8995E5DEDEF5D7956967EA7F75AB3--json emits a deterministic document (same metadata in → same JSON out).
Keys use camelCase to mirror XRPL conventions. Raw values are preserved
alongside human-formatted strings so pipelines never lose fidelity.
{
"format": "xrpl-state-diff",
"version": 1,
"transaction": {
"hash": "043D9639…",
"type": "Payment",
"result": "tesSUCCESS",
"account": "rLBSbJgwwVbbrf9eLcK23mWeVSDQwV8kYj",
"fee": "0.000011 XRP",
"feeDrops": "11",
"ledgerIndex": 106187291,
"date": 839633451,
"validated": true
},
"changes": [
{
"ledgerIndex": "7EFA8F6F…",
"entryType": "AccountRoot",
"operation": "modified",
"label": "rLBSbJgw…V8kYj",
"fields": [
{
"field": "Balance",
"kind": "changed",
"before": "29002980",
"after": "29002967",
"amount": "xrp",
"beforeFormatted": "29.00298 XRP",
"afterFormatted": "29.002967 XRP"
}
]
}
],
"summary": { "created": 0, "modified": 2, "deleted": 0 }
}Field semantics:
kind—added(created objects),removed(deleted objects orPreviousFields-only fields),changed.amount—xrp(drops ÷ 10⁶),iou(decimal value),iouRaw(integer ÷ 10¹⁵, older RippleState balances),raw.before/after— raw JSON values (null/absent for added/removed).beforeFormatted/afterFormatted— human-readable renderings.unchanged— present on each change only with--show-unchanged.
Everything is jq-friendly:
xrpl-state-diff tx <hash> --json | jq '.changes[] | select(.entryType == "AccountRoot")'| Setting | Flag | Env var | Default |
|---|---|---|---|
| XRPL JSON-RPC endpoint | --endpoint URL |
XRPL_ENDPOINT |
https://s1.ripple.com:51234 |
The tool is stateless and local-first: no account, no API key, no hosted service.
Nothing needs to be installed beyond a Rust build, and no keys, seeds or secrets are required. The tool only reads ledger state — it never signs or submits transactions.
-
Endpoint selection — the default public Clio node works for casual use. For production workloads, prefer:
- your own
rippled/clionode (e.g.http://internal-node:51234), or - a reputable public aggregator (e.g.
https://xrplcluster.com). SetXRPL_ENDPOINTin your environment so no flag is needed.
- your own
-
Rate limits — public nodes throttle abusive traffic. If you expect high volume, run your own node.
-
Data availability — nodes only serve data they have. Full-history nodes (
s2.ripple.com, or a node with--ledger_historytuned) can answer for older ledgers; Clio nodes cache recent history. -
Web UI deployment — bind to a specific address, put it behind a TLS reverse proxy, and keep
XRPL_ENDPOINTpointed at your node:export XRPL_ENDPOINT=http://internal-node:51234 xrpl-state-diff web --bind 127.0.0.1:8080 -
Unvalidated transactions —
txmay return unvalidated data; the JSON output reportsvalidatedso pipelines can filter it.
XRPL JSON-RPC ("tx" method)
│
▼
Transaction + Metadata
│
▼
AffectedNode parser (CreatedNode / ModifiedNode / DeletedNode)
│
▼
Normalized field-level state changes
├── Human-readable CLI
├── Deterministic JSON
└── HTMX web UI
Key XRPL facts the parser handles:
AffectedNodesentries are{ "CreatedNode" | "ModifiedNode" | "DeletedNode": { ... } }CreatedNodecarriesNewFields(the object's initial state)ModifiedNodecarriesPreviousFields(only the fields that changed) andFinalFields(the full after-state); fields only inFinalFieldsare unchangedDeletedNodemay carry bothPreviousFieldsandFinalFields- Metadata appears under
meta(Clio/modern rippled) ormetaData(older rippled); both are accepted LedgerIndexmay be a hex string or a number- XRP amounts are integer drops; IOU amounts are decimal strings or
{currency, issuer, value}objects; currencies may be hex-encoded
cargo test # offline — fixtures are checked in
cargo clippy --all-targets -- -D warnings
cargo fmt --checkfixtures/ contains real mainnet transaction responses covering:
| Fixture | Transaction | Covers |
|---|---|---|
payment.json |
Payment | 2 AccountRoots modified, fee |
payment-create-account |
Payment | CreatedNode (new AccountRoot) |
payment-cross-currency |
Payment | RippleState balance changes |
offer-create |
OfferCreate | Created Offer, hex currency |
offer-cancel |
OfferCancel | DeletedNode (Offer) |
trust-set |
TrustSet | AccountRoot modification |
nft-mint |
NFTokenMint | NFTokens array summary |
nft-create-offer |
NFTokenCreateOffer | Created NFTokenOffer |
account-set |
AccountSet | Simple modification |
account-delete |
AccountDelete | Deleted AccountRoot with diff |
check-create |
CheckCreate | Created Check |
ticket-create |
TicketCreate | Created Ticket |
To add a fixture, fetch a transaction and save the full tx response:
curl -s https://s1.ripple.com:51234 \
-H 'Content-Type: application/json' \
-d '{"method":"tx","params":[{"transaction":"<hash>","binary":false}]}' \
> fixtures/my-case.jsonThen add an assertion in tests/diff_tests.rs. scripts/fetch_fixtures.sh
automates gathering a varied set from a live node.
src/
├── main.rs # binary entry point
├── cli.rs # clap CLI definitions
├── config.rs # endpoint resolution, hash validation
├── error.rs # error type
├── xrpl/
│ ├── client.rs # minimal JSON-RPC client (tx method)
│ └── model.rs # lenient tx/meta parsing
├── diff/
│ ├── parser.rs # AffectedNodes → normalized LedgerChange
│ ├── normalize.rs # field-level diffs + summary
│ └── format.rs # amount classification & float-free formatting
├── output/
│ ├── terminal.rs # human-readable renderer
│ └── json.rs # deterministic JSON renderer
└── web/
├── routes.rs # axum routes
└── view.rs # template-ready view model
templates/ # askama + HTMX templates
fixtures/ # offline test fixtures (real mainnet data)
scripts/ # fixture-fetching helper
This is a diagnostic tool, not an explorer or blockchain analytics platform.
Given the same transaction metadata, the same state diff is produced.
No account, API key, hosted service or proprietary backend.
JSON output is suitable for shell pipelines, data-processing jobs and other developer tools.
Output exposes XRPL concepts such as ledger entry types rather than hiding everything behind generic blockchain abstractions.
xrpl-state-diff is deliberately not: a blockchain explorer, an indexer, a
wallet, a trading terminal, an analytics platform, an SDK replacement, a
transaction simulator, an archival database, a hosted SaaS service. It has no
database, no authentication, no accounts, no signing, no submission, and no
"AI explanation" — the value is the correct deterministic state transition.
MIT