Skip to content

About

No description or website provided.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

stellar-focus

A Go testing toolkit for Stellar and Soroban: assert on XDR, manage golden fixtures, and capture real chain data as test data.

CI Go Reference Go Report Card License

Part of stellar-optics — Go tools that make Stellar and Soroban data legible. Siblings: stellar-xdr-lens (decode, explain, diff) and stellar-prism (tail live data).

Why this exists

Testing Go code that produces Stellar XDR means comparing two base64 strings. When they differ, assert.Equal tells you this:

    Error:  Not equal:
            expected: "AAAAAgAAAACS05bpJAmO8a9VODCyfbOCmh2YkUIwQLxzJkMlvAwp/AAAAGQAAAAAAAAAbwAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAB4f..."
            actual  : "AAAAAgAAAACS05bpJAmO8a9VODCyfbOCmh2YkUIwQLxzJkMlvAwp/AAAAGQAAAAAAAAA3gAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAB4f..."

That is technically correct and completely useless. focus tells you which field changed:

--- FAIL: TestPaymentEnvelope (0.00s)
    payment_test.go:29: AssertEnvelopeEqual: envelopes differ — 1 difference
        (ignored: **.SeqNum)

          TransactionEnvelope

          ~ V1.Tx.Operations[0].Body.PaymentOp.Amount
              - 1000000000 100.0000000 (stroops: 1000000000)
              + 2000000000 200.0000000 (stroops: 2000000000)

        want  AAAAAgAAAACS05bpJAmO8a9V…  (172 bytes base64)
        got   AAAAAgAAAACS05bpJAmO8a9V…  (172 bytes base64)
    payment_test.go:30: AssertFee: fee is 100 (0.0000100 XLM), want 200 (0.0000200 XLM)
    payment_test.go:31: AssertOperationTypes: operations are [payment], want [payment, change_trust]
          [0] payment — pay 200.0000000 XLM to GAPB7…WYXZ

That is real output from a real go test run, not a mock-up. Note what it does: names the field path, shows the amount in XLM as well as stroops, says which ignore rule fired, keeps the base64 truncated but available, and reports all three failures in one run.

Diffs are rendered by stellar-xdr-lens, so the diff in a failing test is the same one lens diff prints at the command line.

Install

go get github.com/stellar-optics/stellar-focus

Requires Go 1.25 or later.

The library carries no assertion framework, no diff library, and no CLI framework. Test helpers get pulled into other people's projects, so pkg/focus depends only on the standard library, the Stellar SDK and lens. A test enforces this. The focus command lives in a separate package the library never imports.

Assertions

func TestBuildPayment(t *testing.T) {
	got := BuildPayment(dest, "100")   // whatever your code returns

	focus.AssertEnvelopeEqual(t, wantXDR, got, focus.IgnoreVolatile())
	focus.AssertOperationTypes(t, got, "payment")
	focus.AssertFee(t, got, 100)
	focus.AssertMemo(t, got, "text: invoice-42")
}

Every assertion accepts XDR in whatever form your code produced it — a base64 string, raw []byte, any xdr.* value, or an already-decoded *lens.Value.

Results, in plain English

focus.AssertOperationFailed(t, result, 1, "payment_underfunded")
    submit_test.go:31: AssertOperationFailed: operation 1 failed with
        "payment_no_trust", want "payment_underfunded"

          got:  payment_no_trust — The destination has no trustline for this asset.
                → The destination must establish a trustline before it can receive a non-native asset.
          want: payment_underfunded

Codes are the short forms Horizon returns, and every one of the protocol's 208 result codes carries lens's plain-English explanation and hint.

Soroban

focus.AssertEventEmitted(t, meta, focus.Event{
	ContractID: "CBGSB…",
	Topics:     []any{"transfer", nil},   // nil matches any topic
})
focus.AssertReturnValue(t, meta, xdr.ScVal{Type: xdr.ScValTypeScvU32, U32: &seven})
focus.AssertContractTrapped(t, result)

A non-match lists the events that were emitted, so you are not left guessing.

Ignoring what legitimately varies

Without this, the library is unusable on real suites: a rebuilt transaction differs in its sequence number, signatures and time bounds every run.

focus.IgnoreVolatile()      // sequence + signatures + time bounds
focus.IgnoreSequence()
focus.IgnoreSignatures()
focus.IgnorePaths("**.Signatures", "operations[*].sourceaccount")
focus.NormalizePath("**.SeqNum", func(v any) any { return v.(int64) / 100 * 100 })

Patterns match the dotted paths lens reports: * is one segment, ** is any depth, and indexes accept [*]. Within a segment, *, ? and [abc]/[!abc] classes work like globs; matching is case-insensitive and patterns are anchored to the tail of a path too, so operations[*].sourceaccount needs no prefix.

Two deliberate behaviours:

  • Ignored paths are always named in the failure output. A silently discarded difference is how an assertion library stops being trustworthy.
  • An ignore pattern that matches nothing is reported as a failure, even when the values are otherwise equal. A stale pattern is a test quietly weakening, and this is the only moment anyone would notice.

require for fatal assertions

import "github.com/stellar-optics/stellar-focus/pkg/focus/require"

require.EnvelopeEqual(t, want, got)   // stops the test; same message

Same split, same naming as testify's assert and require.

Assertions never panic

Malformed XDR, a nil value, a wrong type — all reported as ordinary test failures. A panic inside your test binary would destroy the output of every other test in the package, so a test feeds hostile input through every exported assertion.

Golden files

func TestEnvelopeGolden(t *testing.T) {
	g := golden.New(t, golden.WithType("TransactionEnvelope"))
	g.Assert("payment", BuildPayment(dest, "100"))
}
$ go test ./...
    payment_test.go:15: golden: testdata/payment.golden.json does not exist yet.
        Create it by running:

            go test ./... -focus.update

$ go test ./... -focus.update
ok      demo    0.012s

Fixtures are reviewable JSON, not opaque blobs. The base64 XDR is the source of truth for comparison; the decoded form sits beside it so a pull-request diff shows which field changed:

{
  "format": "focus/v1",
  "type": "TransactionEnvelope",
  "description": "a successful Soroban invocation",
  "xdr": "AAAAAgAAAACS05bpJAmO8a9VODCyfbOCmh2Yk…",
  "captured": {
    "network": "testnet",
    "ledger": 3966820,
    "txHash": "2b2e276a…",
    "at": "2026-08-05T12:00:00Z"
  },
  "decoded": {
    "type": "TransactionEnvelope",
    "value": { "V1": { "Tx": { "SourceAccount": "GCJNH…ZST6", "Fee": 58002, … } } }
  }
}

A golden test nobody can review is a golden test nobody updates carefully.

The flag is namespaced -focus.update so it never collides with your own -update.

Capture

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

# Record a real transaction from testnet as fixtures
focus capture tx 2b2e276a8f99a292… --out testdata/

# Later: check they still match the chain
focus verify testdata/
$ focus verify testdata/
testdata/envelope.golden.json            ok
testdata/result.golden.json              ok
testdata/handwritten.golden.json         SKIP    no capture provenance, so there is nothing to re-fetch

3 checked: 2 ok, 0 drifted, 1 skipped

verify exits 2 on drift and prints the structural diff, 1 when a fixture could not be checked at all, and 0 when everything checked still matches. A hand-written fixture is reported as skipped rather than quietly passing a check it was never eligible for.

Defaults to testnet; use --network mainnet or --rpc-url.

Configuration

Flags, FOCUS_* environment variables and a config file are three spellings of the same settings, resolved in that order:

focus verify testdata/ --network mainnet
FOCUS_NETWORK=mainnet focus verify testdata/
focus verify testdata/ --config focus.yaml   # or .focus.yaml in the working directory
# focus.yaml
network: mainnet
rpc-url: https://mainnet.sorobanrpc.com
timeout: 45s
color: never
out: testdata
prefix: nightly
force: false

An explicit --config must exist; a missing .focus.yaml is simply ignored. Invalid settings fail before any request is made.

Snapshots

Failure-message formatting is snapshotted so a layout change is a reviewable diff rather than a silent change in what users see:

focus.AssertFailureSnapshot(t, "envelope_amount_mismatch", func(tb focus.TB) {
	focus.AssertEnvelopeEqual(tb, want, got)
})

Create or update snapshots with FOCUS_UPDATE_SNAPSHOTS=1 go test ./.... They live under testdata/snapshots as plain text, .snap files.

Testing offline

go test ./... makes no network calls. The RPC client sits behind an interface with an in-memory fake, so the whole suite runs offline and deterministically — including the tests of focus capture and focus verify.

Examples

Full Lifecycle (TypeScript)

examples/lifecycle.ts demonstrates the complete Stellar transaction lifecycle on testnet using @stellar/stellar-sdk:

  1. FUND — Create a claimable balance with predicates
  2. CLAIM — Claim the balance by the designated recipient
  3. ATTEST — Record the claim on a Soroban contract
  4. SETTLE — Finalize via contract call
cd examples
npm install
export FUNDER_SECRET="S..."      # testnet account with XLM
export CLAIMANT_SECRET="S..."    # testnet recipient
# Optional for attest/settle:
export ATTESTOR_SECRET="S..."
export ATTESTATION_CONTRACT_ID="C..."
npm run dev

The script prints transaction hashes at each step. View them on Stellar Expert to verify.

CI runs npm run typecheck in examples/ to keep the example compiling.

See examples/README.md for full setup and a sample Soroban contract for the attest/settle steps.

Project status

Early but real. v0.1 does what this README says. The library API may shift before v1.0.

  • Assertions for envelopes, results, fields and Soroban events.
  • Golden files with -focus.update; capture and verify against a live network.
  • Failure-message snapshots, a unified flag/env/config file, documented exit codes, and an offline end-to-end suite for the command.
  • Soroban values render as the XDR wire shape — there is no contract-spec decoding yet. See docs/roadmap.md.

Contributing

Adding an assertion or a fixture source is designed to be a one-file change. See CONTRIBUTING.md and docs/architecture.md.

License

Apache-2.0. See LICENSE.

About

No description or website provided.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages