DEV Community

Padparadscho
Padparadscho

Posted on

Building a Terminal User Interface (TUI) for Stellar RPC with Rust and Ratatui

Introduction

When building Soroban smart contracts, developers often need to interact with Stellar RPC to query ledger data, fetch contract events, simulate transactions, or submit them to the network. Stellar Lab provides a powerful web based API explorer for this, but as a developer who likes to make my workflow fit my needs, I just need a fast, keyboard driven interface to test methods, explore parameters, and inspect responses right from the terminal.

That's exactly what Stellar TUI provides: a clean terminal user interface for exploring and executing all Stellar RPC methods. Select a method, fill in parameters, hit send, and inspect the JSON response. All without leaving the terminal!

Stellar TUI in action


Why Rust? Why Ratatui? Why a TUI?

I chose Rust because it lets me keep improving my skills while building something useful. The strong type system and ownership model help catch errors at compile time, and libraries like tokio and reqwest make async HTTP calls easy. The result is a single binary with no runtime dependencies.

Ratatui had been on my radar for a while, so this project was a good excuse to try it out. It's a solid TUI library with built in components like tables, forms, and modals.

A terminal interface is fast and efficient. Everything happens in the terminal without needing a browser or extra tooling.


Stellar RPC Integration

This is the core of Stellar TUI and explains how the project connects to Stellar's RPC endpoints.

The JSON-RPC 2.0 Client

The HTTP client is built on reqwest and handles sending requests:

// src/rpc/mod.rs
pub struct RpcClient {
    endpoint: String,
    client: reqwest::Client,
}

impl RpcClient {
    pub async fn call<Req, Resp>(
        &self,
        method: &str,
        params: Req,
    ) -> anyhow::Result<RpcResponse<Resp>>
    where
        Req: Serialize,
        Resp: DeserializeOwned,
    {
        let request = RpcRequest::new(method, params);
        let response = self.client
            .post(&self.endpoint)
            .json(&request)
            .send()
            .await?
            .error_for_status()?;

        let body = response.json::<RpcResponse<Resp>>().await?;
        Ok(body)
    }
}
Enter fullscreen mode Exit fullscreen mode

This client sends JSON-RPC 2.0 requests over HTTP POST. The generic call method accepts any serializable parameters and deserializes the response into any type needed.

Request and Response Types

The JSON-RPC 2.0 envelope looks like this:

// src/rpc/types.rs
#[derive(Debug, Serialize)]
pub struct RpcRequest<P> {
    pub jsonrpc: &'static str,   // Always "2.0"
    pub id: u64,                 // Request ID
    pub method: String,          // e.g., "getEvents"
    pub params: P,               // Method-specific parameters
}

#[derive(Debug, Deserialize)]
pub struct RpcResponse<T> {
    pub jsonrpc: String,
    pub id: u64,
    pub result: Option<T>,       // Success response
    pub error: Option<RpcError>, // Error response
}
Enter fullscreen mode Exit fullscreen mode

Method Execution Flow

Pressing r executes a request. Here is what happens:

// src/app/features/runtime.rs
pub fn execute_request(&mut self) {
    let method = &self.methods[self.selected_method];
    let form = &self.request_forms[self.selected_method];

    // Build JSON-RPC params from form input
    let params = match method.build_params(form) {
        Ok(value) => value,
        Err(message) => {
            self.set_timed_status(format!("Params error: {}", message), 5);
            return;
        }
    };

    let endpoint = self.settings.active_network().endpoint.clone();

    // Execute async HTTP call in background
    tokio::spawn(async move {
        let client = RpcClient::new(endpoint);
        let result = client
            .call::<Value, Value>(&method_name, params)
            .await
            .map_err(|e| e.to_string());
        // Response handled via channel
    });
}
Enter fullscreen mode Exit fullscreen mode

The request runs in the background using tokio::spawn, so the UI keeps working while waiting for the RPC response.

Network Management

Stellar TUI comes with Stellar Testnet set up by default. Pressing n switches between networks, though this shortcut only becomes useful once custom endpoints have been added through the Settings modal (s). Each network's RPC URL is stored in a config.json, so switching between Testnet, Mainnet, Futurenet, or any other custom Stellar RPC endpoint is easy.

A list of configured networks in the settings modal

Supported RPC Methods

Stellar TUI supports all 12 Stellar RPC methods available in the API.

For simple methods like getHealth, getFeeStats, getLatestLedger, getNetwork, and getVersionInfo, no parameters are required. The getEvents method offers filtering by ledger range, contract IDs, event topics, and event types with pagination support. Methods like getLedgerEntries, getLedgers, getTransaction, and getTransactions provide access to on chain data with cursor based pagination. For transaction operations, both sendTransaction and simulateTransaction handle raw XDR envelopes, with simulation supporting options for instruction leeway and auth modes.

Example: Building getEvents Parameters

The getEvents method demonstrates how complex RPC parameters are handled, filtering by ledger range, contract IDs, event topics, and more:

// src/app/methods/method/get_events.rs
pub fn build(form: &FormState) -> Result<Value, String> {
    let start_ledger = parse_optional_u64(form, "startLedger")?;
    let end_ledger = parse_optional_u64(form, "endLedger")?;
    let cursor = parse_optional_string(form, "cursor");
    let limit = parse_optional_u64(form, "limit")?;
    let event_type = parse_optional_string(form, "type");
    let contract_ids = parse_list(form, "contractIds");
    let topics = parse_optional_json(form, "topics")?;

    // Validation: contractIds/topics require explicit event type
    if (!contract_ids.is_empty() || topics.is_some()) && event_type.is_none() {
        return Err("Event type is required when using contractIds or topics".to_string());
    }

    // Build pagination object
    let mut pagination = serde_json::Map::new();
    if let Some(value) = limit {
        pagination.insert("limit".to_string(), json!(value));
    }
    if let Some(value) = cursor {
        pagination.insert("cursor".to_string(), json!(value));
    }

    // Build filters for contract events
    let mut filters = serde_json::Map::new();
    if let Some(value) = event_type {
        filters.insert("type".to_string(), json!(value));
    }
    if !contract_ids.is_empty() {
        filters.insert("contractIds".to_string(), json!(contract_ids));
    }

    // ... more parameter building
}
Enter fullscreen mode Exit fullscreen mode

This shows how the TUI translates user input from forms into properly structured JSON-RPC params.

getEvents response in fullscreen mode


Installation

Quick Start

  • Install using Cargo:
# Install
cargo install stellar-tui

# Run
stellar-tui
Enter fullscreen mode Exit fullscreen mode
  • Install from source:
# Clone and install from the repository
git clone https://github.com/padparadscho/stellar-tui.git
cd stellar-tui

cargo install --path .

# Run
stellar-tui
Enter fullscreen mode Exit fullscreen mode

Workflow

  1. Select a method: Use arrow keys or mouse wheel to browse the method list.
  2. Fill parameters: Tab to the request pane, type values.
  3. Execute: Press r/Ctrl+R to send the request.
  4. Inspect response: View formatted JSON in the response pane.

Essential Keybindings

Key Action
Tab Cycle focus between panes
r / Ctrl+R Execute request
n / Ctrl+N Switch network
f / Ctrl+F Toggle fullscreen
i / Ctrl+I Method documentation
s / Ctrl+S Settings
q / Ctrl+Q Quit

Features

  • Three pane layout: Methods list, request form, response viewer.
  • Structured request forms with type badges, inline validation, and contextual hints.
  • Network management for switching between RPC endpoints.
  • Method documentation with links to official Stellar docs.
  • Fullscreen toggle for request and response panes.
  • Response features: Search (regex), pagination, clipboard copy.
  • Responsive layout that adapts to narrow terminals (< 110 columns).
  • Mouse support for pane focus, navigation, and scrolling.

Method information modal with link to the official Stellar documentation for the selected method


Future Development Plans

To get more people using the Stellar TUI and build some interest from potential contributors, here are the features I'm planning to work on next:

  • Request and response history with timestamps.
  • Bookmarks for frequently used data like contract addresses.
  • Network validation for testing connection before adding new endpoints.
  • Export and import configuration for sharing settings across machines.
  • A simple theming system to set custom palettes.
  • Expanded test coverage beyond smoke tests.
  • Accessibility improvements.
  • Documentation improvements.

Conclusion

Building the Stellar TUI has been a fun side project that helped me learn more about Rust. My goal was to build something for the Stellar developer community and contribute my part to the ecosystem. It has been rewarding to watch the TUI grow from a simple idea into a tool that actually works. There's still plenty of work to do!

If this sounds interesting, give Stellar TUI a try:

cargo install stellar-tui
Enter fullscreen mode Exit fullscreen mode

Questions, suggestions, or contributions? Open an issue on GitHub. Stars are always welcome!

Top comments (0)