stellar-xdr-lens

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: Apache-2.0

README

stellar-xdr-lens

A Go CLI and library that explains Stellar and Soroban XDR in plain English, and diffs it structurally.

CI Go Reference Go Report Card License

Why this exists

When a Stellar transaction fails, you get a result code like tx_failed wrapped around opINNER wrapped around InvokeHostFunctionResultCodeInvokeHostFunctionTrapped. Working out which operation failed, what it was trying to do, and what to do about it means decoding two separate blobs and correlating them by hand.

lens does that correlation for you:

$ lens explain "$ENVELOPE" --result "$RESULT"
Transaction failed at operation 0, invoke_host_function (call harvest on contract CBGSB…KKY3 with 2 argument(s)): The contract trapped: it panicked, or explicitly returned an error.
Honest scope: what already exists

The official stellar CLI already ships stellar xdr decode, guess, compare and encode. If you just need to turn a blob into JSON, use that — it is maintained by the SDF, covers every XDR type in the protocol, and is probably already installed.

lens exists for the three things that tool does not do:

stellar xdr lens
Decode to JSON
Guess the type
Explain a failure in plain English
Pair an envelope with its result
Structural, path-level diff ordering only (-1/0/1) ✅ full path diff
Reusable Go library ❌ (Rust) pkg/lens

That last row matters: Horizon, ingestion pipelines and most Stellar backend infrastructure are written in Go, and there was no Go library for any of this.

Install

go install github.com/stellar-optics/stellar-xdr-lens/cmd/lens@latest

Requires Go 1.25 or later. Or build from source:

git clone https://github.com/stellar-optics/stellar-xdr-lens
cd stellar-xdr-lens
go build ./cmd/lens

Everything runs offline. lens makes no network calls, which makes it safe on unsigned transactions and deterministic in CI.

Usage

lens explain — why did this fail?

Pair an envelope with its result and lens names the failing operation, what it was attempting, and what to do next:

$ lens explain --file envelope.xdr --result-file result.xdr
Transaction failed at operation 0, invoke_host_function (call harvest on contract CBGSB…KKY3 with 2 argument(s)): The contract trapped: it panicked, or explicitly returned an error.

  Source           GAPB7YMUQ6MPQ23PTFDNBZP7HGGTZPI2DT5JJURUOIXNPLGHW2O3WYXZ
  Fee-bump source  GA2JRQOF6EA3HQWDCEDBPPMLYPJCFLDDGYZLEQGMS5SOBQIB3BAFHVAW
  Sequence         264125967419700139
  Fee bid          0.0024167 XLM (24167 stroops)
  Fee charged      0.0013355 XLM (13355 stroops)
  Signatures       1
  Precondition     valid until 2026-08-03T13:33:39Z

Operations (1)
  ✗ [0] invoke_host_function — call harvest on contract CBGSB…KKY3 with 2 argument(s)
      invoke_host_function_trapped: The contract trapped: it panicked, or explicitly returned an error.
      → This is a failure inside the contract itself, not a protocol-level rejection. Run the call through simulation and read the diagnostic events to find the contract-level cause.

Outcome
  tx_fee_bump_inner_failed: The fee-bump wrapper was valid, but the inner transaction it paid for failed.
    → The fee was still charged to the fee-bump source. Inspect the inner result for the real cause.
  inner transaction:
  tx_failed: The transaction was well-formed and authorised, but at least one of its operations failed.
    → Check the per-operation results: the transaction itself is fine, the failure is inside an operation.

All 208 protocol result codes have a written explanation. Use --fail-on-error to exit non-zero when the transaction failed, which makes this usable as a CI gate.

lens decode — read the structure
$ lens decode --depth 3 --file envelope.xdr
TransactionEnvelope
├─ Type: EnvelopeTypeEnvelopeTypeTx
└─ V1
   ├─ Tx
   │  ├─ SourceAccount: GCJNHFXJEQEY54NPKU4DBMT5WOBJUHMYSFBDAQF4OMTEGJN4BQU7ZST6
   │  ├─ Fee: 58002
   │  ├─ SeqNum: 259021488982594615
   │  ├─ Cond
   │  │  ... 1 more level(s) hidden
   │  ├─ Memo
   │  │  ... 1 more level(s) hidden
   │  ├─ Operations  [1]
   │  │  ... 1 more level(s) hidden
   │  └─ Ext
   │     ... 1 more level(s) hidden
   └─ Signatures  [1]
      └─ [0]
         ... 1 more level(s) hidden

Accounts render as strkey, amounts as XLM, timestamps as RFC 3339, and enums as their constant names — not raw byte arrays and integers.

lens diff — what changed?
$ lens diff before.xdr after.xdr
TransactionResult

~ FeeCharged
    - 35438 0.0035438 (stroops: 35438)
    + 13355 0.0013355 (stroops: 13355)
~ Result.Code
    - TransactionResultCodeTxFeeBumpInnerSuccess
    + TransactionResultCodeTxFeeBumpInnerFailed
~ Result.InnerResultPair.Result.Result.Results[0].Tr.InvokeHostFunctionResult.Code
    - InvokeHostFunctionResultCodeInvokeHostFunctionSuccess
    + InvokeHostFunctionResultCodeInvokeHostFunctionTrapped
- Result.InnerResultPair.Result.Result.Results[0].Tr.InvokeHostFunctionResult.Success = ea29da78…

5 changed, 1 removed

Use --exit-code to exit non-zero when the values differ, like diff(1).

Piping and scripting

lens reads from an argument, a --file, or stdin, so it composes:

# Explain a live transaction straight from Horizon
curl -s "https://horizon.stellar.org/transactions/$HASH" \
  | jq -r '.envelope_xdr' \
  | lens explain

# Pull one field out with jq
lens decode --json "$XDR" | jq -r '.value.V1.Tx.Fee'

# Gate a deployment on the transaction having succeeded
lens explain "$ENV" --result "$RES" --fail-on-error || exit 1

The --json output has a documented, stable shape:

{
  "type": "TransactionResult",
  "value": { "FeeCharged": 13355, "Result": { "Code": "TransactionResultCodeTxFeeBumpInnerFailed" } }
}

Absent union arms are omitted rather than emitted as null, so jq paths stay short.

explain --json has its own documented shape, in camelCase to match Horizon and Soroban RPC:

{
  "kind": "transaction",
  "headline": "Transaction failed at operation 0, invoke_host_function …",
  "fee": 24167,
  "feeCharged": 13355,
  "operations": [
    { "index": 0, "type": "invoke_host_function",
      "result": { "code": "invoke_host_function_trapped", "summary": "…", "hint": "…" } }
  ],
  "outcome": {
    "success": false,
    "reason": { "code": "tx_fee_bump_inner_failed", "…": "…" },
    "innerReason": { "code": "tx_failed", "…": "…" },
    "failedOperations": [0]
  }
}

These field names are part of the public interface — sibling tools and scripts address them with jq, so renaming one is a breaking change.

Library use

The CLI is a thin shell over pkg/lens:

package main

import (
	"fmt"
	"log"

	"github.com/stellar-optics/stellar-xdr-lens/pkg/lens"
)

func main() {
	env, err := lens.Decode(envelopeB64)
	if err != nil {
		log.Fatal(err)
	}
	res, err := lens.Decode(resultB64)
	if err != nil {
		log.Fatal(err)
	}

	summary, err := lens.ExplainPair(env, res)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(summary.Headline)
	for _, op := range summary.Operations {
		if op.Result != nil && !op.Result.Success {
			fmt.Printf("op %d (%s) failed: %s\n", op.Index, op.Type, op.Result.Summary)
		}
	}
}

See the API reference and docs/architecture.md.

A note on type detection

lens decode detects the XDR type by trying each known type and keeping the ones that decode cleanly, relying on the SDK rejecting input it does not fully consume. On transaction-sized payloads this is reliable — the test suite asserts that every real fixture matches exactly one type.

Short values are genuinely ambiguous, though. An empty Memo and a native Asset are both AAAAAA==, because they are the same four bytes. lens reports this rather than guessing silently:

$ lens decode --candidates "AAAAAA=="
* Memo
  Asset

2 types matched; the starred one is used by default. Use --type to choose.

Pass --type when you need certainty.

Project status

Early but real. v0.1 does what this README says, with tests over real captured mainnet XDR. The library API may still shift before v1.0 — it is not yet frozen.

  • Supports 25 XDR types out of the ~400 in the protocol, chosen to cover what developers actually paste into a terminal. Adding more is a one-line change; see docs/architecture.md.
  • All 208 protocol result codes are explained.
  • No network access anywhere in the library or the tests.

See docs/roadmap.md for what is planned and what is deliberately out of scope.

Contributing

Contributions are welcome — especially new XDR type handlers and better failure explanations, both of which are designed to be one-file changes. See CONTRIBUTING.md.

License

Apache-2.0. See LICENSE.

Directories

Path Synopsis
cmd
lens command
Command lens decodes, explains and diffs Stellar and Soroban XDR.
Command lens decodes, explains and diffs Stellar and Soroban XDR.
internal
cli
Package cli implements the lens command-line interface.
Package cli implements the lens command-line interface.
pkg
lens/format
Package format renders decoded XDR into human- and machine-readable output.
Package format renders decoded XDR into human- and machine-readable output.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL