--- name: data description: Querying Stellar chain data via Stellar RPC (preferred) and Horizon (legacy). Covers RPC JSON-RPC methods, Horizon REST endpoints, streaming, pagination, historical queries, Hubble/Galexie for deep history, and the RPC/Horizon migration story. Use when reading balances, transactions, operations, ledgers, contract events, or building any indexer/analytics workflow. user-invocable: true argument-hint: "[data task]" --- # Stellar Data: RPC + Horizon API access for reading chain state. Stellar RPC is the preferred entry point for new projects; Horizon remains for legacy and historical-query workflows. For deeper history beyond RPC's 7-day window, use Hubble/Galexie. ## When to use this skill - Calling Stellar RPC methods (`getLatestLedger`, `getLedgerEntries`, `getEvents`, `simulateTransaction`, `sendTransaction`) - Querying Horizon endpoints (accounts, transactions, operations, effects, ledgers) - Streaming live events or operations - Pulling historical data beyond RPC's 7-day window (Hubble, Galexie) - Choosing between RPC and Horizon for a given workflow ## Related skills - Building transactions to send → `../dapp/SKILL.md` - Smart contract simulation and event emission → `../smart-contracts/SKILL.md` - Asset balance and trustline lookups → `../assets/SKILL.md` - Standards (SEP-7 deeplinks, SEP-10 auth) → `../standards/SKILL.md` --- ## Overview Stellar provides two API paradigms: | API | Status | Use Case | |-----|--------|----------| | **Stellar RPC** | Preferred | Smart contracts, real-time state, new projects | | **Horizon** | Legacy-focused | Historical data, legacy applications | **Recommendation**: Use Stellar RPC for all new projects. Use Horizon mainly for historical queries and legacy compatibility paths. ## Read the file that matches the task | Task | File | |------|------| | RPC methods and usage | [Stellar RPC](#stellar-rpc) (below) | | Horizon endpoints, common operations, streaming, pagination | [horizon.md](horizon.md) | | Migration strategy | [Migration: Horizon to RPC](#migration-horizon-to-rpc) (below) | | Data history/indexing options | [Historical Data Access](#historical-data-access) (below) | | Environment setup and endpoints | [Network Configuration](#network-configuration) (below) | ## Stellar RPC ### Endpoints > Note: SDF directly provides Futurenet public RPC. For Mainnet RPC, select a provider from the [RPC providers directory](https://developers.stellar.org/docs/data/apis/rpc/providers). | Network | RPC URL | |---------|---------| | Mainnet | Provider-specific endpoint (see [RPC providers directory](https://developers.stellar.org/docs/data/apis/rpc/providers)) | | Testnet | `https://soroban-testnet.stellar.org` | | Futurenet | `https://rpc-futurenet.stellar.org` | | Local | `http://localhost:8000/soroban/rpc` | ### Setup ```typescript import * as StellarSdk from "@stellar/stellar-sdk"; const rpc = new StellarSdk.rpc.Server("https://soroban-testnet.stellar.org"); ``` ### Key Methods #### Get Account ```typescript const account = await rpc.getAccount(publicKey); // Returns account with sequence number for transaction building ``` #### Get Health ```typescript const health = await rpc.getHealth(); // { status: "healthy" } ``` #### Get Latest Ledger ```typescript const ledger = await rpc.getLatestLedger(); // { id: "...", sequence: 123456, protocolVersion: 25 } ``` #### Get Ledger Entries ```typescript // Read contract storage const key = StellarSdk.xdr.LedgerKey.contractData( new StellarSdk.xdr.LedgerKeyContractData({ contract: new StellarSdk.Address(contractId).toScAddress(), key: StellarSdk.xdr.ScVal.scvSymbol("Counter"), durability: StellarSdk.xdr.ContractDataDurability.persistent(), }) ); const entries = await rpc.getLedgerEntries(key); if (entries.entries.length > 0) { const value = StellarSdk.scValToNative( entries.entries[0].val.contractData().val() ); } ``` #### Simulate Transaction ```typescript const simulation = await rpc.simulateTransaction(transaction); if (StellarSdk.rpc.Api.isSimulationError(simulation)) { console.error("Simulation failed:", simulation.error); } else if (StellarSdk.rpc.Api.isSimulationSuccess(simulation)) { console.log("Cost:", simulation.cost); console.log("Result:", simulation.result); } ``` #### Send Transaction ```typescript const response = await rpc.sendTransaction(signedTransaction); if (response.status === "PENDING") { // Poll for result let result = await rpc.getTransaction(response.hash); while (result.status === "NOT_FOUND") { await new Promise(r => setTimeout(r, 1000)); result = await rpc.getTransaction(response.hash); } if (result.status === "SUCCESS") { console.log("Success:", result.returnValue); } else { console.error("Failed:", result.status); } } ``` #### Get Transaction ```typescript const tx = await rpc.getTransaction(txHash); // status: "SUCCESS" | "FAILED" | "NOT_FOUND" // returnValue: ScVal (for contract calls) // ledger: number ``` #### Get Events ```typescript const events = await rpc.getEvents({ startLedger: 1000000, filters: [ { type: "contract", contractIds: [contractId], topics: [ ["*", StellarSdk.xdr.ScVal.scvSymbol("transfer").toXDR("base64")], ], }, ], }); for (const event of events.events) { console.log("Event:", event.topic, event.value); } ``` ### RPC Limitations - **7-day history for most methods**: `getTransaction`, `getEvents`, etc. only cover recent data - **`getLedgers` exception**: on a data-lake-backed provider, "Infinite Scroll" pages back past the retention window — as far as that provider's data lake reaches (potentially genesis). On a plain RPC instance it is bounded by `getHealth().oldestLedger`; requests older than that fail with `-32600`. Check before assuming depth. - **No streaming**: Poll for updates (no WebSocket) - **Contract-focused**: Limited classic Stellar data ## Migration: Horizon to RPC ### Account Loading ```typescript // Horizon (old) const account = await horizonServer.loadAccount(publicKey); // RPC (new) const account = await rpc.getAccount(publicKey); // Note: RPC returns less data, just what's needed for transactions ``` ### Transaction Submission ```typescript // Horizon (for classic transactions) const result = await horizonServer.submitTransaction(tx); // RPC (for smart contract transactions) const response = await rpc.sendTransaction(tx); const result = await pollForResult(response.hash); ``` ### Historical Data ```typescript // Horizon - full history const allTxs = await horizonServer .transactions() .forAccount(publicKey) .call(); // RPC - most methods limited to the retention window (~7 days) // Exception: getLedgers can page further back (Infinite Scroll), but only as far // as the chosen provider's retention or data-lake integration reaches. // Always check the floor of the instance you're talking to first: const { oldestLedger } = await rpc.getHealth(); // For guaranteed full history, use: // 1. Hubble (SDF's BigQuery dataset) // 2. Galexie (data pipeline) // 3. Your own indexer ``` ### Streaming Replacement ```typescript // Horizon - native streaming server.payments().stream({ onmessage: handlePayment }); // RPC - polling (no native streaming) async function pollForUpdates() { const lastLedger = await rpc.getLatestLedger(); // Check for new events/transactions // Repeat on interval } setInterval(pollForUpdates, 5000); ``` ## Historical Data Access For data older than the RPC retention window (~7 days — not available via most RPC methods; `getLedgers` reaches further only on data-lake-backed providers, see [Data Lake](#data-lake) below): ### Hubble (BigQuery) ```sql -- Query Stellar data in BigQuery SELECT * FROM `crypto-stellar.crypto_stellar.history_transactions` WHERE source_account = 'G...' ORDER BY created_at DESC LIMIT 100 ``` ### Galexie Self-hosted data pipeline for processing Stellar ledger data: - https://github.com/stellar/galexie ### Data Lake RPC "Infinite Scroll" is powered by the Stellar data lake — a cloud-based object store (SEP-0054 format). Deep `getLedgers` history is a property of the **provider**, not the method: an instance only serves history past its retention window if its operator wired a data lake in. Instances without one (the public SDF testnet RPC included) reject older start ledgers with JSON-RPC `-32600` — compare your target against `getHealth().oldestLedger` before paging back. - **Public access**: `s3://aws-public-blockchain/v1.1/stellar/ledgers/pubnet` (AWS Open Data) - **Self-host**: Use Galexie to export to AWS S3 or Google Cloud Storage - **Hosted**: [Quasar (Lightsail Network)](https://quasar.lightsail.network) provides hosted Galexie Data Lake + Archive RPC endpoints - **Size**: ~3.8TB, growing ~0.5TB/year - **Cost**: ~$160/month self-hosted ($60 compute + $100 storage) - **Docs**: https://developers.stellar.org/docs/data/apis/rpc/admin-guide/data-lake-integration ### Third-Party Indexers For complex queries, event streaming, or custom data pipelines beyond what RPC/Horizon provide: - **Mercury** — Stellar-native indexer with Retroshades, GraphQL API (https://mercurydata.app) - **SubQuery** — Multi-chain indexer with Stellar support, event handlers (https://subquery.network) - **Goldsky** — Real-time data replication pipelines and subgraphs (https://goldsky.com) - **StellarExpert API** — Free, no-auth REST API for assets, accounts, ledger resolution (https://stellar.expert/openapi.html) See the full indexer directory: https://developers.stellar.org/docs/data/indexers ## Network Configuration > For a React/Next.js-specific setup, see the [dapp skill](../dapp/SKILL.md). > For mainnet RPC, set `STELLAR_MAINNET_RPC_URL` from a provider in the [RPC providers directory](https://developers.stellar.org/docs/data/apis/rpc/providers). ### Environment-Based Setup ```typescript // lib/stellar-config.ts import * as StellarSdk from "@stellar/stellar-sdk"; type NetworkConfig = { rpcUrl: string; horizonUrl: string; networkPassphrase: string; friendbotUrl: string | null; }; const requireEnv = (name: string): string => { const value = process.env[name]; if (!value) throw new Error(`Missing required env var: ${name}`); return value; }; // Lazy per-network factories: requireEnv only runs for the selected network, // so testnet/local work without the mainnet env var set. const configs: Record NetworkConfig> = { mainnet: () => ({ rpcUrl: requireEnv("STELLAR_MAINNET_RPC_URL"), horizonUrl: "https://horizon.stellar.org", networkPassphrase: StellarSdk.Networks.PUBLIC, friendbotUrl: null, }), testnet: () => ({ rpcUrl: "https://soroban-testnet.stellar.org", horizonUrl: "https://horizon-testnet.stellar.org", networkPassphrase: StellarSdk.Networks.TESTNET, friendbotUrl: "https://friendbot.stellar.org", }), local: () => ({ rpcUrl: "http://localhost:8000/soroban/rpc", horizonUrl: "http://localhost:8000", networkPassphrase: "Standalone Network ; February 2017", friendbotUrl: "http://localhost:8000/friendbot", }), }; const network = process.env.STELLAR_NETWORK || "testnet"; const makeConfig = configs[network]; if (!makeConfig) throw new Error(`Unknown network: ${network}`); export const config = makeConfig(); export const rpc = new StellarSdk.rpc.Server(config.rpcUrl); export const horizon = new StellarSdk.Horizon.Server(config.horizonUrl); ``` ## Best Practices ### Use RPC for: - New application development - Smart contract interactions - Transaction simulation and submission - Real-time account state ### Use Horizon for: - Historical transaction queries - Payment streaming - Legacy application maintenance - Rich account metadata ### Error Handling ```typescript // RPC errors try { const result = await rpc.sendTransaction(tx); } catch (error) { if (error.code === 400) { // Invalid transaction } else if (error.code === 503) { // Service unavailable } } // Horizon errors try { const result = await horizon.submitTransaction(tx); } catch (error) { const extras = error.response?.data?.extras; if (extras?.result_codes) { // Detailed error codes console.log("Transaction:", extras.result_codes.transaction); console.log("Operations:", extras.result_codes.operations); } } ``` ### Rate Limiting Both RPC and Horizon have rate limits: - Use exponential backoff for retries - Cache responses where appropriate - Consider running your own nodes for high-volume applications ```typescript async function withRetry(fn: () => Promise, maxRetries = 3): Promise { let lastError: Error; for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { lastError = error; if (error.response?.status === 429) { // Rate limited - exponential backoff await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000)); } else { throw error; } } } throw lastError; } ```