# Stellar Developer Documentation > Full text of the Stellar developer documentation, concatenated as Markdown for LLM ingestion. This file contains all documentation content in a single document following the llmstxt.org standard. ## Stellar Developer Docs ## Navigating the docs ### [Build](./build/README.mdx) Contains tutorials and how-to guides for writing smart contracts, building applications, interacting with the network, and more. ### [Learn](./learn/fundamentals/README.mdx) Find all informational and conceptual content here. Learn about Stellar fundamentals like how accounts and transactions function, dive deeper into the functionality of each operation, discover how fees work, and more. ### [Tokens](./tokens/README.mdx) Information on how to issue assets on the Stellar network and create contract tokens. ### [Data](./data/README.mdx) Discover various data availability options: RPC, Hubble, Galexie, the Ingest SDK, and Horizon (nearing end-of-life). ### [Tools](./tools/README.mdx) Learn about all the available tools at your disposal for building on, interacting with, or just watching the Stellar network. Also, find information on how to use the Anchor Platform or Stellar Disbursement Platform. ### [Networks](./networks/README.mdx) Information about deployed networks (Mainnet, Testnet, and Futurenet), current software versions, and resource limitations and fees. ### [Validators](./validators/README.mdx) Everything you'll need to know if you want to run, operate, and maintain a core validator node on the Stellar network. --- ## Get Started with Blockchain Development; Guides, Tutorials & Tools # Introduction The Build section is split into three parts: 1. [Smart contracts](./smart-contracts/README.mdx) 2. [Applications](./apps/README.mdx) 3. [How-to guides](./guides/README.mdx) Explanations for each section are stated below: ## Smart contracts Smart contracts are self-executing programs with the terms of an agreement written directly into the code. They automatically enforce and execute the terms of the contract when predefined conditions are met. Once written and tested, smart contracts are deployed to the blockchain, where they become immutable and publicly accessible. :::tip While defining the rules and logic of the contract, developers must be security conscious to avoid vulnerabilities such as integer overflows, access control flaws, and other exploits that could compromise the integrity and functionality of the smart contract. ::: This section will walk you through how to write and deploy smart contracts on Stellar, including the installation process, an introduction to testing, storing data, and more. It also provides a variety of example contracts for reference and use. ## Applications Applications interact with the blockchain and can use smart contracts as the backend. They: - Provide user interfaces (UI); - Manage user interactions; - Integrate with smart contracts to operate. Writing **smart contracts** focuses on the backend logic and rules enforced on the blockchain, while **building applications** involves creating the frontend and integrating it with these smart contracts to provide a complete user experience. :::note You can create applications on Stellar without using smart contracts, as demonstrated in the [Wallet SDK tutorial](./apps/wallet/overview.mdx) or the [JS SDK Payment Application tutorial](./apps/example-application-tutorial/overview.mdx). ::: This section walks you through design considerations for applications and tutorials for building applications with or without smart contracts. ## How-to guides This section provides step-by-step instructions to help users complete specific tasks associated with building on Stellar. These tasks can include instructions for aspects of writing contracts, interacting with contracts, building applications, using Stellar operations, setting up infrastructure, and more. :::note How-to guides assume that the user has some experience and knowledge with building on Stellar and are not typically for beginners. ::: --- ## Agentic Payments: HTTP-Native Payment Protocols for AI Agents and APIs # Agentic Payments Agentic payments enable programmatic, per-request payments over HTTP — designed for AI agents, APIs, and machine-to-machine interactions. These protocols extend the `402 Payment Required` HTTP status code into a machine-readable payment negotiation layer, allowing clients to pay for API requests natively on Stellar. - **[x402 on Stellar](./x402/README.mdx)** — An open protocol from the Coinbase Developer Platform for per-request payments using Soroban authorization entries, with facilitator-based verification and settlement. - **[MPP on Stellar](./mpp/README.mdx)** — The Machine Payments Protocol for direct on-chain settlement via Soroban SAC transfers, with support for both one-time charge payments and high-frequency off-chain payment channels. --- ## MPP on Stellar ## What is MPP? The [Machine Payments Protocol (MPP)](https://mpp.dev) is an open protocol that enables programmatic, per-request payments over HTTP, designed especially for AI agents and APIs. It extends the `402 Payment Required` HTTP status code into a machine-readable payment negotiation layer for both humans and autonomous agents. On Stellar, MPP works with Soroban SAC (Stellar Asset Contract) token transfers so that clients can pay for API requests natively, without an external facilitator. This makes it ideal for micropayments, AI agent-driven APIs, and payment-enabled applications. ## Supported MPP Intents The `@stellar/mpp` SDK supports two payment intents: ### [Charge](https://mpp.dev/intents/charge) Immediate one-time payments. Each request triggers a Soroban SAC `transfer` settled on-chain individually. This is the simplest intent — no channel setup or pre-funding required. Two credential modes are available: - **Pull** (default) — The client prepares and signs the Soroban authorization entries; the server broadcasts the transaction. Supports an optional sponsored path where the server rebuilds the transaction with its own account as source, so the client never pays network fees. - **Push** — The client broadcasts the transaction itself and sends a `signedHash` credential for server verification — the transaction hash plus a signature proving the client controls the paying (`from`) account. See the [MPP Charge Guide](./charge-guide.mdx) to get started. ### Session The session intent enables high-frequency, pay-as-you-go payments over unidirectional [payment channels](https://github.com/stellar-experimental/one-way-channel). The funder deposits tokens into the channel once, then makes many off-chain payments by signing cumulative commitments — no per-payment on-chain transactions, ideal for AI agent interactions. The server settles by closing the channel when convenient. See the [MPP Session Guide](./channel-guide.mdx) to get started. ## Demo Try the live demo at [mpp.stellar.buzz](https://mpp.stellar.buzz), or run the [MPP Demo](https://github.com/stellar/stellar-mpp-sdk/tree/main/demo) locally. It runs a Node.js server that charges 0.01 USDC per request and a minimal browser UI for testing end-to-end payment flows on Stellar Testnet. To build an MPP-enabled service or integrate payments into your app, see [Build Applications](../../apps/README.mdx) and the resources below. ## Install ```bash npm install @stellar/mpp mppx @stellar/stellar-sdk ``` ## Examples - **@stellar/mpp (GitHub)** — SDK source code, examples, and integration tests. [View on GitHub](https://github.com/stellar/stellar-mpp-sdk) - **Server example** — A minimal Node.js HTTP server charging 0.01 USDC per request using `@stellar/mpp/charge/server`. [View on GitHub](https://github.com/stellar/stellar-mpp-sdk/blob/main/examples/charge-server.ts) - **Client example** — A Node.js client that automatically handles 402 responses using `@stellar/mpp/charge/client`. [View on GitHub](https://github.com/stellar/stellar-mpp-sdk/blob/main/examples/charge-client.ts) ## Learn more - [MPP Specification](https://mpp.dev) — Official MPP protocol specification - [@stellar/mpp (npm)](https://www.npmjs.com/package/@stellar/mpp) — npm package for MPP on Stellar - [mppx (npm)](https://www.npmjs.com/package/mppx) — Core MPP framework library - [one-way-channel contract](https://github.com/stellar-experimental/one-way-channel) — Soroban contract powering channel mode - [Signing Soroban invocations](../../guides/transactions/signing-soroban-invocations.mdx) — Auth-entry signing and transaction signing on Stellar --- ## MPP Session Guide This guide explains how to use the MPP **session** intent with `@stellar/mpp`. The session intent is implemented using a [one-way payment channel](https://github.com/stellar-experimental/one-way-channel) Soroban contract — the funder deposits tokens once, then makes many off-chain payments by signing cumulative commitments. No on-chain transaction is needed per payment, making this ideal for high-frequency AI agent interactions. This worked example assumes the channel is funded in USDC. Commitment amounts and prices below are therefore denominated in USDC, because channel commitments always use the asset deposited into the channel. ## How channel payments work ``` Client (Funder) Server (Recipient) Soroban RPC / Network | | | | --- CHANNEL SETUP (pre-funded on-chain) --- | | | | | Build + sign deploy tx | | | (commitment key + USDC | | | deposit) | | |----------------------------->| | | | | | | Broadcast deploy tx | | |------------------------->| | | | | | Channel deployed (C...) | | |<-------------------------| | | | | 200 OK + receipt | | |<-----------------------------| | | | | | --- VOUCHER PAYMENTS (off-chain, repeatable) --- | | | | | GET /resource | | |----------------------------->| | | | | | 402 Payment Required | | | { channel: C..., amount, | | | cumulativeAmount, network, | | | reference } | | |<-----------------------------| | | | | | Simulate prepare_commitment | | | (read-only, no tx cost) | | |-----------------------------------------------------> | | | | | Commitment bytes | | |<----------------------------------------------------- | | | | | Sign commitment bytes | | | locally with ed25519 key | | | | | | Credential: action=voucher, | | | amount=, | | | signature= | | |----------------------------->| | | | | | | Simulate | | | prepare_commitment | | | (read-only, no tx cost) | | |------------------------->| | | | | | Commitment bytes | | |<-------------------------| | | | | | Verify ed25519 sig | | | locally (Keypair.verify) | | | | | | Update cumulative in | | | store | | | | | 200 OK + receipt | | |<-----------------------------| | | | | | ... (repeat voucher flow; | | | no on-chain tx needed) | | | | | | --- CLOSE (server-initiated settlement) --- | | | | | | close() with highest | | | commitment amount + | | | signature | | |------------------------->| | | | | | Settlement confirmed | | | (USDC transferred to | | | recipient, remainder | | | returned to funder) | | |<-------------------------| ``` Each commitment is cumulative. The server tracks the highest commitment it has seen; closing the channel batch-settles all payments in a single on-chain transaction. ## Prerequisites Before using channel mode, you need a deployed [one-way-channel contract](https://github.com/stellar-experimental/one-way-channel) on Stellar Testnet or Mainnet. The contract is initialized with: - A **commitment key** — an ed25519 keypair. The client signs commitments with the private key; the contract verifies with the public key. - A **token deposit** — the funder's initial balance in the channel asset. This example uses USDC. See the [one-way-channel repo](https://github.com/stellar-experimental/one-way-channel) for deployment instructions. ## Session server Create `channel-server.js`: ```js title="channel-server.js" const PORT = 3001; const CHANNEL_CONTRACT = process.env.CHANNEL_CONTRACT; // C... (56 chars) const COMMITMENT_PUBKEY = process.env.COMMITMENT_PUBKEY; // 64-char hex ed25519 public key const MPP_SECRET_KEY = process.env.MPP_SECRET_KEY; // Shared secret for MPP credential verification if (!CHANNEL_CONTRACT || !COMMITMENT_PUBKEY) { console.error( "Set CHANNEL_CONTRACT and COMMITMENT_PUBKEY environment variables", ); process.exit(1); } if (!MPP_SECRET_KEY) { console.error( "Set MPP_SECRET_KEY to a strong secret for MPP credential verification", ); process.exit(1); } // Convert raw ed25519 public key (hex) to a Stellar G... address const commitmentPublicKeyG = StrKey.encodeEd25519PublicKey( Buffer.from(COMMITMENT_PUBKEY, "hex"), ); const mppx = Mppx.create({ secretKey: MPP_SECRET_KEY, methods: [ stellar.channel({ channel: CHANNEL_CONTRACT, commitmentKey: commitmentPublicKeyG, store: Store.memory(), // tracks cumulative amounts + replay protection network: "stellar:testnet", }), ], }); const app = express(); app.get("/my-service", async (req, res) => { const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (value == null) continue; if (Array.isArray(value)) { for (const entry of value) { headers.append(key, entry); } } else { headers.set(key, value); } } const webReq = new Request(`http://localhost:${PORT}${req.url}`, { method: req.method, headers, }); const result = await mppx.channel({ amount: "0.1", // 0.1 USDC per request (human-readable) description: "API call", })(webReq); if (result.status === 402) { const challenge = result.challenge; challenge.headers.forEach((value, key) => res.setHeader(key, value)); return res.status(402).send(await challenge.text()); } const response = result.withReceipt( Response.json({ secret: "valuable content" }), ); response.headers.forEach((value, key) => res.setHeader(key, value)); return res.status(response.status).send(await response.text()); }); app.listen(PORT, () => { console.log( `MPP channel server listening on http://localhost:${PORT}/my-service`, ); }); ``` Start the server: ```bash CHANNEL_CONTRACT=CABC... COMMITMENT_PUBKEY=<64-hex-chars> MPP_SECRET_KEY=replace-me node channel-server.js ``` ## Session client Create `channel-client.js`: ```js title="channel-client.js" // Load commitment secret key from .env const env = Object.fromEntries( readFileSync(".env", "utf-8") .split("\n") .filter((l) => l.includes("=")) .map((l) => l.split("=")), ); const COMMITMENT_SECRET = env.COMMITMENT_SECRET?.trim(); // 64-char hex ed25519 secret if (!COMMITMENT_SECRET) { console.error("Add COMMITMENT_SECRET=<64-hex-chars> to .env"); process.exit(1); } // Convert raw hex ed25519 seed to a Stellar Keypair const commitmentKey = Keypair.fromRawEd25519Seed( Buffer.from(COMMITMENT_SECRET, "hex"), ); console.log(`Commitment public key: ${commitmentKey.publicKey()}`); // Polyfill global fetch — 402 responses are handled automatically Mppx.create({ methods: [ stellar.channel({ commitmentKey, onProgress(event) { switch (event.type) { case "challenge": console.log( `Challenge: ${event.amount} base units via channel ${event.channel.slice(0, 12)}...`, ); break; case "signed": console.log( `Commitment signed (cumulative: ${event.cumulativeAmount} base units)`, ); break; } }, }), ], }); // Requests are automatically paid with off-chain commitment signatures const res1 = await fetch("http://localhost:3001/my-service"); console.log(`Request 1 (${res1.status}):`, await res1.json()); // Second request increments cumulative amount; still off-chain const res2 = await fetch("http://localhost:3001/my-service"); console.log(`Request 2 (${res2.status}):`, await res2.json()); ``` Add the commitment secret key to `.env`: ```bash title=".env" COMMITMENT_SECRET=<64-hex-chars> ``` Run the client: ```bash node channel-client.js ``` Each request signs an increasing cumulative commitment off-chain. The server verifies the ed25519 signature against the on-chain `commitment_key` in the contract (via Soroban simulation, no transaction needed) and returns the protected content immediately. ## Closing the channel When the server wants to settle, it closes the channel using the highest commitment amount and signature it has tracked. The server must persist the highest cumulative amount and its corresponding ed25519 signature so it can supply them at close time. ```js const SIGNER_SECRET = process.env.SIGNER_SECRET; if (!SIGNER_SECRET) { throw new Error("Set SIGNER_SECRET to a Stellar secret key (S...)"); } // Close the channel using the highest commitment seen by the server const txHash = await close({ channel: CHANNEL_CONTRACT, amount: 2000000n, // cumulative committed amount in base units (bigint) signature: lastCommitmentSig, // Uint8Array — the ed25519 signature for this commitment feePayer: { envelopeSigner: Keypair.fromSecret(SIGNER_SECRET), // signs the close transaction }, network: "stellar:testnet", }); console.log("Channel closed, tx:", txHash); ``` Closing submits a single on-chain transaction that transfers the cumulative committed amount from the channel to the recipient. The remainder is returned to the funder. ## Subpath exports Channel mode uses separate subpath exports to avoid bundling unused code: | Path | Purpose | | --- | --- | | `@stellar/mpp/channel/server` | `stellar`, `channel`, `close`, `getChannelState`, `watchChannel` | | `@stellar/mpp/channel/client` | `stellar`, `channel` | | `@stellar/mpp/channel` | Channel method schema (Zod) | ## Additional documentation - [one-way-channel contract](https://github.com/stellar-experimental/one-way-channel) — Soroban contract, deployment scripts, and parameters - [@stellar/mpp on GitHub](https://github.com/stellar/stellar-mpp-sdk) — Full API reference and integration tests - [MPP Charge Guide](./charge-guide.mdx) — Charge mode (per-request on-chain settlement) - [MPP Specification](https://mpp.dev) — Protocol specification --- ## MPP Charge Guide The [**charge** intent](https://mpp.dev/intents/charge) is for immediate, one-time payments. Each API request triggers a Soroban SAC `transfer` that settles on-chain individually — no channel setup, no pre-funding, and no external facilitator required. This is the simplest way to get started with MPP on Stellar. ## How charge payments work ``` Client (Payer) Server (Recipient) Soroban RPC / Network | | | | GET /resource | | |----------------------------->| | | | | | 402 Payment Required | | | (currency, amount, recipient,| | | network) | | |<-----------------------------| | | | | | Build Soroban SAC transfer | | | Simulate (prepareTransaction)| | |-----------------------------------------------------> | | | Simulation | |<----------------------------------------------------- | | | | | Sign transaction envelope | | | Send signed XDR credential | | |----------------------------->| | | | | | | Verify SAC invocation | | | Simulate + validate | | | transfer events | | |------------------------->| | |<-------------------------| | | | | | Broadcast transaction | | |------------------------->| | | | | | Poll until confirmed | | |<-------------------------| | | | | 200 OK + receipt | | |<-----------------------------| | ``` In **pull** mode (default), the client builds and signs the full transaction envelope; the server validates the SAC transfer via simulation, then broadcasts. With **sponsored fees**, the client signs only the Soroban auth entries and the server rebuilds the transaction with its own account as source. In **push** mode, the client broadcasts the transaction itself and sends a `signedHash` credential — the transaction hash plus a signature proving control of the `from` account — for server verification. This tutorial walks through building a payment-gated API with Node.js and Express using `@stellar/mpp`. To follow this guide, you will need [Node.js](https://nodejs.org) installed locally. Recommend using the latest LTS version. ## Create a project Create a new folder for the tutorial and initialize a Node.js project: ```bash mkdir mpp-quickstart cd mpp-quickstart npm init -y npm pkg set type=module ``` The `type=module` setting lets you use ES module `import` syntax in the examples below. Install the npm packages used by the server and client: ```bash npm install express @stellar/mpp mppx @stellar/stellar-sdk ``` ## Create `server.js` Create a file named `server.js` and paste in the following code: ```js title="server.js" const PORT = 3001; const RECIPIENT = process.env.STELLAR_RECIPIENT; // Your Stellar public key (G...) const MPP_SECRET_KEY = process.env.MPP_SECRET_KEY; // Shared secret for MPP credential verification if (!RECIPIENT) { console.error("Set STELLAR_RECIPIENT to a Stellar public key (G...)"); process.exit(1); } if (!MPP_SECRET_KEY) { console.error( "Set MPP_SECRET_KEY to a strong secret for MPP credential verification", ); process.exit(1); } // Create the MPP server instance const mppx = Mppx.create({ secretKey: MPP_SECRET_KEY, methods: [ stellar.charge({ recipient: RECIPIENT, currency: USDC_SAC_TESTNET, network: "stellar:testnet", store: Store.memory(), // Required by v0.7 for replay protection }), ], }); const app = express(); // Payment-gated endpoint app.get("/my-service", async (req, res) => { // Convert Node.js IncomingMessage to Web Request const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (value == null) continue; if (Array.isArray(value)) { for (const entry of value) { headers.append(key, entry); } } else { headers.set(key, value); } } const webReq = new Request(`http://localhost:${PORT}${req.url}`, { method: req.method, headers, }); const result = await mppx.charge({ amount: "0.01", description: "Premium API access", })(webReq); if (result.status === 402) { const challenge = result.challenge; challenge.headers.forEach((value, key) => res.setHeader(key, value)); return res.status(402).send(await challenge.text()); } const response = result.withReceipt( Response.json({ secret: "valuable content" }), ); response.headers.forEach((value, key) => res.setHeader(key, value)); return res.status(response.status).send(await response.text()); }); app.listen(PORT, () => { console.log(`MPP server listening on http://localhost:${PORT}/my-service`); }); ``` Set `STELLAR_RECIPIENT` to the Stellar public key (`G...`) for the account that should receive USDC payments. Your account will need a testnet USDC trustline — see [Setting up a testnet wallet](#setting-up-a-testnet-wallet) below. Start the API locally: ```bash STELLAR_RECIPIENT=GYOUR_PUBLIC_KEY MPP_SECRET_KEY=replace-me node server.js ``` When a client requests your endpoint, the server responds with `402 Payment Required`, including headers that describe the payment requirements. A compliant client builds a signed Soroban SAC transfer and retries the request — no external facilitator needed. The server verifies and broadcasts the transaction directly. ## Create `client.js` ### Setting up a testnet wallet Create a fresh account and fund it with testnet XLM and testnet USDC using Stellar Lab: 1. Create a new keypair: https://lab.stellar.org/account/create 2. Fund with testnet XLM (Friendbot): https://lab.stellar.org/account/fund 3. Create the USDC trustline (there's a button on the fund page above) 4. Get testnet USDC from the Circle faucet — select **Stellar Testnet** and paste in your public key: https://faucet.circle.com Create a `.env` file and add your testnet secret key: ```bash title=".env" STELLAR_SECRET=S... ``` :::caution Secret keys provide full access to any digital assets held in the wallet. Use `.env` files only for hot wallets in testnet deployments. ::: ### Client code With your server running, create a file named `client.js` and paste in the following code: ```js title="client.js" // Load .env manually (no dotenv package needed) const env = Object.fromEntries( readFileSync(".env", "utf-8") .split("\n") .filter((l) => l.includes("=")) .map((l) => l.split("=")), ); const STELLAR_SECRET = env.STELLAR_SECRET?.trim(); if (!STELLAR_SECRET) { console.error("Add STELLAR_SECRET=S... to .env"); process.exit(1); } const keypair = Keypair.fromSecret(STELLAR_SECRET); console.log(`Using Stellar account: ${keypair.publicKey()}`); // Polyfill global fetch — 402 responses are handled automatically Mppx.create({ methods: [ stellar.charge({ keypair, mode: "pull", // server broadcasts the signed transaction onProgress(event) { console.log(`[${event.type}]`, event); }, }), ], }); // Make the request — payment is handled transparently on 402 const response = await fetch("http://localhost:3001/my-service"); const data = await response.json(); console.log(`Response (${response.status}):`, data); ``` ## Run the client Once the account is funded and the secret key is in `.env`, run the client in a second terminal: ```bash node client.js ``` The client: 1. Makes a `GET /my-service` request 2. Receives a `402 Payment Required` with payment details in the response headers 3. Builds and signs a Soroban SAC `transfer` on Stellar Testnet 4. Retries the request with the signed credential 5. Receives `200 OK` with the protected content: `{ secret: 'valuable content' }` The 0.01 USDC settles directly to the `STELLAR_RECIPIENT` wallet. No facilitator, no extra infrastructure. ## Push mode The examples above use **pull mode** (default), where the server broadcasts the signed transaction. In **push mode**, the client broadcasts the transaction directly and sends a signed hash credential (`signedHash`) to the server for verification. ### Push mode client To use push mode, set `mode: "push"` in the client configuration and provide the keypair of the `from` account (the account funding the transfer). The SDK handles `signedHash` credential generation automatically — no manual signing code required: ```js title="client.js (push mode)" // Load .env manually (no dotenv package needed) const env = Object.fromEntries( readFileSync(".env", "utf-8") .split("\n") .filter((l) => l.includes("=")) .map((l) => l.split("=")), ); const STELLAR_SECRET = env.STELLAR_SECRET?.trim(); if (!STELLAR_SECRET) { console.error("Add STELLAR_SECRET=S... to .env"); process.exit(1); } const keypair = Keypair.fromSecret(STELLAR_SECRET); console.log(`Using Stellar account: ${keypair.publicKey()}`); // Polyfill global fetch — 402 responses are handled automatically Mppx.create({ methods: [ stellar.charge({ keypair, mode: "push", // client broadcasts the transaction onProgress(event) { console.log(`[${event.type}]`, event); }, }), ], }); // Make the request — payment is handled transparently on 402 const response = await fetch("http://localhost:3001/my-service"); const data = await response.json(); console.log(`Response (${response.status}):`, data); ``` ### Push mode server The server accepts both pull and push credentials automatically — no extra configuration needed. The only change from the pull mode server is moving `store` into `stellar.charge()`: ```js title="server.js (push mode)" const PORT = 3001; const RECIPIENT = process.env.STELLAR_RECIPIENT; const MPP_SECRET_KEY = process.env.MPP_SECRET_KEY; if (!RECIPIENT) { console.error("Set STELLAR_RECIPIENT to a Stellar public key (G...)"); process.exit(1); } if (!MPP_SECRET_KEY) { console.error( "Set MPP_SECRET_KEY to a strong secret for MPP credential verification", ); process.exit(1); } const mppx = Mppx.create({ secretKey: MPP_SECRET_KEY, methods: [ stellar.charge({ recipient: RECIPIENT, currency: USDC_SAC_TESTNET, network: "stellar:testnet", store: Store.memory(), // Required by v0.7 for replay protection }), ], }); const app = express(); app.get("/my-service", async (req, res) => { const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (value == null) continue; if (Array.isArray(value)) { for (const entry of value) { headers.append(key, entry); } } else { headers.set(key, value); } } const webReq = new Request(`http://localhost:${PORT}${req.url}`, { method: req.method, headers, }); const result = await mppx.charge({ amount: "0.01", description: "Premium API access", })(webReq); if (result.status === 402) { const challenge = result.challenge; challenge.headers.forEach((value, key) => res.setHeader(key, value)); return res.status(402).send(await challenge.text()); } const response = result.withReceipt( Response.json({ secret: "valuable content" }), ); response.headers.forEach((value, key) => res.setHeader(key, value)); return res.status(response.status).send(await response.text()); }); app.listen(PORT, () => { console.log(`MPP server listening on http://localhost:${PORT}/my-service`); }); ``` ### Signed hash credentials The SDK generates `signedHash` credentials automatically when `mode: "push"` is set — you do not write any signing code. Under the hood, the client: 1. Computes a hash of the transaction envelope 2. Creates the string `"{challenge.id}:{hash}"` where `challenge.id` is the MPP challenge identifier 3. Signs this string using the keypair of the `from` account The server verifies the signature against the public key of the `from` account — the account funding the transfer — ensuring the client authorized the specific hash. With sponsored fees the transaction envelope is sourced by the fee payer, so the signature is always checked against the `from` account rather than whoever broadcasts. ## Sponsored fees (optional) By default, the client pays Stellar network fees. To have the server pay fees on behalf of the client, configure a `feePayer` on the server: ```js const RECIPIENT = process.env.STELLAR_RECIPIENT; const MPP_SECRET_KEY = process.env.MPP_SECRET_KEY; const FEE_PAYER_SECRET = process.env.FEE_PAYER_SECRET; if (!MPP_SECRET_KEY) { throw new Error( "Set MPP_SECRET_KEY to a strong secret for MPP credential verification", ); } if (!FEE_PAYER_SECRET) { throw new Error("Set FEE_PAYER_SECRET to a Stellar secret key (S...)"); } const mppx = Mppx.create({ secretKey: MPP_SECRET_KEY, methods: [ stellar.charge({ recipient: RECIPIENT, currency: USDC_SAC_TESTNET, network: "stellar:testnet", store: Store.memory(), // Required by v0.7 for replay protection feePayer: { envelopeSigner: Keypair.fromSecret(FEE_PAYER_SECRET), // pays tx fees }, }), ], }); ``` Set `FEE_PAYER_SECRET` before running the server if you use sponsored fees. When `feePayer` is configured, the server automatically signals fee sponsorship to the client via the challenge. The client then signs only the Soroban auth entries (not the full transaction envelope). The server rebuilds the transaction with the `envelopeSigner`'s account as source and broadcasts it. Optionally, add `feeBumpSigner` inside `feePayer` to wrap the transaction in a fee bump. :::info[Channel open removal] The channel `open` MPP action was removed in v0.7 as it was dead code — the Soroban contract has no on-chain open entrypoint. Channels are created on-chain directly by signing and broadcasting a deploy transaction. See the [MPP Session Guide](./channel-guide.mdx) for channel setup details. ::: ## Learn more - [@stellar/mpp on GitHub](https://github.com/stellar/stellar-mpp-sdk) — Full API reference, channel mode, and examples - [@stellar/mpp (npm)](https://www.npmjs.com/package/@stellar/mpp) — npm package for MPP on Stellar - [MPP Specification](https://mpp.dev) — Official MPP protocol specification and whitepaper - [mppx (npm)](https://www.npmjs.com/package/mppx) — Core MPP framework library used by both server and client - [Signing Soroban invocations](../../guides/transactions/signing-soroban-invocations.mdx) — Auth-entry signing on Stellar - [one-way-channel contract](https://github.com/stellar-experimental/one-way-channel) — Soroban contract powering the channel payment mode - [MPP Session Guide](./channel-guide.mdx) — Set up off-chain payment channels for high-frequency payments --- ## x402 on Stellar ## What is x402? x402 is an open protocol from the Coinbase Developer Platform that enables programmatic, per request payments over HTTP, designed especially for AI agents and APIs. It effectively turns the old “402 Payment Required” HTTP status code into something usable, for both humans and AI agents. On Stellar, x402 works with Soroban authorization so that clients can pay for API requests via signed auth entries, ideal for micropayments and payment enabled apps. To build an x402-enabled service or integrate payments into your app, see [Build Applications](../../apps/README.mdx) and the resources below. ## Demo Try the [x402 Demo](https://stellar.org/x402-demo). Send an x402 payment on Stellar Testnet or Mainnet and see the x402 payment flow in action. ([source code](https://github.com/stellar/x402-stellar)) [![Screenshot of the x402 payment flow demo on Stellar](/assets/guides/x402-demo.png)](https://stellar.org/x402-demo) ## x402 Compatible Wallets To support x402 on Stellar, a wallet must support [auth-entry signing](../../guides/transactions/signing-soroban-invocations.mdx#method-2-auth-entry-signing) (Soroban authorization entry signing). The following wallets support auth-entry signing: - Freighter Browser Extension - Albedo - Hana - HOT - Klever - OneKey :::note Freighter Mobile does not currently support x402; use the Freighter browser extension. Mobile support is planned for a future release. ::: ## Supported Assets x402 on Stellar supports any [SEP-41](https://stellar.org/protocol/sep-41) compliant token. The default is USDC. ### Testnet USDC | Property | Value | | --------------- | ---------------------------------------------------------- | | Asset Code | `USDC` | | Issuer | `GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5` | | SEP-41 Contract | `CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA` | ### Mainnet USDC | Property | Value | | --------------- | ---------------------------------------------------------- | | Asset Code | `USDC` | | Issuer | `GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN` | | SEP-41 Contract | `CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75` | ## x402 Facilitators You can use a facilitator to verify and settle x402 payments. Two options are available for Stellar: ### Coinbase x402 Facilitator Coinbase’s x402 facilitator supports Stellar on **Testnet** with sponsored fees. Check which networks and options are supported: - **[x402 Facilitator Supported Networks](https://www.x402.org/facilitator/supported)** — Lists supported networks (including `stellar:testnet`) and facilitator configuration. ### Build on Stellar Relayer (with OpenZeppelin x402 Plugin) The [Relayer Plugin for x402](https://github.com/OpenZeppelin/relayer-plugin-x402-facilitator) implements the x402 Facilitator API so you can serve x402 payments directly from a Relayer instance. The x402 Facilitator leverages the OpenZeppelin Relayer Framework. It works with the [Coinbase x402](https://github.com/coinbase/x402) ecosystem and exposes the expected `/verify`, `/settle`, and `/supported` endpoints under the Relayer plugin router. #### How to Use This x402 facilitator service is available on Testnet and Mainnet. Under the hood, the plugin leverages OpenZeppelin Channels to submit transactions onchain via a managed Relayer and Facilitator setup. #### Testnet To use the facilitator on testnet, you will need: - An API Key (Relayer Service). Generate your testnet API key here: https://channels.openzeppelin.com/testnet/gen - Facilitator URL. Use the following facilitator endpoint in your configuration: `https://channels.openzeppelin.com/x402/testnet` #### Mainnet To use the Facilitator on mainnet, you will need: - An API Key (Relayer Service). Generate your mainnet API key here: https://channels.openzeppelin.com/gen - Facilitator URL. Use the following facilitator endpoint in your configuration: `https://channels.openzeppelin.com/x402` :::note This version supports x402 v2 specification. For x402 v1 support, please use a previous version of this plugin (check git history for v1 compatible releases). ::: ## Examples - **x402 on Stellar (Stellar repo)** — Tools, examples, and references for the x402 protocol on Stellar. Use this as the canonical source for Stellar-specific x402 demos and tooling. [View on GitHub](https://github.com/stellar/x402-stellar) - **x402 Starter Template** — A starter template for building payment-enabled applications with x402. Simplified scaffolding demonstrating x402 payment protocol integration with browser wallet support; use it as a foundation for micropayment-enabled services, SaaS applications, or any project that needs frictionless web payments. [View on GitHub](https://github.com/ElliotFriend/x402/tree/stellar-browser-wallet-example/examples/typescript/fullstack/browser-wallet-example) - **Economic Load Balancer** — An intelligent multi-chain payment router that automatically selects the most cost-efficient network for high-frequency AI agent micropayments. [View on GitHub](https://github.com/marcelosalloum/x402/tree/x402-hackathon) - **1-shot Stellar x402 app** — Example x402 app with a video paywall and guide for building payment-enabled apps on Stellar. [View on GitHub](https://github.com/oceans404/1-shot-stellar/tree/main/x402-app) ## Additional Documentation - [OpenZeppelin x402 Facilitator Plugin Source Code](https://github.com/OpenZeppelin/relayer-plugin-x402-facilitator) - [OpenZeppelin x402 Facilitator Docs](https://docs.openzeppelin.com/relayer/1.4.x/guides/stellar-x402-facilitator-guide) - [OpenZeppelin Stellar Relayer SDK](https://github.com/OpenZeppelin/openzeppelin-relayer-sdk) - [OpenZeppelin Stellar Relayer Docs](https://docs.openzeppelin.com/relayer/1.4.x) - [OpenZeppelin Smart Account](https://docs.openzeppelin.com/stellar-contracts/accounts/smart-account) ## Learn more - [x402 on Stellar (Landing page)](https://stellar.org/x402) — Stellar x402 overview and resources - [x402 on Stellar (Blog)](https://stellar.org/blog/foundation-news/x402-on-stellar) — Foundation news and announcement - [x402 protocol (Coinbase Developer Platform)](https://docs.cdp.coinbase.com/x402) — Official x402 protocol overview and spec - [x402 protocol specification](https://www.x402.org) — x402 Specification and Whitepaper - [Coinbase x402 GitHub](https://github.com/coinbase/x402) — Official x402 Protocol GitHub Repo - [x402-stellar (npm)](https://www.npmjs.com/package/x402-stellar) — npm package for x402 on Stellar - [Signing Soroban invocations](../../guides/transactions/signing-soroban-invocations.mdx) — Auth-entry signing and transaction signing on Stellar --- ## Built on Stellar x402 Facilitator The **Built on Stellar** x402 Facilitator is a production-ready payment facilitator for the [x402 protocol](./README.mdx) on Stellar. It handles payment verification and settlement so that sellers can accept per-request payments without running their own blockchain infrastructure. Built with the [OpenZeppelin Relayer][oz-relayer] and the [x402 Facilitator Plugin][oz-plugin], it exposes the standard x402 `/verify`, `/settle`, and `/supported` endpoints and is fully compatible with the [Coinbase x402 ecosystem][x402-ecosystem]. ## Key information | | Testnet | Mainnet | | --- | --- | --- | | **Facilitator URL** | `https://channels.openzeppelin.com/x402/testnet` | `https://channels.openzeppelin.com/x402` | | **API key generation** | [Generate testnet key](https://channels.openzeppelin.com/testnet/gen) | [Generate mainnet key](https://channels.openzeppelin.com/gen) | | **x402 version** | v2 | v2 | | **x402 scheme** | `exact` | `exact` | | **Supported assets** | Any [SEP-41] token (defaults to USDC) | Any [SEP-41] token (defaults to USDC) | Verify endpoint availability: ```bash curl -I https://channels.openzeppelin.com/x402/supported # Expected: HTTP 200 with supported assets/networks ``` ## Get started ### 1. Generate an API key Generate an API key for the network you want to use: - **Testnet**: https://channels.openzeppelin.com/testnet/gen (no authentication required) - **Mainnet**: https://channels.openzeppelin.com/gen (requires GitHub OAuth) Store the generated API key securely. It cannot be retrieved after creation. ### 2. Configure the facilitator URL Use the facilitator URL in your x402 server configuration. Here's an example using `@x402/express`: ```typescript const facilitatorClient = new HTTPFacilitatorClient({ url: "https://channels.openzeppelin.com/x402/testnet", createAuthHeaders: async () => { const headers = { Authorization: `Bearer YOUR_API_KEY` }; return { verify: headers, settle: headers, supported: headers }; }, }); const app = express(); app.use( paymentMiddleware( { "GET /weather": { accepts: [ { scheme: "exact", price: "$0.001", network: "stellar:testnet", payTo: "SERVER_STELLAR_ADDRESS", }, ], description: "Weather data", mimeType: "application/json", }, }, new x402ResourceServer(facilitatorClient).register( "stellar:testnet", new ExactStellarScheme(), ), ), ); app.get("/weather", (req, res) => { res.send({ weather: "sunny", temperature: 70 }); }); app.listen(4021); ``` #### Pricing formats The `price` field supports two formats: **Human-readable** — A dollar-string like `"$0.001"`. The x402 SDK converts this to the equivalent on-chain amount and assumes USDC on Stellar. ```typescript price: "$0.001"; ``` **Explicit asset and amount** — Specify the on-chain [SEP-41] asset contract address and the amount in base units (the smallest unit as defined by the token's `decimals`; for example, if a USDC token has 7 decimals, then 1 USDC = 10,000,000 base units). ```typescript price: { asset: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", amount: "10000", }; ``` Use the explicit format when you want to accept a specific asset other than USDC, or when you need precise control over the on-chain amount. ### 3. Accept payments With the middleware in place, any request to a protected route will trigger the x402 payment flow: 1. The client requests the resource 2. The server responds with `402 Payment Required` and a `PAYMENT-REQUIRED` header containing the payment instructions (price, network, facilitator URL) 3. The client signs a Soroban authorization entry and resubmits the request with a `PAYMENT-SIGNATURE` header 4. The facilitator verifies and settles the payment on-chain 5. The server returns the resource along with a `PAYMENT-RESPONSE` header confirming settlement ## How it works The Built on Stellar facilitator leverages the OpenZeppelin Relayer framework with the x402 Facilitator Plugin. Under the hood, it uses the [OpenZeppelin Relayer][oz-relayer] for high-throughput transaction submission. ### Verification When a payment is received, the facilitator: 1. Validates the x402 protocol version, scheme, and network 2. Decodes the transaction XDR and checks it is an `invokeHostFunction` calling `transfer` 3. Confirms the amount and recipient match the payment requirements 4. Verifies the authorization entries are properly signed by the payer 5. Simulates the transaction on-chain to confirm it will succeed ### Settlement After verification, the facilitator submits the payment on-chain via the OpenZeppelin Relayer and returns confirmation to the server. ## Understanding Soroban authorization x402 on Stellar uses Soroban's authorization model rather than pre-signed transactions. When a client pays for a resource, they sign an **authorization entry** - a statement that authorizes a specific contract call. ### What the client signs The authorization entry contains: - **Contract ID**: The USDC token contract - **Function**: `transfer` - **Arguments**: `from` (payer), `to` (recipient), `amount` - **Expiration**: When this authorization expires This approach provides several benefits: - The facilitator can wrap the auth entry with sponsored fees - No sequence number conflicts - Built-in replay protection via expiration ## Supported features - **Networks**: `stellar:testnet`, `stellar:pubnet` ([CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) identifiers) - **Assets**: Any [SEP-41] token asset (defaults to USDC) - **x402 scheme**: `exact-v2` - **Endpoints**: `/verify`, `/settle`, `/supported` - **Settlement**: Managed on-chain submission via [OpenZeppelin Relayer][oz-relayer] - **Compatibility**: Works with all [x402 ecosystem packages][x402-ecosystem] and [Stellar x402-compatible wallets](./README.mdx#x402-compatible-wallets) ## Self-hosting If you want to run your own instance of the facilitator instead of using the hosted service, you can deploy the OpenZeppelin Relayer with the x402 Facilitator Plugin directly. See the [OpenZeppelin x402 Facilitator guide][oz-facilitator-guide] and the [plugin source code][oz-plugin] for setup instructions. ## Resources - [@x402/stellar (npm)](https://www.npmjs.com/package/@x402/stellar) — npm package for x402 on Stellar - [x402-stellar (repo)](https://github.com/stellar/x402-stellar) — Tools, examples, and references for x402 on Stellar - [x402 on Stellar](./README.mdx) — Overview of the x402 protocol on Stellar - [x402 protocol specification](https://www.x402.org) — x402 specification and whitepaper - [OpenZeppelin x402 Facilitator Plugin][oz-plugin] — Source code - [OpenZeppelin x402 Facilitator Docs][oz-facilitator-guide] — Full configuration and setup guide - [OpenZeppelin Stellar Relayer SDK](https://github.com/OpenZeppelin/openzeppelin-relayer-sdk) — SDK for interacting with the Relayer [oz-relayer]: https://docs.openzeppelin.com/relayer/1.4.x [oz-plugin]: https://github.com/OpenZeppelin/relayer-plugin-x402-facilitator [oz-facilitator-guide]: https://docs.openzeppelin.com/relayer/1.4.x/guides/stellar-x402-facilitator-guide [SEP-41]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md [x402-ecosystem]: https://github.com/coinbase/x402 --- ## x402 Quickstart Guide This tutorial shows how to build the simplest possible paid API with Node.js and Express using the x402 packages with settlement on the Stellar network. To follow this guide, you will need [Node.js](https://nodejs.org) installed locally. Recommend using the latest LTS version. ## Create A Project Create a new folder for the tutorial and initialize a Node.js project: ```bash mkdir x402-quickstart-guide cd x402-quickstart-guide npm init -y npm pkg set type=module ``` The `type=module` setting lets you run the `import` syntax used in the example files below. Install the npm packages used by the server and client examples: ```bash npm install express dotenv @stellar/stellar-sdk @x402/core @x402/express @x402/fetch @x402/stellar ``` This installs everything needed for both `server.js` and `client.js`. ## Create `server.js` Create a file named `server.js` and paste in the following code: ```js title="server.js" // Set up configuration const PORT = "3001"; const ROUTE_PATH = "/my-service"; const PRICE = "$0.01"; // price in USDC const NETWORK = "stellar:testnet"; // Use pubnet for mainnet const FACILITATOR_URL = "https://www.x402.org/facilitator"; const PAY_TO = "GABC123...YOURADDRESS...123ABC"; // Add Stellar Address const app = express(); // Return information for / app.get("/", (_, res) => res.json({ route: ROUTE_PATH, price: PRICE, network: NETWORK }), ); // Create x402 middleware config app.use( paymentMiddlewareFromConfig( { [`GET ${ROUTE_PATH}`]: { accepts: { scheme: "exact", price: PRICE, network: NETWORK, payTo: PAY_TO, }, }, }, new HTTPFacilitatorClient({ url: FACILITATOR_URL }), [{ network: NETWORK, server: new ExactStellarScheme() }], ), ); // Attach x402 middleware config app.get(ROUTE_PATH, (_, res) => res.json({ secret: "valuable content" })); // Start server app.listen(Number(PORT), () => { console.log(`x402 server listening on http://localhost:${PORT}${ROUTE_PATH}`); }); ``` Update `PAY_TO` with the Stellar address that should receive USDC funds whenever a client pays for your service. Your wallet will need a testnet trustline setup for USDC. More information here: [Setting up a testnet wallet](#setting-up-a-testnet-wallet) `FACILITATOR_URL` points to a managed facilitator endpoint that handles verification and settlement. In this example, the facilitator is Coinbase's testnet facilitator. Start the API locally: ```bash node server.js ``` When a client requests your endpoint, your server responds with a `402 Payment Required` status. The response includes headers that describe the payment requirements so that a compliant client can build a signed payment payload and retry the request. Behind the scenes, the facilitator handles verification and onchain settlement. You do not need to run your own blockchain nodes or build raw transactions yourself. A managed facilitator service takes the signed payment payload from the client, verifies it against the specified network and asset, settles the transaction, and returns verification to your server so it can complete the request. The client generates a signed payment payload that authorizes a stablecoin transfer, the facilitator verifies and settles that payload on the Stellar network, and the server returns a `200` response with the protected data. The funds settle directly to the wallet address in `PAY_TO`. ## Create `client.js` With your Express server running, create a file named `client.js` and paste in the following code: ```js title="client.js" // Load environment variables dotenv.config({ path: fileURLToPath(new URL("./.env", import.meta.url)), quiet: true, }); // Set up configuration const STELLAR_PRIVATE_KEY = process.env.STELLAR_PRIVATE_KEY; const RESOURCE_SERVER_URL = "http://localhost:3001"; // host and port const ENDPOINT_PATH = "/my-service"; const NETWORK = "stellar:testnet"; // use pubnet for mainnet const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org"; async function main() { // Setup x402Client configuration const url = new URL(ENDPOINT_PATH, RESOURCE_SERVER_URL).toString(); const signer = createEd25519Signer(STELLAR_PRIVATE_KEY, NETWORK); const rpcConfig = STELLAR_RPC_URL ? { url: STELLAR_RPC_URL } : undefined; const client = new x402Client().register( "stellar:*", new ExactStellarScheme(signer, rpcConfig), ); const httpClient = new x402HTTPClient(client); console.log(`Target: ${url}\nClient address: ${signer.address}`); // Try without payment const firstTry = await fetch(url); console.log(`Payment requested: ${firstTry.status}`); // Grab response which includes instructions for payment const paymentRequired = httpClient.getPaymentRequiredResponse((name) => firstTry.headers.get(name), ); // Create payment payload let paymentPayload = await client.createPaymentPayload(paymentRequired); const networkPassphrase = getNetworkPassphrase(NETWORK); const tx = new Transaction( paymentPayload.payload.transaction, networkPassphrase, ); const sorobanData = tx.toEnvelope().v1()?.tx()?.ext()?.sorobanData(); // Configure fee to 1 stroop, prevents testnet facilitator limit issue if (sorobanData) { paymentPayload = { ...paymentPayload, payload: { ...paymentPayload.payload, transaction: TransactionBuilder.cloneFrom(tx, { fee: "1", sorobanData, networkPassphrase, }) .build() .toXDR(), }, }; } const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload); // Send request const paidResponse = await fetch(url, { method: "GET", headers: paymentHeaders, }); const text = await paidResponse.text(); const paymentResponse = httpClient.getPaymentSettleResponse((name) => paidResponse.headers.get(name), ); // Log response console.log("Settlement response:", paymentResponse); console.log(`Access Granted! ${paidResponse.status} "${text}"`); } main().catch((error) => { console.error("Client failed:", error); process.exit(1); }); ``` ### Handling payment responses When integrating x402 into your own client, handle the key response states: ```js const response = await fetch(protectedUrl, { headers: paymentHeaders }); if (response.status === 402) { const paymentRequired = response.headers.get("PAYMENT-REQUIRED"); console.error("Payment required:", paymentRequired); // Re-initiate payment flow } else if (response.status === 200) { const data = await response.json(); console.log("Access granted:", data); } else { console.error("Unexpected error:", response.status); } ``` ### Setting up a testnet wallet Next, create a fresh account and fund it with testnet XLM and testnet USDC using Stellar Lab: - [Fund a testnet account in Stellar Lab](https://lab.stellar.org/account/fund) 1. Create a new keypair using [Stellar Lab](https://lab.stellar.org/account/create) or the CLI: ```bash stellar keys generate --network testnet my-x402-wallet stellar keys address my-x402-wallet # Returns G... public key stellar keys show my-x402-wallet # Returns S... secret key ``` 2. Fund with testnet XLM (Native Stellar Token): https://lab.stellar.org/account/fund 3. Create the USDC trustline (there's a button on the fund page above), sign and submit that transaction (for detailed instructions, [see below](#step-3-establish-usdc-trustline)) 4. Visit the Circle faucet, select Stellar Testnet from the networks and add your public key for the wallet address input: https://faucet.circle.com Create a local environment file for the client and add your Stellar testnet secret key: ```bash title=".env" STELLAR_PRIVATE_KEY=S... ``` Edit `.env` and replace `S...` with the secret key for the account that will sign transactions and pay for the resource. :::caution Secret keys provide full access to any digital assets held within the wallet. Use `.env` files only for hot wallets in testnet deployments. ::: ### Step 3 (expanded): Establish USDC trustline A trustline tells Stellar your account accepts USDC. Without it, you cannot receive payments. **Testnet USDC Issuer:** `GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5` #### Using Stellar Lab 1. Go to [Transaction Builder](https://lab.stellar.org/transaction/build) 2. Enter your public key as source account 3. Click "Fetch next sequence number" 4. Add Operation → "Change Trust" 5. Asset Code: `USDC` 6. Issuer: `GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5` 7. Sign and submit #### Using code ```javascript Keypair, Networks, TransactionBuilder, Operation, Asset, Horizon, } from "@stellar/stellar-sdk"; const server = new Horizon.Server("https://horizon-testnet.stellar.org"); const keypair = Keypair.fromSecret(process.env.STELLAR_PRIVATE_KEY); const account = await server.loadAccount(keypair.publicKey()); const USDC = new Asset( "USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", ); const tx = new TransactionBuilder(account, { fee: "100", networkPassphrase: Networks.TESTNET, }) .addOperation(Operation.changeTrust({ asset: USDC })) .setTimeout(30) .build(); tx.sign(keypair); await server.submitTransaction(tx); ``` ## Run The Client Once the account is funded and the secret key is in `.env`, run the client in a second terminal: ```bash node client.js ``` This sends `$0.01` of testnet USDC from the client account to the server account. The client then receives the protected JSON data: `{ secret: "valuable content" }` The server is set up as an MVP for machine-to-machine payments. If you want to enable human payments, the next step is to build a paywall. Take a look at the demo code for examples: https://github.com/stellar/x402-stellar/tree/main/examples/simple-paywall ## Additional Documentation - [OpenZeppelin x402 Facilitator Plugin Source Code](https://github.com/OpenZeppelin/relayer-plugin-x402-facilitator) - [OpenZeppelin x402 Facilitator Docs](https://docs.openzeppelin.com/relayer/1.4.x/guides/stellar-x402-facilitator-guide) - [OpenZeppelin Stellar Relayer SDK](https://github.com/OpenZeppelin/openzeppelin-relayer-sdk) - [OpenZeppelin Stellar Relayer Docs](https://docs.openzeppelin.com/relayer/1.4.x) - [OpenZeppelin Smart Account](https://docs.openzeppelin.com/stellar-contracts/accounts/smart-account) ## Learn more - [x402 on Stellar (Landing page)](https://stellar.org/x402) - Stellar x402 overview and resources - [x402 on Stellar (Blog)](https://stellar.org/blog/foundation-news/x402-on-stellar) - Foundation news and announcement - [x402 protocol (Coinbase Developer Platform)](https://docs.cdp.coinbase.com/x402) - Official x402 protocol overview and spec - [x402 protocol specification](https://www.x402.org) - x402 Specification and Whitepaper - [x402 GitHub](https://github.com/x402-foundation/x402) - Official x402 Protocol GitHub Repo - [@x402/stellar (npm)](https://www.npmjs.com/package/@x402/stellar) - npm package for x402 on Stellar - [x402-stellar (repo)](https://github.com/stellar/x402-stellar) - Tools, examples, and references for x402 on Stellar - [Signing Soroban invocations](../../guides/transactions/signing-soroban-invocations.mdx) - Auth-entry signing and transaction signing on Stellar --- ## Build Blockchain Apps: Guides, Tools, and Best Practices for Development # Build Applications This section walks you through design considerations for applications and tutorials for building applications with or without smart contracts. --- ## Application Design Considerations ## Custody models When building an application, one of the first things you have to decide is how your users’ secret keys will be secured and stored. Stellar applications give users access to their accounts that are stored on the ledger, and access to these accounts is controlled by the account’s secret key. That secret key proves that the user has custody or “owns” the account. There are four custody options to consider: - Non-custodial service - the user of the application stores their own secret key - Custodial service - the service provider (application) stores the users’ secret keys - Mixture of both - with the use of [multisig](../../learn/fundamentals/transactions/signatures-multisig.mdx), this option is useful for maintaining non-custodial status while still allowing for account recovery - Third-party key management services - integrate a third-party custodial service into your application that can store your users’ secret keys ### Non-custodial service In a non-custodial service, the user of the application stores the secret key for their account and permissions the application to send requests to delegate transaction signing. There are some potential usability issues as the user has to know how to securely store their own account credentials and safely navigate transaction signing on their end. If they lose their secret key, they will also lose access to their account. Typically, non-custodial applications create or import a pre-existing Stellar account for each user. ### Custodial service With a custodial service, the service provider (an application such as a centralized exchange) stores the users’ secret keys and delegates usage rights to the user. Many custodial services choose to use a single pooled Stellar account (called shared, omnibus, or [pooled accounts](../guides/transactions/pooled-accounts-muxed-accounts-memos.mdx)) to handle transactions on behalf of their users instead of creating a new Stellar account for each user. To distinguish between individual users in a pooled account, we encourage the implementation of [muxed accounts](../guides/transactions/pooled-accounts-muxed-accounts-memos.mdx). ### A mixture of non-custodial and custodial Building an application with [multi-signature](../../learn/fundamentals/transactions/signatures-multisig.mdx) capabilities allows you to have a non-custodial service with account recovery. If the user loses their secret key, they can still sign transactions with other authorized signatures, granted the signature threshold is high enough. ### Third-party key management services​ There are several apps and services that specialize in adding additional security layers to users' accounts. Check them out if you're interested in integrating a third-party key management service: - [Ledger](https://www.ledger.com) - [Trezor](https://trezor.io) - [StellarGuard](https://stellarguard.me) - [LobstrVault](https://vault.lobstr.co) ## Application security Even though wallets can operate client-side, they deal with a user’s secret keys, which give direct access to their account, and to any value they hold. That’s why it’s essential to require all web traffic to flow over strong TLS methods. Even when developing locally, use a non-signed localhost certificate to develop secure habits from the very beginning. Stellar is a powerful money-moving software — don’t skimp on security. For more information, check out our guide to [securing web-based products](https://stellar.org/developers/guides/walkthroughs/securing-web-projects.html). ## Wallet services A wallet typically has these basic functions: key storage, account creation, transaction signing, and queries to the Stellar database. There are some services that take care of all of these functions for you, so you can build whatever you’d like around it. Check out some of these wallet services below. - [Albedo](https://albedo.link) - [Freighter](https://www.freighter.app) ## Account creation strategies In this section, we will go over the new user account creation flow between non-custodial wallets and anchors with SEP-24 and/or SEP-6 implementations. A Stellar account is created with a keypair (a public key and private key) and the minimum balance of XLM. When a new customer downloads the wallet application and goes through the deposit flow for the first time, their Stellar account can be created by either the user’s wallet application or the anchor facilitating the first deposit. This section describes each of these strategies. ### Option 1: The anchor creates and funds the Stellar account​ For this option, the wallet needs to allow users to initiate their first deposit without having to add an asset/establish a trustline. The wallet then prompts the user to add the trustline once funds are received by the anchor. The flow looks like this: 1. The wallet registers a new user and issues a keypair. 2. The wallet initiates the first deposit on behalf of the user without requiring the user to add the asset/create the trustline. 3. The anchor provides deposit instructions to the customer. 4. The user transfers money from a bank account to the anchor’s bank account. 5. Once the anchor receives the transfer, the anchor creates and funds the Stellar account for the customer. 6. The wallet detects that the account has been created and a trustline must be established. 7. The wallet prompts the user to add the asset/create the trustline. 8. Finally, the anchor sends the deposit funds to the user’s Stellar account. :::info An anchor should always maintain a healthy amount of XLM in its distribution account to support new account creations. If doing so becomes unsustainable, it’s recommended that the anchor collaborates with wallets to determine a strategy based on the number of account creation requests. The recommended amount is 2XLM per user account creation (1XLM to meet the minimum balance requirement, and 1XLM for establishing trustlines and covering transaction fees). ::: With the flow described above, the wallet and the anchor have to facilitate listening for and responding to the trustline status, which can create user experience frictions when waiting for the trustline to be established. To address this issue, Protocol 15 introduced claimable balances, which enhance the flow by allowing users to start using the wallet without having to secure XLM. Both the wallet and the anchor have to implement claimable balance support in order to make this flow work. The flow with Claimable Balances looks like this: 1. The wallet registers a new user, and generates a keypair. 2. The wallet initiates a deposit on behalf of a user. 3. The anchor provides deposit instructions to the wallet. 4. The user transfers money from a bank account to the anchor’s account. 5. The anchor creates and funds the user's Stellar account plus the amount required for trustlines and transaction fees. Again, we suggest 2 XLM to start. 6. The anchor creates a Claimable Balance. 7. The wallet detects the Claimable Balance for the account, claims the funds, and posts it in the wallet. ### Option 2: the wallet creates and funds the Stellar account upon user sign-up​ For this option, the wallet creates and funds the Stellar account upon every new user sign-up with the minimum requirement of 1XLM, plus the .5XLM reserve for establishing the first trustline, plus a bit more to cover transaction fees. For more information on minimum balances, check out the [Lumens section](../../learn/fundamentals/lumens.mdx#minimum-balance). The flow looks like this: 1. Upon a new user signup, the wallet issues a keypair, then creates and funds the user's Stellar account with 2XLM. 2. Then the wallet creates a trustline, and initiates the first deposit. 3. Once the deposit request is sent to the anchor, the anchor provides instructions for the deposit. 4. The customer transfer funds from a personal bank account to the anchor’s account. 5. The anchor receives the funds, then sends them to the user’s Stellar account. 6. The wallet detects that funds were sent and notifies the user. :::note In the examples above, we suggest having the anchor or wallet cover minimum balance and trustline XLM requirements by depositing funds directly into a user's account. We made that suggestion for the sake of simplicity, but in all cases, the anchor or wallet could instead use sponsored reserves to ensure that when a user closes a trustline or merges their account, the reserve reverts to the sponsoring account rather than to the user's account. ::: --- ## Build a dapp Frontend: Connect Wallets, Handle Transactions & More # Develop a Contract with Frontend Templates This guide picks up where [Getting Started tutorial](../smart-contracts/getting-started/README.mdx) left off. From there, we'll build our own simple frontend template. Building our own template will be a great way to learn how they work. They're not that complicated! ## Make your own template Let’s make our own template! In this example template, we use SolidJS as the JavaScript framework, but other frameworks can be used with minor modifications. The template is using the `hello-world` example smart contract. As a part of the template initialization, bindings for the `hello-world` smart contract are created. This example template is very simple, most of the work goes into creating the `initialize.js` file, which is used to take care of creating a user account, building and deploying the smart contract, and creating the smart contract TypeScript bindings. ### 1. Initialize a SolidJS project ```bash # See https://github.com/solidjs/templates for SolidJS template options npx degit solidjs/templates/vanilla/bare soroban-template-solid cd soroban-template-solid npm install npm run dev ``` The basic SolidJS application is now running on localhost port 3000. #### Dependencies Most of the needed dependencies are already included by the SolidJS template, we just need to add three more: ```bash npm install dotenv glob util ``` The `dotenv` package is needed for reading the environment variables, `glob` is used to find files in the project based on a pattern, and `util` contains a function that can be used to execute system commands asynchronously. #### Smart contract If you already have the `hello-world` smart contract, it can be used for the following steps. If not, run the `stellar contract init heelo-world` CLI command in you project root folder. Since we are going to interact with the smart contract from the `initialize.js` script, add the relative path to the smart contract in the `.env` file in the next step so the `initialize.js` script can find the contract files. The root directory should look like this: ```text ├── hello-world │ └── contracts │ └── hello-world │ ├── src │ │ └── lib.rs │ ├── Cargo.toml │ └── Makefile └── soroban-template-solid ├── node_modules ├── packages ├── src │ ├── App.tsx │ └── index.tsx ├── .env ├── index.html ├── tsconfig.json ├── vite.config.ts ├── initialize.js ├── package.json └── Cargo.toml ``` ### 2. Environment variables The SolidJS code itself doesn’t need environment variables for this simple example, but since we are going to add smart contract bindings, it makes sense to store information about the network and the user in an .env file instead of hard coding those values. These are the variables needed: ```bash PUBLIC_STELLAR_NETWORK="testnet" PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" PUBLIC_STELLAR_RPC_URL="https://soroban-testnet.stellar.org" PUBLIC_STELLAR_ACCOUNT="my-user-name" PUBLIC_STELLAR_CONTRACT_PATH="../hello-world" ``` The variables used here are for deploying the contract to testnet and creating the contract bindings for testnet. The user name can be any name, but let’s say you use alice, and have previously created the user `alice` with the Stellar CLI, creating a new account named `alice` will fail. ### 3. initialize.js The goal is to have a script that will handle everything smart contract-related, from creating a user account to deploying the smart contract and providing a TypeScript binding for easy smart contract calls from frontend code. The file `initialize.js` contains that script, and the functionality of it will be broken down in the following sections. #### Definitions Before diving into the functions in the `initialize.js` script, a few constants and variables are defined. The most noteworthy here is `execAsync()`, which will let us execute CLI commands and wait for the command responses. ```javascript // Get directory names const __filename = fileURLToPath(import.meta.url); const dirname = path.dirname(__filename); // Define array to hold deployed smart contract info var smartContracts = Array(); // Run exec commands asynchronously const execAsync = promisify(exec); ``` #### User Now that we have the environment variables, dependencies and definitions taken care of, we can get into the scripts that handle the smart contract deployment and integration. First step towards the integration is to create a user: ```javascript // ###################### Create User ######################## function createUser() { execSync( `stellar keys generate --fund ${process.env.PUBLIC_STELLAR_ACCOUNT} | true`, ); } ``` The user is created by calling the Stellar CLI command `stellar keys generate`, and funding it with Friendbot. The user’s name is fetched from the environment variables. You can check the new user’s public key by running this CLI command: ```bash stellar keys public-key ``` With the public key, you can look up the account on [Stellar Expert](https://stellar.expert/explorer/testnet). #### Build contracts We want the script to build the contract, or contracts in case there are more than one, and it’s a 2-step process. First, we clean up the target folder in case there’s a previous build, and then we call the CLI command to build the contract(s). ```javascript // Remove all previous build files function removeFiles(pattern) { glob(pattern).forEach((entry) => rmSync(entry)); } function buildAll() { removeFiles( `${dirname}/${process.env.PUBLIC_STELLAR_CONTRACT_PATH}/target/wasm32v1-none/release/*.wasm`, ); removeFiles( `${dirname}/${process.env.PUBLIC_STELLAR_CONTRACT_PATH}/target/wasm32v1-none/release/*.d`, ); execSync( `cd ${process.env.PUBLIC_STELLAR_CONTRACT_PATH} && stellar contract build`, ); console.log("Build complete"); } ``` The helper function `removeFiles` will delete any `wasm` or `d` files in the target directory. #### Deploy contracts Now that the smart contract has been built, we can deploy it to the network, so we can invoke the its functions from any client, such as our SolidJS template. There are three functions related to contract deployment. One that uses the Stellar CLI to deploy the Wasm to the network (`deploy()`), one that calls the deploy function for each Wasm found (`deployAll()`), in case there is more than one smart contract, and finally a helper function that gets the contract name by parsing the Wasm file name. ```javascript // Get smart contract name from filename function filenameNoExtension(filename) { return path.basename(filename, path.extname(filename)); } async function deploy(wasm) { // Deploy a single contract and get the contract id const { stdout, stderr } = await execAsync( `stellar contract deploy --wasm ${wasm} --ignore-checks --alias ${filenameNoExtension(wasm)} --source ${process.env.PUBLIC_STELLAR_ACCOUNT} --network ${process.env.PUBLIC_STELLAR_NETWORK} --rpc-url ${process.env.PUBLIC_STELLAR_RPC_URL} --network-passphrase "${process.env.PUBLIC_STELLAR_NETWORK_PASSPHRASE}"`, ); // Add deployed contract to array with alias, wasm path and contract id smartContracts.push({ alias: filenameNoExtension(wasm), wasm: wasm, contractid: stdout.trimEnd(), }); console.log(`Deployed ${filenameNoExtension(wasm)}`); } async function deployAll() { console.log("Deploying all contracts"); const wasmFiles = glob( `${dirname}/${process.env.PUBLIC_STELLAR_CONTRACT_PATH}/target/wasm32v1-none/release/*.wasm`, ); for (const wasm of wasmFiles) { await deploy(wasm); } } ``` The `deploy()` function will get the contract ID from the CLI call, and add the contract name, wasm file path, and contract ID to the `smartContracts[]` array. In this example, we only use one smart contract, but it’s not uncommon to use multiple smart contracts in a dapp, so the template supports the use of multiple contracts. ##### Create bindings The Stellar CLI has a convenient command to create an NPM package that makes it easy to call smart contract functions from a JavaScript/TypeScript-based frontend. We call the package “bindings” because that’s what it does: it binds the contract and the frontend together. As with the contract build functions, the binding function is also capable of handling multiple contracts, so there’s a function for creating the binding package for a contract (`bind()`) and a function that calls `bind()` for each contract (`bindAll()`). ```javascript function bind({ alias, wasm, contractid }) { // Create bindings for a deployed contract execSync( `stellar contract bindings typescript --contract-id ${contractid} --output-dir ${dirname}/packages/${alias} --overwrite`, ); // Build the package execSync(`(cd ${dirname}/packages/${alias} && npm i && npm run build)`); } async function bindAll() { // Bind all deployed contracts for (const contract of smartContracts) { await bind(contract); } } ``` The `bindAll()` function iterates the `smartContracts[]` array. The reason for not just using the array of wasms, like in the `deployAll()` function, is that we need the contract ID to invoke the generated bindings functions on the network. #### Import bindings The last step is to configure the smart contract bindings client. The `importContract()` function creates a TypeScript file with a script that configures a client based on the smart contract ID, the network passphrase, and the RPC URL. The client makes it easy to make calls in the frontend code to the smart contract functions. The file is stored with the contract name as the file name, and with the `.ts` as the extension, e.g., `hello_world.ts`. ```javascript function importContract({ alias, wasm, contractid }) { const outputDir = `${dirname}/src/contracts/`; mkdirSync(outputDir, { recursive: true }); const importContent = `import { Client } from '${alias}';\n` + `export default new Client({\n` + ` contractId: "${contractid}",\n` + ` networkPassphrase: "${process.env.PUBLIC_STELLAR_NETWORK_PASSPHRASE}",\n` + ` rpcUrl: "${process.env.PUBLIC_STELLAR_RPC_URL}",\n` + `${ process.env.PUBLIC_STELLAR_NETWORK === "local" || "standalone" ? ` allowHttp: true,\n` : null }` + `});\n`; const outputPath = `${outputDir}/${alias}.ts`; writeFileSync(outputPath, importContent); console.log(`Created import for ${alias}`); } function importAll() { smartContracts.forEach(importContract); } ``` #### Main function At last, we have the main function, which calls the above functions in the right order. Note the asynchronous calls of `deployAll()` and `bindAll()`. The functions following them depend on the completion of the previous functions. ```javascript // Calling the functions in sequence async function main() { createUser(); buildAll(); await deployAll(); await bindAll(); importAll(); } main().catch((e) => { console.error("Initialization failed", e); process.exit(1); }); ``` #### Complete initialize.js file This is the complete file. Place it in the SolidJS root: ```javascript // ###################### Definitions ######################## // Get directory names const __filename = fileURLToPath(import.meta.url); const dirname = path.dirname(__filename); // Define array to hold deployed smart contracts var smartContracts = Array(); // Run exec commands asynchronously const execAsync = promisify(exec); // ###################### Create User ######################## function createUser() { execSync( `stellar keys generate --fund ${process.env.PUBLIC_STELLAR_ACCOUNT} | true`, ); } // ###################### Build Contracts ######################## // Remove all previous build files function removeFiles(pattern) { glob(pattern).forEach((entry) => rmSync(entry)); } function buildAll() { removeFiles(`${dirname}/target/wasm32v1-none/release/*.wasm`); removeFiles(`${dirname}/target/wasm32v1-none/release/*.d`); execSync(`stellar contract build`); console.log("Build complete"); } // ###################### Deploy Contracts ######################## // Get smart contract name from filename function filenameNoExtension(filename) { return path.basename(filename, path.extname(filename)); } async function deploy(wasm) { // Deploy a single contract and get the contract id const { stdout, stderr } = await execAsync( `stellar contract deploy --wasm ${wasm} --ignore-checks --alias ${filenameNoExtension(wasm)} --source ${process.env.PUBLIC_STELLAR_ACCOUNT} --network ${process.env.PUBLIC_STELLAR_NETWORK} --rpc-url ${process.env.PUBLIC_STELLAR_RPC_URL} --network-passphrase "${process.env.PUBLIC_STELLAR_NETWORK_PASSPHRASE}"`, ); // Add deployed contract to array with alias, wasm path and contract id smartContracts.push({ alias: filenameNoExtension(wasm), wasm: wasm, contractid: stdout.substring(0, stdout.length - 1), }); console.log(`Deployed ${filenameNoExtension(wasm)}`); } async function deployAll() { console.log("Deploying all contracts"); const wasmFiles = glob(`${dirname}/target/wasm32v1-none/release/*.wasm`); for (const wasm of wasmFiles) { await deploy(wasm); } } // ###################### Create Bindings ######################## function bind({ alias, wasm, contractid }) { // Create bindings for a deployed contract execSync( `stellar contract bindings typescript --contract-id ${contractid} --output-dir ${dirname}/packages/${alias} --overwrite`, ); // Build the package execSync(`(cd ${dirname}/packages/${alias} && npm i && npm run build)`); // Install the package execSync(`pnpm add file:./packages/${alias}`); } async function bindAll() { // Bind all deployed contracts for (const contract of smartContracts) { await bind(contract); } } // ###################### Import Bindings ######################## function importContract({ alias, wasm, contractid }) { const outputDir = `${dirname}/src/contracts/`; mkdirSync(outputDir, { recursive: true }); const importContent = `import * as Client from '${alias}';\n` + `export default new Client.Client({\n` + ` contractId: "${contractid}",\n` + ` networkPassphrase: "${process.env.PUBLIC_STELLAR_NETWORK_PASSPHRASE}",\n` + ` rpcUrl: "${process.env.PUBLIC_STELLAR_RPC_URL}",\n` + `${ process.env.PUBLIC_STELLAR_NETWORK === "local" || "standalone" ? ` allowHttp: true,\n` : null }` + `});\n`; const outputPath = `${outputDir}/${alias}.ts`; writeFileSync(outputPath, importContent); console.log(`Created import for ${alias}`); } function importAll() { smartContracts.forEach(importContract); } // ###################### Main ######################## // Calling the functions in sequence async function main() { createUser(); buildAll(); await deployAll(); await bindAll(); importAll(); } main().catch((e) => { console.error("Initialization failed", e); process.exit(1); }); ``` ### 4. Modify Vite config SolidJS is using the build tool Vite, and we need to make a minor addition to the Vite configuration file (`vite.config.ts`) for the module exports to work. Add these lines to the config: ```javascript optimizeDeps: { include: ['@stellar/stellar-sdk', 'hello_world'], }, ``` ### 5. Build the frontend The template is now ready to use the smart contract and its binding through the client. Let’s build a very simple dapp, a frontend for the Hello World smart contract, with a text input field and a send button. When a user enters a text string in the input field and clicks the send button, the contract function is invoked with the text string as the argument. The returned value, a string array, is displayed in the frontend. Here’s an example of how the code could look the existing code in the `src/App.tsx` file with this code: ```javascript const App: Component = () => { const [input, setInput] = createSignal(''); const [greeting, setGreeting] = createSignal(''); const [loading, setLoading] = createSignal(false); const [error, setError] = createSignal(null); async function getGreeting(e?: Event) { e?.preventDefault(); setError(null); setLoading(true); try { const { result } = await helloWorld.hello({ to: input() || 'you' }); const greet = Array.isArray(result) ? result.join(' ') : String(result); setGreeting(greet); } catch (err: any) { console.error(err); setError(err?.message || 'Unknown error'); } finally { setLoading(false); } } return ( Hello Soroban Solid Template!
setInput(e.target.value)} />
{error() && Error: {error()}} {greeting()} ); }; export default App; ``` ### 6. Try it out We can now run the code with this command: ```bash npm run dev ``` The URL for the dapp will be shown in the terminal, typically it’s `http://localhost:3000` unless the port 3000 is already in use. --- ## Build a Payment App with the JS SDK Create a basic payment application to transfer tokens between accounts on the Stellar Testnet. --- ## Account Creation Accounts are the central data structure in Stellar and can only exist with a valid keypair (a public and secret key) and the required minimum balance of XLM. Read more in the [Accounts section]. ## User experience To start, we'll have our user create an account. In BasicPay, the signup page will display a randomized public and secret keypair that the user can select with the option to choose a new set if preferred. :::info Since we are building a [non-custodial application], the encrypted secret key will only ever live in the browser. It will never be shared with a server or anybody else. ::: ![public and private keys](/assets/basic-pay/public-and-private-keys.png) Next, we'll trigger the user to submit a pincode to encrypt their secret key before it gets saved to their browser's `localStorage` (this is handled by the [`@stellar/typescript-wallet-sdk-km`]). The user will need to remember their pincode for future logins and to submit transactions. With BasicPay, when the user clicks the “Signup” button, they will be asked to confirm their pincode. When they do, the `create_account` operation is triggered, and the user's account is automatically funded with XLM for the minimum balance (starting with 10,000 XLM). ![funded account](/assets/basic-pay/funded-account.png) When you're ready to move the application to Pubnet, accounts will need to be funded with real XLM. This is something the application can cover itself by depositing XLM into the user's account, with the use of [sponsored reserves], or the user can cover the required balance with their own XLM. ## Code implementation We will create a Svelte `store` to interact with our user's randomly generated keypair. The store will take advantage of [`@stellar/typescript-wallet-sdk-km`] to encrypt/decrypt the keypair, as well as sign transactions. ### Creating the `walletStore` store Our `walletStore` will make a few things possible throughout our application. 1. We can "register" a keypair, which encrypts the keypair, stores it in the browser's storage, and keeps track of that keypair's `keyId`. 2. We can "sign" transactions by providing the pincode to decrypt the keypair. 3. We can "confirm" the pincode is valid for the stored keypair (or that it matches for signups). ```js title="/src/lib/stores/walletStore.js" KeyManager, LocalStorageKeyStore, ScryptEncrypter, KeyType, } from "@stellar/typescript-wallet-sdk-km"; // We are wrapping this store in its own function which will allow us to write // and customize our own store functions to maintain consistent behavior // wherever the actions need to take place. function createWalletStore() { // Make a `persisted` store that will determine which `keyId` the // `keyManager` should load, when the time comes. const { subscribe, set } = persisted("bpa:walletStore", { keyId: "", publicKey: "", }); return { subscribe, // Registers a user by storing their encrypted keypair in the browser's // `localStorage`. register: async ({ publicKey, secretKey, pincode }) => { try { // Get our `KeyManager` to interact with stored keypairs const keyManager = setupKeyManager(); // Use the `keyManager` to store the key in the browser's local // storage let keyMetadata = await keyManager.storeKey({ key: { type: KeyType.plaintextKey, publicKey: publicKey, privateKey: secretKey, }, password: pincode, encrypterName: ScryptEncrypter.name, }); // Set the `walletStore` fields for the `keyId` and `publicKey` set({ keyId: keyMetadata.id, publicKey: publicKey, // Don't include this in a real-life production application. // It's just here to make the secret key accessible in case // we need to do some manual transactions or something. devInfo: { secretKey: secretKey, }, }); } catch (err) { console.error("Error saving key", err); throw error(400, { message: err.toString() }); } }, // Compares a submitted pincode to make sure it is valid for the stored, encrypted keypair. confirmPincode: async ({ pincode, firstPincode = "", signup = false }) => { // If we are not signing up, make sure the submitted pincode successfully // decrypts and loads the stored keypair. if (!signup) { try { const keyManager = setupKeyManager(); let { keyId } = get(walletStore); await keyManager.loadKey(keyId, pincode); } catch (err) { throw error(400, { message: "invalid pincode" }); } // If we are signing up for the first time (thus, there is no stored // keypair), just make sure the first and second pincodes match. } else { if (pincode !== firstPincode) { throw error(400, { message: "pincode mismatch" }); } } }, // Sign and return a Stellar transaction sign: async ({ transactionXDR, network, pincode }) => { try { // Get our `keyManager` to interact with stored keypairs const keyManager = setupKeyManager(); // Use the `keyManager` to sign the transaction with the // encrypted keypair let signedTransaction = await keyManager.signTransaction({ transaction: TransactionBuilder.fromXDR(transactionXDR, network), id: get(walletStore).keyId, password: pincode, }); return signedTransaction; } catch (err) { console.error("Error signing transaction", err); throw error(400, { message: err.toString() }); } }, }; } // We export `walletStore` as the variable that can be used to interact with the wallet store. export const walletStore = createWalletStore(); // Configure a `KeyManager` for use with stored keypairs. const setupKeyManager = () => { // We make a new `KeyStore` const localKeyStore = new LocalStorageKeyStore(); // Configure it to use `localStorage` and specify a(n optional) prefix localKeyStore.configure({ prefix: "bpa", storage: localStorage, }); // Make a new `KeyManager`, that uses the previously configured `KeyStore` const keyManager = new KeyManager({ keyStore: localKeyStore, }); // Configure the `KeyManager` to use the `scrypt` encrypter keyManager.registerEncrypter(ScryptEncrypter); // Return the `KeyManager` for use in other functions return keyManager; }; ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stores/walletStore.js ### Creating the account on the Stellar network After we've registered the user, we need to fund the account on the Stellar network. As discussed previously, there are multiple ways to accomplish this task, but we are using Friendbot to ensure the user has some Testnet XLM to experiment with. ```js title="/src/lib/stellar/horizonQueries.js" // Fund an account using the Friendbot utility on the Testnet. export async function fundWithFriendbot(publicKey) { console.log(`i am requesting a friendbot funding for ${publicKey}`); await server.friendbot(publicKey).call(); } ``` Source: https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/horizonQueries.js ### Using the `walletStore` store Our `walletStore` is used in a ton of places in our application, especially in the confirmation modal when asking a user to input their pincode. Read on to see how we've done that. [accounts section]: ../../../learn/fundamentals/stellar-data-structures/accounts.mdx [non-custodial application]: ../application-design-considerations.mdx#non-custodial-service [`@stellar/typescript-wallet-sdk-km`]: https://www.npmjs.com/package/@stellar/typescript-wallet-sdk-km [sponsored reserves]: ../../guides/transactions/sponsored-reserves.mdx [contacts list]: ./contacts-list --- ## Anchor Integration Interact with non-Lumen assets in your application. Use anchors for deposits and withdrawals to get tokens on or off the network. --- ## SEP-1: Stellar TOML The [`stellar.toml` file](../../../../tokens/publishing-asset-info.mdx#completing-your-stellartoml) is a common place where the Internet can find information about an organization’s Stellar integration. Regardless of which type of transfer we want to use (SEP-6 or SEP-24), we'll need to start with SEP-1. For anchors, we’re interested in the `CURRENCIES` they issue, the `TRANSFER_SERVER` and/or `TRANSFER_SERVER_SEP0024` keywords that indicate if the anchor supports SEP-6, SEP-24, or both, and the `WEB_AUTH_ENDPOINT` which allows a wallet to set up an authenticated user session. BasicPay is interoperating with the testing anchor located at `testanchor.stellar.org` and you can view its toml file [here](https://testanchor.stellar.org/.well-known/stellar.toml). ```js title=/src/lib/stellar/sep1.js // Fetches and returns the stellar.toml file hosted by a provided domain. export async function fetchStellarToml(domain) { let stellarToml = await StellarToml.Resolver.resolve(domain); return stellarToml; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep1.js Strictly speaking, `StellarToml.Resolver.resolve()` from the JavaScript SDK is the only call we _need_ to retrieve and use the information provided by the anchor (heck, we could even just write our own `fetch`-based function, and bypass the SDK altogether). However, we've created quite a few "helper" functions to make the rest of our queries a bit more verbose and clear as to what we're looking for from the anchor server. Make sure to check out the `sep1.js` source file linked above! Using the `stellar.toml` information for an asset with a `home_domain`, we can display to the user some options (depending on the available infrastructure). We'll start with SEP-10 authentication. --- ## SEP-10: Stellar Web Authentication Similar to the SEP-1 information, both SEP-6 and SEP-24 protocols make use of SEP-10 for authentication with the user. The user must prove they own the account before they can withdraw or deposit any assets as part of SEP-10: Stellar Web Authentication. Since we have the `stellar.toml` file information already, we can use that to display some interactive elements to the user. ## Prompt for authentication :::note The `/src/routes/dashboard/transfers/+page.svelte` is doing **a lot** of work throughout these sections, and we are chopping it up in various ways for display as part of this tutorial. For a full picture of this file, please remember to check the source code. ::: ```html title=/src/routes/dashboard/transfers/+page.svelte ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/+page.svelte ## Requesting a challenge transaction Now, when the user clicks the "authenticate" button, it triggers the `auth` function. ![authenticate](/assets/basic-pay/authenticate.png) ```html title=/src/routes/dashboard/transfers/+page.svelte ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/+page.svelte As part of the `auth` function, BasicPay makes a `GET` request with an `account` param (the public key of the user) to the anchor, which sends back a Stellar transaction signed by the server's signing key (called a challenge transaction) with an invalid sequence number so it couldn't actually do anything if it were accidentally submitted to the network. ```js title=/src/lib/stellar/sep10.js // Requests, validates, and returns a SEP-10 challenge transaction from an anchor server. export async function getChallengeTransaction({ publicKey, homeDomain }) { let { WEB_AUTH_ENDPOINT, TRANSFER_SERVER, SIGNING_KEY } = await fetchStellarToml(homeDomain); // In order for the SEP-10 flow to work, we must have at least a server // signing key, and a web auth endpoint (which can be the transfer server as // a fallback) if (!(WEB_AUTH_ENDPOINT || TRANSFER_SERVER) || !SIGNING_KEY) { throw error(500, { message: "could not get challenge transaction (server missing toml entry or entries)", }); } // Request a challenge transaction for the users's account let res = await fetch( `${WEB_AUTH_ENDPOINT || TRANSFER_SERVER}?${new URLSearchParams({ // Possible parameters are `account`, `memo`, `home_domain`, and // `client_domain`. For our purposes, we only supply `account`. account: publicKey, })}`, ); let json = await res.json(); // Validate the challenge transaction meets all the requirements for SEP-10 validateChallengeTransaction({ transactionXDR: json.transaction, serverSigningKey: SIGNING_KEY, network: json.network_passphrase, clientPublicKey: publicKey, homeDomain: homeDomain, }); return json; } // Validates the correct structure and information in a SEP-10 challenge transaction. function validateChallengeTransaction({ transactionXDR, serverSigningKey, network, clientPublicKey, homeDomain, clientDomain, }) { if (!clientDomain) { clientDomain = homeDomain; } try { // Use the `readChallengeTx` function from Stellar SDK to read and // verify most of the challenge transaction information let results = WebAuth.readChallengeTx( transactionXDR, serverSigningKey, network, homeDomain, clientDomain, ); // Also make sure the transaction was created for the correct user if (results.clientAccountID === clientPublicKey) { return; } else { throw error(400, { message: "clientAccountID does not match challenge transaction", }); } } catch (err) { throw error(400, { message: JSON.stringify(err) }); } } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep10.js ## Sign and submit the challenge transaction In response, the user signs the transaction. You may have noticed we present this challenge transaction to the user with our regular confirmation modal. Once they've signed the transaction, the application sends it back to the anchor with a `POST` request. If the signature checks out, the success response will contain a [JSON Web Token (JWT)](https://jwt.io), which BasicPay stores in the `webAuthStore` store to use for future interactions with the anchor. ```html title=/src/routes/dashboard/transfers/+page.svelte ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/+page.svelte The `submitChallengeTransaction` function is quite simple. We take the transaction (in XDR format) and the domain name, and submit it to the relevant `WEB_AUTH_ENDPOINT` provided by the home domain's `stellar.toml` file. ```js title=/src/lib/stellar/sep10.js // Submits a SEP-10 challenge transaction to an authentication server and returns the SEP-10 token. export async function submitChallengeTransaction({ transactionXDR, homeDomain, }) { let webAuthEndpoint = await getWebAuthEndpoint(homeDomain); if (!webAuthEndpoint) throw error(500, { message: "could not authenticate with server (missing toml entry)", }); let res = await fetch(webAuthEndpoint, { method: "POST", mode: "cors", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transaction: transactionXDR }), }); let json = await res.json(); if (!res.ok) { throw error(400, { message: json.error }); } return json.token; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep10.js ## About the `webAuthStore` store Like so much of our BasicPay application, the various authentication tokens the user may have accumulated over time are stored in the browser's `localStorage`. There's not much special about this particular store, but here's how we put it together: ```js title=/src/lib/stores/webAuthStore.js function createWebAuthStore() { const { subscribe, update } = persisted("bpa:webAuthStore", {}); return { subscribe, // Stores a JWT authentication token associated with a home domain server. setAuth: (homeDomain, token) => update((store) => { return { ...store, [homeDomain]: token, }; }), // Determine whether or not a JSON web token has an expiration date in the future or in the past. isTokenExpired: (homeDomain) => { let token = get(webAuthStore)[homeDomain]; if (token) { let payload = JSON.parse( Buffer.from(token.split(".")[1], "base64").toString(), ); let timestamp = Math.floor(Date.now() / 1000); return timestamp > payload.exp; } else { return undefined; } }, }; } export const webAuthStore = createWebAuthStore(); ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stores/webAuthStore.js Now that we have successfully authenticated our user with an asset anchor, we can display and process the various transfer capabilities of the anchor in question. We'll begin with SEP-6, since that will lay the groundwork for SEP-24 to follow. --- ## SEP-24: Hosted Deposit and Withdrawal SEP-24 provides a standard way for wallets and anchors to interact by having the user open a webview hosted by an anchor to collect and handle KYC information. In this integration, a user's KYC information is gathered and handled entirely by the anchor. For the most part, after the anchor's webview has opened, BasicPay will have little knowledge about what's going on. :::info Remember, SEP-24 depends on [SEP-10 authentication]. Everything below assumes the user has successfully authenticated with the anchor server, and BasicPay has access to an unexpired authentication token to send with its requests. ::: ## Find the anchor's `TRANSFER_SERVER_SEP0024` Before we can ask anything about _how_ to make a SEP-24 transfer, we have to figure out _where_ to discover that information. Fortunately, the SEP-1 protocol describes standardized fields to find out what we need. ```js title=/src/lib/stellar/sep1.js // Fetches and returns the endpoint used for SEP-24 transfer interactions. export async function getTransferServerSep24(domain) { let { TRANSFER_SERVER_SEP0024 } = await fetchStellarToml(domain); return TRANSFER_SERVER_SEP0024; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep1.js ## Get `/info` Our application will request the `/info` endpoint from the anchor's transfer server to understand the supported transfer methods (deposit, withdraw) and available endpoints, as well as additional features that may be available during transfers. ```js title=/src/lib/stellar/sep24.js // Fetches and returns basic information about what the SEP-24 transfer server supports. export async function getSep24Info(domain) { let transferServerSep24 = await getTransferServerSep24(domain); let res = await fetch(`${transferServerSep24}/info`); let json = await res.json(); if (!res.ok) { throw error(res.status, { message: json.error, }); } else { return json; } } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep24.js ## The user clicks "deposit" or "withdraw" Now that we have all the SEP-24 information the anchor has made available to us, it's up to the user to actually begin the initiation process. In BasicPay, they do that by simply clicking a button that will then trigger the `launchTransferWindowSep24` function. :::note This file was pretty heavily covered in the [SEP-6 section]. We'll be presenting here the additions we make to this file, though we won't repeat things we've already covered. Remember to check the source files for the full picture. ::: ```html title=/src/routes/dashboard/transfers/+page.svelte ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/+page.svelte ## Retrieve the interactive URL BasicPay then initiates a transfer method by sending a `POST` request to either the “SEP-24 Deposit” or “SEP-24 Withdraw” endpoint. The anchor then sends an interactive URL that BasicPay will open as a popup for the user to complete and confirm the transfer. ```js title=/src/lib/stellar/sep24.js // Initiates a transfer using the SEP-24 protocol. export async function initiateTransfer24({ authToken, endpoint, homeDomain, urlFields = {}, }) { let transferServerSep24 = await getTransferServerSep24(homeDomain); let res = await fetch( `${transferServerSep24}/transactions/${endpoint}/interactive`, { method: "POST", mode: "cors", headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}`, }, body: JSON.stringify(urlFields), }, ); let json = await res.json(); if (!res.ok) { throw error(res.status, { message: json.error, }); } else { return json; } } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep24.js ## Launch the popup window and listen for a callback BasicPay doesn't really need (or want) to know everything that's happening between the user and the anchor during a SEP-24 transfer. However, we _do_ want to know when the interaction is over, since we may need to take some action at that point. So, we add a callback to the interactive URL and open the popup window. :::caution Since BasicPay is an entirely client-side application, we can't provide a callback as a URL. So, we are using a `postMessage` callback. For more information on the details of these callback options, check out [this section of the SEP-24 specification]. ::: ```html title=/src/routes/dashboard/transfers/+page.svelte ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/+page.svelte ## Complete transfer Once the user is finished with the interactive window from the anchor, they'll be brought back to BasicPay. We store the details of the transfer in the `transfersStore` store (remember, this is just so we can track which anchors to query for transfers later on). ```html title=/src/routes/dashboard/transfers/+page.svelte ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/+page.svelte ## (Sometimes) Send a Stellar payment In a withdrawal transaction, BasicPay will also build and present to the user a Stellar transaction for them to sign with their pincode. ```html title=/src/routes/dashboard/transfers/+page.svelte ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/+page.svelte [sep-10 authentication]: ./sep10.mdx [sep-6 section]: ./sep6.mdx [this section of the sep-24 specification]: https://stellar.org/protocol/sep-24#adding-parameters-to-the-url --- ## SEP-6: Deposit and Withdrawal API SEP-6 allows wallets and other clients to interact with anchors directly without the user needing to leave the wallet to go to the anchor’s site. In this integration, a user’s KYC information is gathered and handled by the wallet and submitted to the anchor _on behalf of_ the user. ## Find the anchor's `TRANSFER_SERVER` Before we can ask anything about _how_ to make a SEP-6 transfer, we have to figure out _where_ to discover that information. Fortunately, the SEP-1 protocol describes standardized fields to find out what we need. ```js title=/src/lib/stellar/sep1.js // Fetches and returns the endpoint used for SEP-6 transfer interactions. export async function getTransferServerSep6(domain) { let { TRANSFER_SERVER } = await fetchStellarToml(domain); return TRANSFER_SERVER; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep1.js ## Get `/info` Now that we know where the transfer server is located, BasicPay needs to fetch the `/info` endpoint from the anchor's transfer server to understand the supported transfer methods ([deposit, withdraw, deposit-exchange, and withdraw-exchange](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md#info)) and available endpoints, as well as additional features that may be available during transfers. :::note At this time, BasicPay only supports the `deposit` and `withdraw` transfer methods. A future version of this tutorial will incorporate the `*-exchange` transfer methods. ::: ```js title=/src/lib/stellar/sep6.js // Fetches and returns basic information about what the SEP-6 transfer server suppports. export async function getSep6Info(domain) { let transferServer = await getTransferServerSep6(domain); let res = await fetch(`${transferServer}/info`); let json = await res.json(); return json; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep6.js ## Display interactive elements Since many of the SEP-6 (and SEP-24) endpoints require authentication, we wait until our user is [authenticated with SEP-10](./sep10.mdx) before we display what kinds of transfers are available. When they have a valid authentication token, we can display some buttons the user can use to begin a transfer. The user can then initiate one of the transfer methods (in BasicPay, only deposits and withdraws are supported) by clicking the “Deposit” or “Withdraw” button underneath a supported asset. ![sep6](/assets/basic-pay/sep6_deposit_withdraw.png) ```html title=/src/routes/dashboard/transfers/+page.svelte ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/+page.svelte ## A special SEP-6 modal component If you recall back to our [confirmation modal section](../confirmation-modal.mdx), we designed our modal component to be useful for anything we might require. Well, that's _almost_ true. The fact is that SEP-6 interactions are just plain complex. To facilitate that complexity, we've created a purpose-built SEP-6 transfer modal. There is **so much** to it, that we couldn't possibly cover everything it does here. However, we'll cover the main bits and link to the relevant source files. The modal component itself has been broken into several smaller Svelte components. Check out this source file to start looking through how we've put it together: https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/components/TransferModalSep6.svelte ### Launching the SEP-6 modal The above buttons will use the `launchTransferModalSep6` function to display the modal to the user. Here's how it's defined in that same file. ```html title=/src/routes/dashboard/transfers/+page.svelte ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/+page.svelte Once launched, the `TransferModalSep6` will walk the user through a "wizard" to gather all the required information and ultimately create the transfer. ### Modal step 1: Transfer details BasicPay prompts the user to input additional information such as transfer type, destination, and amount. Some of this is prepopulated based on which button the user clicked. However, the user can change any of the fields if they so choose. We'll spare the code sample in this section, since it's mostly Svelte things going on. You can view the source here: https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/components/TransferDetails.svelte ### Modal step 2: Gather KYC information To find out what infrastructure the anchor has made available for us to use, we need to query the anchor's SEP-1 `stellar.toml` file for the `KYC_SERVER` field. If this is not defined, BasicPay will fallback to using the `TRANSFER_SERVER` for these requests. ```js title=/src/lib/stellar/sep1.js // Fetches and returns the endpoint used for SEP-12 KYC interactions. export async function getKycServer(domain) { let { KYC_SERVER, TRANSFER_SERVER } = await fetchStellarToml(domain); // If `KYC_SERVER` is undefined in the domain's TOML file, `TRANSFER_SERVER` // will be used return KYC_SERVER ?? TRANSFER_SERVER; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep1.js Our SEP-6 modal then queries the anchor’s SEP-12 endpoint for the required KYC fields with a `GET` request, and we present these fields for the user to complete. ```js title=/src/lib/stellar/sep12.js // Sends a `GET` request to query KYC status for a customer, returns current status of KYC submission export async function getSep12Fields({ authToken, homeDomain }) { let kycServer = await getKycServer(homeDomain); let res = await fetch(`${kycServer}/customer`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}`, }, }); let json = await res.json(); return json; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep12.js Again, the presentation of the fields the user must complete is more on the Svelte side of things, so we won't share those details here. However, the source for this component is available here: https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/components/KYCInformation.svelte ### Modal step 3: Put KYC fields and report status Now that the user has provided the necessary information for the KYC requirements of the anchor, we can submit them to the anchor's KYC server with a `PUT` request. ```js title=/src/lib/stellar/sep12.js // Sends a `PUT` request to the KYC server, submitting the supplied fields for the customer's record. export async function putSep12Fields({ authToken, fields, homeDomain }) { let kycServer = await getKycServer(homeDomain); let res = await fetch(`${kycServer}/customer`, { method: "PUT", mode: "cors", headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}`, }, body: JSON.stringify(fields), }); let json = await res.json(); return json; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep12.js BasicPay receives back from the anchor a status message for the user to see. Once the status message is `ACCEPTED`, we can finally submit the actual transfer request! This component of the SEP-6 modal, like most of them, is almost entirely Svelte-related. So as to keep this tutorial (somewhat) uncluttered, we'll refer you to the source for the component, which you can find here: https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/components/KYCStatus.svelte ### Modal step 4: Submit transfer BasicPay makes this request by taking all the fields that have been collected during this process and wrapping them into a URL that contains query parameters: [example](https://testanchor.stellar.org/sep6/deposit?account=GAXQIC2BSZ5HZP3BD6RZQSD3LB66TB6A2TA5W3LZX2VDFMBAKHC4B62J&asset_code=SRT&type=bank_account&amount=11) We submit a `GET` request to the URL with our authorization token in the headers, and the anchor takes it from there! ```js title=/src/lib/stellar/sep6.js // Initiates a transfer using the SEP-6 protocol. export async function initiateTransfer6({ authToken, endpoint, formData, domain, }) { let transferServer = await getTransferServerSep6(domain); let searchParams = new URLSearchParams(formData); let res = await fetch(`${transferServer}/${endpoint}?${searchParams}`, { method: "GET", mode: "cors", headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}`, }, }); let json = await res.json(); if (!res.ok) { throw error(res.status, { message: json.error, }); } else { return json; } } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/sep12.js We then store the details of the transfer in the `transfersStore` store and display the transfer server's response to the user. We then wait for them to close the modal. :::note The only reason we're storing anything about the transfer in BasicPay is to help us keep track of which anchors the user has initiated transfers with. Otherwise, we wouldn't be able query for a transfer history (like on the `/dashboard` page). ::: ### (Sometimes) Modal step 5: Send a Stellar payment In a withdrawal transaction, BasicPay will also build and present to the user a Stellar transaction for them to sign with their pincode. Here we will finally get to move back to our "regular" modal that _is_ so good at so many things! ```html title=/src/routes/dashboard/transfers/+page.svelte ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/+page.svelte --- ## Setup for Anchored Assets An anchor is a Stellar-specific term for the on and off-ramps that connect the Stellar network to traditional financial rails, such as financial institutions or fintech companies. When a user deposits with an anchor, that anchor will credit their Stellar account with the equivalent amount of digital tokens. The user can then hold, transfer, or trade those tokens just like any other Stellar asset. When a user withdraws those tokens, the anchor redeems them for cash in hand or money in the bank. Read more about anchors in this [anchor section](../../../../learn/fundamentals/anchors.mdx). When a customer downloads a wallet application that is connected to an anchor service, their Stellar account can either be created by the wallet application or the anchor service. In this example, the account has been created by the wallet application, BasicPay. Account creation strategies are described more in-depth [here](../../application-design-considerations.mdx#account-creation-strategies). In this example, we’ll use an anchor on Stellar’s Testnet to simulate a bank transfer into and out of the user’s wallet using [SEP-6: Deposit and Withdrawal API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md) and/or [SEP-24: Hosted Deposit and Withdrawal](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md). :::info SEPs define community-decided standards for interoperability on Stellar. Read more in our [SEPs section](../../../../learn/fundamentals/stellar-ecosystem-proposals.mdx). ::: Our integrations will also use the following SEPs: - [SEP-1: Stellar TOML](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) - a file that provides a place for the Internet to find information about an organization’s Stellar integration - [SEP-9: Standard KYC Fields](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md) - defines a list of standard KYC fields for use in Stellar ecosystem protocols - [SEP-10: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) - defines the standard way for clients to create authenticated web sessions on behalf of a user who holds a Stellar account - [SEP-12: KYC API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) - defines a standard way for Stellar clients to upload KYC information to anchors ## Finding anchored assets BasicPay takes care of all the anchor transfer details on the `/dashboard/transfers` page. See it in action here: https://basicpay.pages.dev/dashboard/transfers We need our application to know how it can communicate with anchors to get asset and infrastructure information. The first thing we'll do is determine whether the user holds trustlines to any assets that have a `home_domain` field set on the Stellar network. The presence of that field on an issuer's account tells us the asset _may_ be plugged into the existing Stellar rails allowing for transfers of the asset. If it's present, we'll display some interactive elements to the user for that asset/domain. ```js title=/src/routes/dashboard/transfers/+page.js /** @type {import('./$types').PageLoad} */ export async function load({ parent }) { const { balances } = await parent(); return { /** @type {import('$lib/stellar/horizonQueries').HomeDomainBalanceLine[]} */ homeDomainBalances: await fetchAssetsWithHomeDomains(balances), }; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/transfers/+page.js Now we know if there are any assets the user holds that may have the necessary infrastructure for authenticated transfers and we can query the domain in question for the relevant details. --- ## Confirmation Modal Since the user's keypair is encrypted with a pincode and stored in their browser, we will occasionally need to prompt them for that pincode to sign a transaction or otherwise prove that they should be permitted to perform some action or view some data. ## User Experience The user should be informed about any actions that may take place, especially when funds are on the line. To ensure this, we will overtly request their confirmation via pincode before anything is done. The application has no way of knowing a user's pincode, so it can't decrypt their keypair without their confirmation. The modal window we've implemented facilitates this confirmation flow whenever we need it. ![confirmation modal](/assets/basic-pay/confirm-pincode.png) ## Code implementation Our modal function uses the `svelte-simple-modal` package to give us a versatile starting point. If you need to, install it now. ```bash npm2yarn npm install --save-dev svelte-simple-modal ``` ### Wrapping the rest of our app in the modal On the Svelte side, this modal component will be a "wrapper" around the rest of our application, which allows us to trigger the modal from anywhere we need, and it should behave similarly no matter what. ```html title="/src/routes/+layout.svelte" // highlight-next-line // highlight-next-line ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/+layout.svelte ### Creating a reusable modal Svelte component To avoid reinventing the wheel every time we need a modal, we will create a reusable component that can accomodate most of our needs. Then, when we need the confirmation modal, we can pass an object of props to customize the modal's behavior. :::note In our `*.svelte` component files, we will **not** dive into the HTML markup outside of the ` ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/components/ConfirmationModal.svelte ### Trigger the modal component at signup We can now use this modal component whenever we need to confirm something from the user. For example, here is how the modal is triggered when someone signs up. ```html title="/src/routes/signup/+page.svelte" ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/signup/+page.svelte ### Customizing confirmation and rejection behavior Now, as these components have been written so far, they don't actually _do_ anything when the user inputs their pincode or clicks on a button. Let's change that! Since the confirmation behavior must vary depending on the circumstances (for example, different actions for signup, transaction submission, etc.), we need a way to pass that as a prop when we open the modal window. First, in our modal component, we declare a dummy function to act as a prop, as well as an "internal" function that will call the prop function during the course of execution. ```html title="/src/lib/components/ConfirmationModal.svelte" ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/components/ConfirmationModal.svelte Now that our modal component is setup to make use of a prop function for confirmation and rejection, we can declare what those functions should do inside the page that spawns the modal. ```html title="/src/routes/signup/+page.svelte" ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/signup/+page.svelte As you can see, we didn't actually need a customized `onReject` function, so we didn't pass one. No harm, no foul! --- ## Contacts List One central feature of BasicPay is a list of contacts containing a user's name and associated Stellar addresses. ## User experience There are a few ways for a user to interact with the contact list. One way is that they can add a user and address on the `/dashboard/contacts` page (which also checks for a valid public key!). ![contact list](/assets/basic-pay/add-contact.png) See it in action here: https://basicpay.pages.dev/dashboard/contacts ## Code implementation We will create a Svelte `store` to keep track of a user's contact list. ### Creating the `contacts` store As with the rest of our user-data, the contacts list will live in the browser's `localStorage`. We are using the [`svelt-local-storage-store` package] to facilitate this. We create a Svelte `store` to hold the data, and add a few custom functions to manage the list: `empty`, `remove`, `add`, `favorite`, and `lookup`. :::note This tutorial code is simplified for display here. The code is fully typed, documented, and commented in the [source code repository]. ::: ```js title="/src/lib/stores/contactsStore.js" // We are wrapping this store in its own function which will allow us to write // and customize our own store functions to maintain consistent behavior // wherever the actions need to take place. function createContactsStore() { // Make a `persisted` store that will hold our entire contact list. const { subscribe, set, update } = persisted("bpa:contactList", []); return { subscribe, // Erases all contact entries from the list and creates a new, empty contact list. empty: () => set([]), // Removes the specified contact entry from the list. remove: (id) => update((list) => list.filter((contact) => contact.id !== id)), // Adds a new contact entry to the list with the provided details. add: (contact) => update((list) => { if (StrKey.isValidEd25519PublicKey(contact.address)) { return [...list, { ...contact, id: uuidv4() }]; } else { throw error(400, { message: "invalid public key" }); } }), // Toggles the "favorite" field on the specified contact. favorite: (id) => update((list) => { const i = list.findIndex((contact) => contact.id === id); if (i >= 0) { list[i].favorite = !list[i].favorite; } return list; }), // Searches the contact list for an entry with the specified address. lookup: (address) => { let list = get(contacts); let i = list.findIndex((contact) => contact.address === address); if (i >= 0) { return list[i].name; } else { return false; } }, }; } // We export `contacts` as the variable that can be used to interact with the contacts store. export const contacts = createContactsStore(); ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stores/contactsStore.js ### Using the `contacts` store #### On the `/dashboard/contacts` page We also have a page dedicated to managing contacts. The `/dashboard/contacts` page will allow the user to collect and manage a list of contact entries that stores the contact's name and Stellar address. The contact can also be flagged or unflagged as a "favorite" contact to be displayed on the main `/dashboard` page. ```html title="/src/routes/dashboard/contacts/+page.svelte" ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/contacts/+page.svelte #### On the `/dashboard` page The `contacts` store is now exported from this file and can be accessed and used inside a Svelte page or component. Here is how we've implemented a "favorite contacts" component for display on the main BasicPay dashboard. ```html title="/src/routes/dashboard/components/FavoriteContacts.svelte" ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/components/FavoriteContacts.svelte [`svelt-local-storage-store` package]: https://github.com/joshnuss/svelte-local-storage-store [source code repository]: https://github.com/stellar/basic-payment-app --- ## Manage Trust For an account to hold and trade assets other than XLM, it must establish a [trustline](../../../learn/fundamentals/stellar-data-structures/accounts.mdx#trustlines) with the issuing account of that particular asset. Each trustline increases the account’s [base reserve](../../../learn/fundamentals/stellar-data-structures/accounts.mdx#base-reserves-and-subentries) by 0.5 XLM, which means the account will have to hold more XLM in its minimum balance. ## User experience First, we’ll have the user create a trustline for an asset by navigating to the Assets page, selecting an asset, and clicking the “Add Asset” button. :::info An asset is displayed as an asset code and issuer address. Learn more in our [Assets section](../../../learn/fundamentals/stellar-data-structures/assets.mdx). ::: ![add-assets](/assets/basic-pay/add-assets.png) This triggers a modal form for the user to confirm the transaction with their pincode. Once confirmed, a transaction containing the `changeTrust` operation is signed and submitted to the network, and a trustline is established between the user's account and the issuing account for the asset. The `changeTrust` operation can also be used to modify or remove trustlines. :::info Every transaction must contain a sequence number that is used to identify and verify the order of transactions with the account. A transaction’s sequence number must always increase by one. In BasicPay, fetching and incrementing the sequence number is handled automatically by the transaction builder. ::: Trustlines hold the balances for all of their associated assets (except XLM, which are held at the account level), and you can display the user’s various balances in your application. ![display assets](/assets/basic-pay/display-assets.png) See it in action here: https://basicpay.pages.dev/dashboard/assets ## Code implementation The trustlines an account holds will be necessary to view in several parts of the BasicPay application. First, we'll discuss how we manage different trustlines for the account. ### The `/dashboard/assets` page The `/dashboard/assets` page allows the user to manage the Stellar assets their account carries trustlines to. On this page, they can select from several pre-suggested or highly ranked assets, or they could specify their own asset to trust using an asset code and issuer public key. They can also remove trustlines that already exist on their account. The layout of the page is quite similar to our contacts page. It has a table displaying the existing trustlines and a section where you can add new ones. The key difference is that the `contacts` store is held in the browser's `localStorage`, whereas an account's balances are held on the blockchain. So, we will be querying the network to get that information. For more information about how we query this information from the Stellar network, check out the `fetchAccountBalances()` function in [this querying data section]. ```html title="/src/routes/dashboard/assets/+page.svelte" ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/assets/+page.svelte ### The `createChangeTrustTransaction` function In the above page, we've made use of the `createChangeTrustTransaction` function. This function can be used to add, delete, or modify trustlines on a Stellar account. ```js title="/src/lib/stellar/transactions.js" TransactionBuilder, Networks, Operation, Asset, } from "@stellar/stellar-sdk"; // We are setting a very high maximum fee, which increases our transaction's // chance of being included in the ledger. We're making this a `const` so we can // change it on one place as and when recommendations and/or best practices // evolve. Current recommended fee is `100_000` stroops. const maxFeePerOperation = "100000"; const rpcUrl = "https://soroban-testnet.stellar.org"; const networkPassphrase = Networks.TESTNET; const standardTimebounds = 300; // 5 minutes for the user to review/sign/submit // Constructs and returns a Stellar transaction that will create or modify a // trustline on an account. export async function createChangeTrustTransaction({ source, asset, limit }) { // We start by converting the asset provided in string format into a Stellar // Asset() object let trustAsset = new Asset(asset.split(":")[0], asset.split(":")[1]); // Next, we setup our transaction by loading the source account from the // network using RPC, and initializing the TransactionBuilder. let server = new Server(rpcUrl); let sourceAccount = await server.getAccount(source); // Chaning everything together from the `transaction` declaration means we // don't have to assign anything to `builtTransaction` later on. Either // method will have the same results. let transaction = new TransactionBuilder(sourceAccount, { networkPassphrase: networkPassphrase, fee: maxFeePerOperation, }) // Add a single `changeTrust` operation (this controls whether we are // adding, removing, or modifying the account's trustline) .addOperation( Operation.changeTrust({ asset: trustAsset, limit: limit?.toString(), }), ) // Before the transaction can be signed, it requires timebounds .setTimeout(standardTimebounds) // It also must be "built" .build(); return { transaction: transaction.toXDR(), network_passphrase: networkPassphrase, }; } ``` [this querying data section]: ./querying-data.mdx#fetchaccountbalances --- ## Overview :::note This tutorial walks through how to build an application with the [`@stellar/stellar-sdk`]. To build with the Wallet SDK, please follow the [Build a Wallet tutorial](../wallet/overview.mdx). To build with smart contracts, navigate to the [Smart Contracts section](../../../build/smart-contracts/getting-started/setup.mdx). ::: In this tutorial, we'll walk through the steps needed to build a basic payment application on Stellar's Testnet. After this tutorial, you should have a good understanding of the fundamental Stellar concepts and a solid base for iterative development. For this tutorial, we'll walk through the steps as we build a sample application we've called [BasicPay], which will be used to showcase various features. :::caution Although BasicPay is a full-fledged application on Stellar's Testnet, it has been built solely to showcase Stellar functionality for the educational purposes of this tutorial, not to be copied, pasted, and used on Mainnet. ::: ## Project Setup ### Project Requirements To build a basic Stellar application, you'll need: - Application framework: we're using [SvelteKit] opting for JavaScript with JSDoc typing. SvelteKit is quite flexible and could be used for a lot, but we are mainly using it for its routing capabilities to minimize this being a "SvelteKit tutorial". - Frontend framework: we're using [DaisyUI] to simplify the use of [Tailwind CSS]. - A way to interact with the Stellar network: we're using the [`@stellar/stellar-sdk`], but you could use traditional fetch requests. The [`@stellar/stellar-sdk`] is also used for building transactions to submit to the Stellar network. - A way to interact with the user's keypair: we're using [`@stellar/typescript-wallet-sdk-km`], but you can opt to use an existing wallet. :::note While we are using the above components to construct our application, we have done our best to write this tutorial in such a way that dependency on any one of these things is minimized. Ideally, you should be able to use the JavaScript code we've written and plug it into any other framework you'd like with minimal effort. ::: We've made the following choices during the development of BasicPay that you may also need to consider as you follow along: - We've designed this app for desktop. For the most part, the app is responsive to various screen sizes, but we have _chosen_ not to go out of our way to prioritize the mobile user experience. - We have enabled the default DaisyUI "light" and "dark" themes, which should switch with the preferences of your device. There is no toggle switch enabled, though. - This is written as a client-side application. No server-side actions actually take place. If you are building an application with a backend and frontend, you will need to consider carefully which information lives where, especially when a user's secret key is involved. - We're deploying this as a static "single-page application" with [Cloudflare Pages]. Your own deployment decisions will have an impact on your configuration and build process. - The application is likely not as performant as it could be. Neither is it as optimized as it could be. We've tried to encapsulate the various functionalities in a way that makes sense to the developer reading the codebase, so there is some code duplication and things _could_ be done in a "better" way. - We do _some_ error handling, but not nearly as much as you would want for a real-world application. If something seems like it's not working, and you're not seeing an error, open your developer console, and you might be able to figure out what has gone wrong. - We have not implemented _any_ automated testing. You'll probably want some for your application. :::note This tutorial is probably best viewed as "_nearly_ comprehensive." We aren't going to walk you through each and every file in our codebase, and the files we do use to illustrate concepts in the tutorial may not be _entirely_ present or explained. However, we will cover the basics, and point you to more complete examples in the codebase when applicable. ::: ### Dev Helpers - [Stellar Lab]: an experimental playground to interact with the Stellar network - Friendbot: a bot that funds accounts with 10,000 fake XLM on Stellar's Testnet; you can fund your account by going to `https://friendbot.stellar.org/?addr=G...` - [Testnet toml file]: an example `stellar.toml` file that demonstrates what information an anchor might publish - [BasicPay dev helpers]: if you're _using_ the BasicPay application, we've created a few helpful tools to help you explore its functionality ## Getting Started Here are the steps we've taken to start building BasicPay. Feel free to be inspired and customize these directions as you see fit. The entire [BasicPay codebase] is freely open and available on GitHub for reference. :::note This part of the tutorial will need a large helping of "your mileage may vary." We will outline what steps we've taken for our deployment situation, but you will want to review what options are needed for your environment(s). ::: ### Install Frameworks The first thing we'll need to do is create a SvelteKit app, using `npm`, we are using v18.x of nodejs. ```bash npx sv create my-basic-payment-app ``` This will walk you through the SvelteKit creation process, asking you about the various options. We've chosen the following options: - **Which Svelte app template?** Skeleton project - **Are you type checking with TypeScript?** Yes, using JavaScript with JSDoc comments - **Select additional options** Add ESLint for code linting; Add Prettier for code formatting After this process, the scaffolding for your application will live inside the `my-basic-payment-app` directory. You can `cd` into that directory and add some UI dependencies. ```bash npm2yarn cd my-basic-payment-app npm install --save-dev svelte-preprocess tailwindcss@^3.4.13 autoprefixer postcss @sveltejs/adapter-static \ @tailwindcss/typography \ daisyui svelte-feather-icons ``` Before we configure anything, we'll need to generate our `tailwind.config.js` and `postcss.config.js` files. ```bash npx tailwindcss init -p ``` Now, we will require a bit of configuration to make all those components work together. First, modify your `svelte.config.js` file: ```js title="/svelte.config.js" // highlight-start // highlight-end /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { // Note: Your `adapter` configuration may need customizations depending // on how you are building and deploying your application. // highlight-start adapter: adapter({ fallback: "index.html", }), // highlight-end }, // highlight-start preprocess: [ preprocess({ postcss: true, }), ], // highlight-end }; export default config; ``` Next, you can configure the `tailwind.config.js` file. 1. Import the `daisyui` and `typography` plugins 2. Configure our content paths (you may need to modify these values depending on your project structure) 3. Add the `daisyui` plugin **after** any officialy `@tailwindcss` plugins (only `typography` in our example) ```js title="/tailwind.config.js" // highlight-start const daisyui = require("daisyui"); const typography = require("@tailwindcss/typography"); // highlight-end /** @type {import('tailwindcss').Config} */ export default { content: [ // highlight-start "./src/routes/**/*.{html,js,svelte,ts}", "./src/routes/**/**/*.{html,js,svelte,ts}", "./src/lib/components/**/*.{html,js,svelte,ts}", // highlight-end ], // highlight-next-line plugins: [typography, daisyui], }; ``` Add your tailwind directives to your app's main CSS file. ```css title="/src/app.postcss" @tailwind base; @tailwind components; @tailwind utilities; ``` Then import the CSS file into your base SvelteKit layout (you may need to create this file). ```html title="/src/routes/+layout.svelte" ``` We also created a `/src/routes/+layout.js` file to configure our application as _only_ client-side. This means the app will be delivered to the client as unrendered HTML and JavaScript. ```js title="/src/routes/+layout.js" // Disable pre-rendering of pages during build-time export const prerender = false; // Disable server-side rendering export const ssr = false; ``` Your SvelteKit project is now configured and ready to run! ```bash npm2yarn npm run dev ``` ### Stellar dependencies To work with the Stellar network, datastructures, and locally stored keypairs, we're going to install and configure a few more dependencies. ```bash npm2yarn # Stellar SDKs npm install @stellar/stellar-sdk @stellar/typescript-wallet-sdk-km # Wallet integration packages (required for vite.config.js SSR bundling) npm install @creit.tech/stellar-wallets-kit@^1 @stellar/freighter-api @lobstrco/signer-extension-api # We will need some polyfills to make things available client-side npm install --save-dev @esbuild-plugins/node-globals-polyfill @esbuild-plugins/node-modules-polyfill \ path @rollup/plugin-inject buffer svelte-local-storage-store uuid ``` :::info[Stellar Wallets Kit v1] The Stellar Wallets Kit dependency is pinned to `@^1` on purpose. This tutorial and its companion [`stellar/basic-payment-app`](https://github.com/stellar/basic-payment-app) are written against the Kit v1 API (`new StellarWalletsKit(...)`). Installing the package unpinned now resolves to v2.x, whose initialization API is incompatible. This pin keeps the tutorial working until it and BasicPay are migrated to v2 together. ::: We will use a `window.js` file to inject Buffer into our client-side code, since that's required by some parts of the `@stellar/stellar-sdk`. ```js title="/src/lib/window.js" if (browser) { window.Buffer = Buffer; } else { globalThis.Buffer = Buffer; globalThis.window = {}; } export default globalThis; ``` The actual "injection" takes place in our `vite.config.js` file. ```js title="/vite.config.js" export default defineConfig({ plugins: [sveltekit()], optimizeDeps: { esbuildOptions: { define: { global: "globalThis", }, plugins: [ NodeGlobalsPolyfillPlugin({ buffer: true, }), ], }, }, build: { rollupOptions: { plugins: [ inject({ window: path.resolve("src/lib/window.js"), }), ], }, }, ssr: { noExternal: [ "@creit.tech/stellar-wallets-kit", "@stellar/freighter-api", "@lobstrco/signer-extension-api", ], }, }); ``` That should take care of everything you need! If you've followed these steps, you now have running client-side-only application that's ready to build out an application that interacts with the Stellar network! Way to go! Next up, we'll look at how we register a user and create their account on the Stellar network. [basicpay]: https://basicpay.pages.dev [sveltekit]: https://kit.svelte.dev [daisyui]: https://daisyui.com [tailwind css]: https://tailwindcss.com [`@stellar/stellar-sdk`]: https://www.npmjs.com/package/@stellar/stellar-sdk [`@stellar/typescript-wallet-sdk-km`]: https://www.npmjs.com/package/@stellar/typescript-wallet-sdk-km [cloudflare pages]: https://pages.cloudflare.com/ [stellar lab]: https://lab.stellar.org [testnet toml file]: https://testanchor.stellar.org/.well-known/stellar.toml [basicpay codebase]: https://github.com/stellar/basic-payment-app [basicpay dev helpers]: https://basicpay.pages.dev/dashboard/settings/dev --- ## Path Payment A path payment is where the asset sent can be different from the asset received. There are two possible path payment operations: 1) `path_payment_strict_send`, which allows the user to specify the amount of the asset to send, and 2) `path_payment_strict_receive`, which allows the user to specify the amount of the asset received. Read more in the [Path Payments Guide](../../../build/guides/transactions/path-payments.mdx). ## User experience With BasicPay, the user sends a path payment by navigating to the Payments page, where they can either select a user from their contacts or input the public key of a destination address. They then select the Send and Receive Different Assets toggle and determine whether they want to specify the asset sent or received. Finally they select the asset sent and the asset received and the amounts and select the Preview Transaction button. ![path payment](/assets/basic-pay/path-payment.png) The user will then preview the transaction, input their pincode, and select the Confirm button to sign and submit the transaction to the network. ## Code implementation ### The `/dashboard/send` page Most of this page has been discussed in the [Payment section](./payment.mdx#the-dashboardsend-page). Below, we're highlighting the unique pieces that are added to BasicPay to allow for the path payment feature. ```html title="/src/routes/dashboard/send/+page.svelte" ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/send/+page.svelte ### The transaction functions In the above section, we used the `createPathPaymentStrictReceiveTransaction` and `createPathPaymentStrictSendTransaction` functions. These are used to create transactions that contain the actual path payment operation. ```js title="/src/lib/stellar/transactions.js" // Constructs and returns a Stellar transaction that will contain a path payment strict send operation to send/receive different assets. export async function createPathPaymentStrictSendTransaction({ source, sourceAsset, sourceAmount, destination, destinationAsset, destinationAmount, memo, }) { // First, we setup our transaction by loading the source account from the // network, and initializing the TransactionBuilder. This is the first step // in constructing all Stellar transactions. let server = new Server(horizonUrl); let sourceAccount = await server.loadAccount(source); let transaction = new TransactionBuilder(sourceAccount, { networkPassphrase: networkPassphrase, fee: maxFeePerOperation, }); // We work out the assets to be sent by the source account and received by // the destination account let sendAsset = sourceAsset === "native" ? Asset.native() : new Asset(sourceAsset.split(":")[0], sourceAsset.split(":")[1]); let destAsset = destinationAsset === "native" ? Asset.native() : new Asset( destinationAsset.split(":")[0], destinationAsset.split(":")[1], ); // We will calculate an acceptable 2% slippage here for... reasons? let destMin = ((98 * parseFloat(destinationAmount)) / 100).toFixed(7); // If a memo was supplied, add it to the transaction if (memo) { transaction.addMemo(Memo.text(memo)); } // Add a single `pathPaymentStrictSend` operation transaction.addOperation( Operation.pathPaymentStrictSend({ sendAsset: sendAsset, sendAmount: sourceAmount.toString(), destination: destination, destAsset: destAsset, destMin: destMin, }), ); // Before the transaction can be signed, it requires timebounds, and it must // be "built" let builtTransaction = transaction.setTimeout(standardTimebounds).build(); return { transaction: builtTransaction.toXDR(), network_passphrase: networkPassphrase, }; } // Constructs and returns a Stellar transaction that will contain a path payment strict receive operation to send/receive different assets. export async function createPathPaymentStrictReceiveTransaction({ source, sourceAsset, sourceAmount, destination, destinationAsset, destinationAmount, memo, }) { // First, we setup our transaction by loading the source account from the // network, and initializing the TransactionBuilder. This is the first step // in constructing all Stellar transactions. let server = new Server(horizonUrl); let sourceAccount = await server.loadAccount(source); let transaction = new TransactionBuilder(sourceAccount, { networkPassphrase: networkPassphrase, fee: maxFeePerOperation, }); // We work out the assets to be sent by the source account and received by // the destination account let sendAsset = sourceAsset === "native" ? Asset.native() : new Asset(sourceAsset.split(":")[0], sourceAsset.split(":")[1]); let destAsset = destinationAsset === "native" ? Asset.native() : new Asset( destinationAsset.split(":")[0], destinationAsset.split(":")[1], ); /** @todo Figure out a good number to use for slippage. And why! And how to calculate it?? */ // We will calculate an acceptable 2% slippage here for... reasons? let sendMax = ((100 * parseFloat(sourceAmount)) / 98).toFixed(7); // If a memo was supplied, add it to the transaction if (memo) { transaction.addMemo(Memo.text(memo)); } // Add a single `pathPaymentStrictSend` operation transaction.addOperation( Operation.pathPaymentStrictReceive({ sendAsset: sendAsset, sendMax: sendMax, destination: destination, destAsset: destAsset, destAmount: destinationAmount, }), ); // Before the transaction can be signed, it requires timebounds, and it must // be "built" let builtTransaction = transaction.setTimeout(standardTimebounds).build(); return { transaction: builtTransaction.toXDR(), network_passphrase: networkPassphrase, }; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/transactions.js --- ## Payment A payment operation sends an amount in a specific asset (XLM or non-XLM) to a destination account. With a basic payment operation, the asset sent is the same as the asset received. BasicPay also allows for path payments (where the asset sent is different than the asset received), which we’ll talk about in the next section. ## User experience In our BasicPay application, the user will navigate to the Payments page where can either select a user from their contacts or input the public key of a destination address with a specified asset they’d like to send along with the amount of the asset. ![payment](/assets/basic-pay/payment.png) The user clicks the "Confirm Transaction" button. If the destination account exists and is properly funded with XLM, this will trigger a Transaction Preview where they can view the transaction details. All Stellar transactions require a small fee to make it to the ledger. Read more in our [Fees section](../../../learn/fundamentals/fees-resource-limits-metering.mdx). In BasicPay, we’ve set it up so that the user always pays a static fee of 100,000 [stroops](../../../learn/fundamentals/stellar-data-structures/assets.mdx#amount-precision) (one stroop equals 0.0000001 XLM) per operation. Alternatively, you can add a feature to your application that allows the user to set their own fee. ![payment](/assets/basic-pay/fees.png) The user then inputs their pincode and clicks the "Confirm" button, which signs and submits the transaction to the ledger. ## Code implementation ### The `/dashboard/send` page The `/dashboard/send` page allows the user to send payments to other Stellar addresses. They can select from a dropdown containing their contact list names, or they can enter their own "Other..." public key. The following additional features have been implemented: - If the destination address is not a funded account, the user is informed they will be using a `createAccount` operation, and must send at least 1 XLM to fund the account. - The user can select to send/receive different assets and paths are queried from Horizon depending on the four below points: 1. If they want to strict send or strict receive, 2. The source/destination assets they have selected, 3. The source/destination accounts, and 4. The amount entered for the send/receive value. - An optional memo field is available for text-only memos. For now, we'll focus on regular payments, and we'll dig into the path payments in a [later section](./path-payment.mdx). ```html title="/src/routes/dashboard/send/+page.svelte" ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/routes/dashboard/send/+page.svelte ### The transaction functions In the above section, we used the `createPaymentTransaction` function. This function can be used to send a payment of any asset from one Stellar address to another. We also used the `createCreateAccountTransaction` function. This is used when the destination account is not currently funded and active on the Stellar network. The only asset possible in this scenario is native XLM. ```js title="/src/lib/stellar/transactions.js" TransactionBuilder, Networks, Operation, Asset, Memo, } from "@stellar/stellar-sdk"; // We are setting a very high maximum fee, which increases our transaction's // chance of being included in the ledger. We're making this a `const` so we can // change it on one place as and when recommendations and/or best practices // evolve. Current recommended fee is `100_000` stroops. const maxFeePerOperation = "100000"; const rpcUrl = "https://soroban-testnet.stellar.org"; const networkPassphrase = Networks.TESTNET; const standardTimebounds = 300; // 5 minutes for the user to review/sign/submit // Constructs and returns a Stellar transaction that contains a `payment` operaion and an optional memo. export async function createPaymentTransaction({ source, destination, asset, amount, memo, }) { // First, we setup our transaction by loading the source account from the // network, and initializing the TransactionBuilder. This is the first step // in constructing all Stellar transactions. let server = new Server(rpcUrl); let sourceAccount = await server.getAccount(source); let transaction = new TransactionBuilder(sourceAccount, { networkPassphrase: networkPassphrase, fee: maxFeePerOperation, }); let sendAsset; if (asset && asset !== "native") { sendAsset = new Asset(asset.split(":")[0], asset.split(":")[1]); } else { sendAsset = Asset.native(); } // If a memo was supplied, add it to the transaction. Here, we have the // option of a hash memo because this is common practice by anchor transfers if (memo) { if (typeof memo === "string") { transaction.addMemo(Memo.text(memo)); } else if (typeof memo === "object") { transaction.addMemo(Memo.hash(memo.toString("hex"))); } } // Add a single `payment` operation transaction.addOperation( Operation.payment({ destination: destination, amount: amount.toString(), asset: sendAsset, }), ); // Before the transaction can be signed, it requires timebounds, and it must // be "built" let builtTransaction = transaction.setTimeout(standardTimebounds).build(); return { transaction: builtTransaction.toXDR(), network_passphrase: networkPassphrase, }; } // Constructs and returns a Stellar transaction that contains a `createAccount` operation and an optional memo. export async function createCreateAccountTransaction({ source, destination, amount, memo, }) { // The minimum account balance on the Stellar network is 1 XLM (2 base // reserves). We'll check that `amount` meets or exceeds that requirement // early, so we can fail quickly. if (parseFloat(amount.toString()) < 1) { throw error(400, { message: "insufficient starting balance" }); } // First, we setup our transaction by loading the source account from the // network, and initializing the TransactionBuilder. This is the first step // in constructing all Stellar transactions. let server = new Server(rpcUrl); let sourceAccount = await server.getAccount(source); let transaction = new TransactionBuilder(sourceAccount, { networkPassphrase: networkPassphrase, fee: maxFeePerOperation, }); // If a memo was supplied, add it to the transaction if (memo) { transaction.addMemo(Memo.text(memo)); } // Add a single `createAccount` operation transaction.addOperation( Operation.createAccount({ destination: destination, startingBalance: amount.toString(), }), ); // Before the transaction can be signed, it requires timebounds, and it must // be "built" let builtTransaction = transaction.setTimeout(standardTimebounds).build(); return { transaction: builtTransaction.toXDR(), network_passphrase: networkPassphrase, }; } ``` --- ## Querying Data Your application will be querying data from Horizon (one of Stellar's APIs) throughout its functionality. Information such as account balances, transaction history, sequence numbers for transactions, asset availability, and more are stored in Horizon’s database. Here is a list of some common queries that you'll make. :::note In other places in this tutorial, we have omitted the JSDoc descriptions and typing for the sake of clean presentation. Here, we're including those to make these functions more copy/paste-able. ::: ## Imports and types ```js title="/src/lib/stellar/horizonQueries.js" Horizon, TransactionBuilder, Networks, StrKey, Asset, } from "@stellar/stellar-sdk"; const horizonUrl = "https://horizon-testnet.stellar.org"; const server = new Horizon.Server(horizonUrl); /** * @module $lib/stellar/horizonQueries * @description A collection of function that helps query various information * from the [Horizon API](https://developers.stellar.org/docs/data/apis/horizon). This * allows us to abstract and simplify some interactions so we don't have to have * _everything_ contained within our `*.svelte` files. */ // We'll import some type definitions that already exist within the // `@stellar/stellar-sdk` package, so our functions will know what to expect. /** @typedef {import('@stellar/stellar-sdk').ServerApi.AccountRecord} AccountRecord */ /** @typedef {import('@stellar/stellar-sdk').Horizon.ErrorResponseData} ErrorResponseData */ /** @typedef {import('@stellar/stellar-sdk').ServerApi.PaymentOperationRecord} PaymentOperationRecord */ /** @typedef {import('@stellar/stellar-sdk').Horizon.BalanceLine} BalanceLine */ /** @typedef {import('@stellar/stellar-sdk').Horizon.BalanceLineAsset} BalanceLineAsset */ /** @typedef {import('@stellar/stellar-sdk').Transaction} Transaction */ /** @typedef {import('@stellar/stellar-sdk').ServerApi.PaymentPathRecord} PaymentPathRecord */ ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/horizonQueries.js ## fetchAccount Gives an account's sequence number, asset balances, and trustlines. ```js title="/src/lib/stellar/horizonQueries.js" /** * Fetches and returns details about an account on the Stellar network. * @async * @function fetchAccount * @param {string} publicKey Public Stellar address to query information about * @returns {Promise} Object containing whether or not the account is funded, and (if it is) account details * @throws {error} Will throw an error if the account is not funded on the Stellar network, or if an invalid public key was provided. */ export async function fetchAccount(publicKey) { if (StrKey.isValidEd25519PublicKey(publicKey)) { try { let account = await server.accounts().accountId(publicKey).call(); return account; } catch (err) { // @ts-ignore if (err.response?.status === 404) { throw error(404, "account not funded on network"); } else { // @ts-ignore throw error(err.response?.status ?? 400, { // @ts-ignore message: `${err.response?.title} - ${err.response?.detail}`, }); } } } else { throw error(400, { message: "invalid public key" }); } } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/horizonQueries.js ## fetchAccountBalances Gets existing balances for a given `publicKey`. ```js title="/src/lib/stellar/horizonQueries.js" /** * Fetches and returns balance details for an account on the Stellar network. * @async * @function fetchAccountBalances * @param {string} publicKey Public Stellar address holding balances to query * @returns {Promise} Array containing balance information for each asset the account holds */ export async function fetchAccountBalances(publicKey) { const { balances } = await fetchAccount(publicKey); return balances; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/horizonQueries.js ## fetchRecentPayments Finds any payments made to or from the given `publicKey` (includes: payments, path payments, and account merges). ```js title="/src/lib/stellar/horizonQueries.js" /** * Fetches and returns recent `payment`, `createAccount` operations that had an effect on this account. * @async * @function fetchRecentPayments * @param {string} publicKey Public Stellar address to query recent payment operations to/from * @param {number} [limit] Number of operations to request from the server * @returns {Promise} Array containing details for each recent payment */ export async function fetchRecentPayments(publicKey, limit = 10) { const { records } = await server .payments() .forAccount(publicKey) .limit(limit) .order("desc") .call(); return records; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/horizonQueries.js ## submit Submit a signed transaction to the Stellar network. ```js title="/src/lib/stellar/horizonQueries.js" /** * Submits a Stellar transaction to the network for inclusion in the ledger. * @async * @function submit * @param {Transaction} transaction Built transaction to submit to the network * @throws Will throw an error if the transaction is not submitted successfully. */ export async function submit(transaction) { try { await server.submitTransaction(transaction); } catch (err) { throw error(400, { // @ts-ignore message: `${err.response?.title} - ${err.response?.data.extras.result_codes}`, }); } } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/horizonQueries.js ## fetchAssetsWithHomeDomains We create a brand new `HomeDomainBalanceLine` type that includes the balance information of a user's trustline, and also adds the `home_domain` of the asset issuer. If you're using something else for type safety (or nothing at all), feel free to adapt or ignore the `@typedef`s we've included here. Then, it looks at all the issuer accounts and returns only the ones with a `home_domain` set on the account. ```js title="/src/lib/stellar/horizonQueries.js" /** * @typedef {Object} HomeDomainObject * @property {string} home_domain Domain name the issuer of this asset has set for their account on the Stellar network. */ /** @typedef {BalanceLineAsset & HomeDomainObject} HomeDomainBalanceLine */ /** * Fetches `home_domain` from asset issuer accounts on the Stellar network and returns an array of balances. * @async * @function fetchAssetsWithHomeDomains * @param {BalanceLine[]} balances Array of balances to query issuer accounts of * @returns {Promise} Array of balance details for assets that do have a `home_domain` setting */ export async function fetchAssetsWithHomeDomains(balances) { let homeDomains = await Promise.all( balances.map(async (asset) => { // We are only interested in issued assets (i.e., not LPs and not XLM) if ("asset_issuer" in asset) { // Fetch the account from the network, and add its info to the array, along with the home_domain let account = await fetchAccount(asset.asset_issuer); if ("home_domain" in account) { return { ...asset, home_domain: account.home_domain, }; } } }), ); // Filter out any null array entries before returning // @ts-ignore return homeDomains.filter((balance) => balance); } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/horizonQueries.js ## findStrictSendPaths Find the available strict send paths between a source asset/amount and receiving account. ```js title="/src/lib/stellar/horizonQueries.js" /** * Fetches available paths on the Stellar network between the destination account, and the asset sent by the source account. * @async * @function findStrictSendPaths * @param {Object} opts Options object * @param {string} opts.sourceAsset Stellar asset which will be sent from the source account * @param {string|number} opts.sourceAmount Amount of the Stellar asset that should be debited from the srouce account * @param {string} opts.destinationPublicKey Public Stellar address that will receive the destination asset * @returns {Promise} Array of payment paths that can be selected for the transaction * @throws Will throw an error if there are no available payment paths. */ export async function findStrictSendPaths({ sourceAsset, sourceAmount, destinationPublicKey, }) { let asset = sourceAsset === "native" ? Asset.native() : new Asset(sourceAsset.split(":")[0], sourceAsset.split(":")[1]); let response = await server .strictSendPaths(asset, sourceAmount.toString(), destinationPublicKey) .call(); if (response.records.length > 0) { return response.records; } else { throw error(400, { message: "no strict send paths available" }); } } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/horizonQueries.js ## findStrictReceivePaths Find the available strict receive paths between a source account and receiving asset/amount. ```js title="/src/lib/stellar/horizonQueries.js" /** * Fetches available paths on the Stellar network between the source account, and the asset to be received by the destination. * @async * @function findStrictReceivePaths * @param {Object} opts Options object * @param {string} opts.sourcePublicKey Public Stellar address that will be the source of the payment operation * @param {string} opts.destinationAsset Stellar asset which should be received in the destination account * @param {string|number} opts.destinationAmount Amount of the Stellar asset that should be credited to the destination account * @returns {Promise} Array of payment paths that can be selected for the transaction * @throws Will throw an error if there are no available payment paths. */ export async function findStrictReceivePaths({ sourcePublicKey, destinationAsset, destinationAmount, }) { let asset = destinationAsset === "native" ? Asset.native() : new Asset( destinationAsset.split(":")[0], destinationAsset.split(":")[1], ); let response = await server .strictReceivePaths(sourcePublicKey, asset, destinationAmount.toString()) .call(); if (response.records.length > 0) { return response.records; } else { throw error(400, { message: "no strict receive paths available" }); } } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/stellar/horizonQueries.js ## Query Stellar Expert Stellar Expert is a third-party block explorer that is indispensable as a tool for understanding what is happening on the Stellar network. On our `/dashboard/assets` page, we're pre-populating a list of asset trustlines a user might choose to add to their Stellar account. We get this list of assets from the Stellar Expert API. :::note Stellar Expert employs their own algorithm to determine the quality of an asset. This algorithm considers things like the number of payments made using an asset, the number of accounts holding that asset, how much of the asset is available on the DEX or in liquidity pools, and so on. These rankings aren't a "final determination" of an asset's quality by any means, and are more like an observation of which Stellar assets see the most use and activity on the network. ::: We've created our own `RankedAsset` type so BasicPay knows how to interact with these objects. And we are then retrieving the ten most highly-rated assets from the Stellar Expert API. ```js title="/src/lib/utils/stellarExpert.js" const network = "testnet"; const baseUrl = `https://api.stellar.expert/explorer/${network}`; /** * An asset object that has been returned by our query to Stellar.Expert * @typedef {Object} RankedAsset * @property {string} asset Asset identifier * @property {number} traded_amount Total traded amount (in stroops) * @property {number} payments_amount Total payments amount (in stroops) * @property {number} created Timestamp of the first recorder operation with asset * @property {number} supply Total issued asset supply * @property {Object} trustlines Trustlines established to an asset * @property {number} trades Total number of trades * @property {number} payments Total number of payments * @property {string} domain Associated `home_domain` * @property {Object} tomlInfo Asset information from stellar.toml file * @property {Object} rating Composite asset rating * @property {number} paging_token Paging token * @see {@link https://stellar.expert/openapi.html#tag/Asset-Info-API/operation/getAllAssets} */ /** * Fetches and returns the most highly rated assets, according to the Stellar.Expert calculations. * @async * @function fetchAssets * @returns {Promise} Array of objects containing details for each asset */ export async function fetchAssets() { let res = await fetch( `${baseUrl}/asset?${new URLSearchParams({ // these are all the defaults, but you could customize them if needed search: "", sort: "rating", order: "desc", limit: "10", cursor: "0", })}`, ); let json = await res.json(); let records = json._embedded.records; return records; } ``` **Source:** https://github.com/stellar/basic-payment-app/blob/main/src/lib/utils/stellarExpert.js --- ## Build a Passkey Powered Guestbook Dapp This section walks you through designing and building a decentralized application (dapp) that interacts with a smart contract guestbook, allowing users to read and write public messages. This tutorial also implements a passkey-powered smart wallet for user authentication. --- ## Generate Bindings Let's turn our attention to how we'll interact with the deployed smart contract. This is where the TypeScript bindings come in! But, I hear you ask: What are TypeScript bindings? These bindings are a feature of the Stellar CLI that will generate and produce a fully typed NPM package ready for integration into your frontend. This means you can import and invoke a smart contract as if it were any other nodejs package! You get typed functions for each of your contract's functions, and those will result in a built, simulated, signable, and submittable assembled transaction! :::tip This can be done with ANY contract that's live on the network! Or, you can use it on contracts you've only compiled locally, too. ::: We'll be generating our contract bindings, and keeping them in the same repository as our frontend code. However, you could do this in a lot of different ways: - Your frontend can instantiate a `contract.Client` instance using the [`fromWasmHash` function](https://stellar.github.io/js-stellar-sdk/module-contract.Client.html#.fromWasmHash) of the JavaScript SDK. This can generate bindings on-the-fly as your users browse your application. - Your deploy process might include a step that builds/deploys/binds a contract package at deploy-time. - You could even generate and publish a bindings package all by itself. Then `pnpm install ` can be done in any dapp that you (or somebody else) might need to interact with that contract. ### The manual method Before you skip ahead! Take a look at this (brief) section. It's _really_ useful to have a full understanding of what steps we're going through in the automated section. This will help you adapt and/or troubleshoot this tutorial for your specific purposes. #### Install the compiled contract The smart contract code needs to be installed to the network first. This uploads the compiled, binary Wasm file to the blockchain to be instantiated into a contract later on. From inside your project directory: ```shell stellar contract upload \ --source-account \ --network testnet \ --wasm ./target/wasm32v1-none/release/ye_olde_guestbook.wasm ``` #### Deploy a contract instance This will return a hexadecimal hash corresponding to the uploaded Wasm executable. This hash can then be used in the deploy command to create a new contract instance: ```shell stellar contract deploy \ --source-account \ --network testnet \ --wasm-hash ``` #### Generate bindings for the deployed contract Now we can (again) use the Stellar CLI to generate bindings from the contract we've just deployed. You can also generate these bindings from your local Wasm file using the `--wasm-hash` parameter. The `--overwrite` parameter is used to tell the CLI that it should output the generated bindings package, even if it finds the directory is not empty (i.e., we're re-binding a contract because we've modified the code and redeployed it). ```shell stellar contract bindings typescript \ --network testnet \ --id \ --output-dir ./packages/ye_olde_guestbook \ --overwrite ``` We'll need to build the bindings package, since (in its initial state) the package is mostly TypeScript types and stubs for the various contract functions. ```shell cd packages/ye_olde_guestbook pnpm install pnpm run build cd ../.. ``` #### Import the bindings package as a project dependency With our bindings generated, we can add it to our frontend project. Run this from the root of your project: ```shell pnpm add file:./packages/ye_olde_guestbook ``` #### Import the bindings client into the SvelteKit project :::info We're straying just a _bit_ into the Svelte-ish side of things here. The main goal of this step is to get the contract client (which is the "bindings package" we've just generated) into our frontend in a way that makes it usable anywhere we need it. In SvelteKit, we put it into `src/lib/contracts` because that means we can easily access the client by importing from `$lib/contracts/ye_olde_guestbook` whenever and wherever we need it. ::: Now, we'll define the contract client in a way we can easily access it through the rest of our app. ```js title="src/lib/contracts/ye_olde_guestbook.ts" // instantiate and export the Client class from the bindings package export default new Client.Client({ ...Client.networks.testnet, // this includes the contract address and network passphrase rpcUrl: PUBLIC_STELLAR_RPC_URL, // this is required to invoke the contract through RPC calls }); ``` ### The automated way That was a lot of steps and a lot of work wasn't it!? The good news is that our starter template (remember that?) comes with an `initialize.js` script that will perform all of those actions for you! This script will go through all the following steps for you: - Create and fund a keypair in the CLI - Install and deploy **all contracts** in the `/contracts` directory - Generate bindings from the deployed contracts - Create a `$lib/contracts/.ts` file for easy import into your frontend code You can always customize this script to suit your needs. Check out the [source code here](https://github.com/ElliotFriend/soroban-template-sveltekit-passkeys/blob/main/initialize.js) (which has been documented with comments). Or, you can see the [officially maintained script](https://github.com/stellar/soroban-template-astro/blob/main/initialize.js) in the [`soroban-template-astro` repository](https://github.com/stellar/soroban-template-astro), as well. Run the initialization script like so: ```shell node initialize.js ``` :::info For a more comprehensive overview of the process of creating, customizing, and using initialization scripts like this, check out the [frontend template guide](../../guides/dapps/soroban-contract-init-template.mdx). ::: We've also added a command to the `package.json` scripts, so you can run this initialize script simply by running (from your project's root directory): ```shell pnpm run setup ``` Right, so we've now created a starter project, written a guestbook smart contract, and generated an NPM package that will help us interact with that contract on the network. Amazing! Next up, let's take a look at how our users will connect with and interact with our dapp. It's time for passkeys! (insert air horn noises)📢 --- ## Dapp Frontend Walkthrough So, we now have all the pieces in place, and we're ready to connect the dots. ## Account type things Since we've just gone through all the passkeys setup, let's begin there. We'll create the functions that will be used to create the user's smart wallet, login with their smart wallet, and the logout functionality. We'll also add a "profile menu" that can drop down when a user is logged in and give them options for viewing their smart wallet on a block explorer, sending one of those all-important donations to our guestbook, requesting more (Testnet) funds, etc. :::info We're using some pieces of [Svelte state](https://svelte.dev/docs/svelte/$state) to keep the value of the user's smart wallet contract address as well as the public key of their passkey. Your implementation of keeping this state may differ depending on your chosen frontend, state management, and project design. Hopefully, in any situation, you can draw inspiration from the way we've done it for this tutorial. ::: ### Connect Buttons Setup We have a component in `$lib/components/connectButtons.svelte` that houses all the signup, login, and logout functionality. This gets put into the header component, and is available throughout the entirety of the dapp. The basic premise of this component is that we have a collection of buttons, as well as the corresponding functions that should take place when the button is clicked. The buttons themselves are simple enough: ```html title="src/lib/components/connectButtons.svelte" ``` If you look at the [source code](https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/src/lib/components/ConnectButtons.svelte) of this component, you will see that we do quite a bit more state-checking surrounding the display of the buttons. This makes it so a "login" button doesn't display when a user is already _logged in_, for example. For the purpose of this tutorial, though, we'll focus on the functions themselves, rather than the HTML of the buttons. Let's begin with the Signup function. #### User signup In order to signup our user, we'll make use of the `account` instance of the `PasskeyKit` class from our `$lib/passkeyClient.ts` file. The `account` instance has a function called `createWallet` that will do most of the heavy lifting for us, we only need to make sure we call the function properly. We do a little bit of error checking here, but not much. In practical applications, you would probably want to dive into the cause of any errors here, and ensure they are mitigated before telling a user to try again. ```js title="src/lib/components/connectButtons.svelte" async function signup() { console.log("signing up"); try { // The createWallet function takes two strings, an app name and a user name. // It returns the public key of the passkey, a contract address which will // be the user's wallet, and a built transaction (ready to submit) to create // the smart wallet on-chain. const { keyId_base64, contractId: cid, built, } = await account.createWallet("Ye Olde Guestbook", "User Name Goes Here"); // Store the key ID and contract address in our localStorage stores keyId.set(keyId_base64); contractId.set(cid); if (!built) { error(500, { message: "built transaction missing", }); } // Send the transaction, fund the smart wallet, refresh the balance await send(built); await fundContract($contractId); getBalance(); } catch (err) { console.log(err); toastStore.trigger({ message: "Something went wrong signing up. Please try again later.", background: "variant-filled-error", }); } } ``` #### User login Awesome! The user signs up and gets some (Testnet) lumens all in one go. Let's give them a way to login now with the passkey they've already associated with the smart wallet. ```js title="src/lib/components/connectButtons.svelte" async function login() { console.log("logging in"); try { // The connectWallet function requires us to pass a function that can // be used to reverse-lookup the smart wallet address, provided we know // the passkey's ID (the user supplies that during the function's execution) const { keyId_base64, contractId: cid } = await account.connectWallet({ getContractId, }); // Store the key ID and contract address in our localStorage stores keyId.set(keyId_base64); console.log($keyId); contractId.set(cid); console.log($contractId); } catch (err) { console.log(err); toastStore.trigger({ message: "Something went wrong logging in. Please try again later.", background: "variant-filled-error", }); } } ``` Similar, yet simpler, when compared with our `signup` function. We're using the `account.connectWallet` function. This function will: 1. Trigger the user to authenticate, providing the passkey's ID along the way, 2. Use Mercury to reverse-lookup the contract ID given that passkey ID, and finally 3. Return the passkey ID and smart wallet address to our dapp. Great! Let's get the user logged out when they need to. #### User logout This is quite a bit easier than either signup or login functions. We don't really need to communicate with the Stellar network or Mercury here. All we'll do is clear out the user state, essentially. ```js title="src/lib/components/connectButtons.svelte" async function logout() { try { // Reset the localStorage entry for the keyId keyId.reset(); localStorage.removeItem("yog:keyId"); // Set the contract address store to an empty string contractId.set(""); // Refresh the page, just for good measure window.location.reload(); } catch (err) { console.log(err); toastStore.trigger({ message: "Something went wrong logging out. Please try again later.", background: "variant-filled-error", }); } } ``` With those three functions, our dapp is ready for users to authenticate with the dapp! Much easier than you probably expected it to be, right!? ### The "profile menu" Still in our `connectButtons.svelte` component, we also have a collection of buttons and functions that represent a "profile menu" of sorts. The user can use these buttons to view their smart wallet balance, see it on [Stellar Expert](https://stellar.expert), send a donation to our (humble) guestbook maintainer, request more (Testnet) funding, etc. Much of this is unnecessary to dive into here in this tutorial, though I highly recommend taking a look at the [source code](https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/src/lib/components/ConnectButtons.svelte) to get a better understanding of this functionality. However, we will look into the `donate` function here. This is a really useful example of how a dapp can enable their smart wallet users to interact with any asset on the Stellar network. (Here, we are using Testnet XLM for our asset, but the flow would be identical for _any_ asset you may want to use.) The button is still pretty simple, just like the authentication buttons. We are adding some "loading" logic for when the transaction is taking place, though. So, it's got a _few_ more bells and whistles. ```html title="src/lib/components/connectButtons.svelte" ``` The `donate` function takes advantage of the `native` SAC client we made in the `$lib/passkeyClient.ts` file. This allows us to call the transfer function of the contract just like any other JavaScript function. ```js title="src/lib/components/connectButtons.svelte" async function donate() { console.log('starting donation process'); isDonating = true; try { const user = prompt("Give this passkey a name") const at = await native.transfer({ to: networks.testnet.contractId, from: $contractId, amount: BigInt(donation * 10_000_000), }); await account.sign(at, { keyId: $keyId }); const res = await send(at.built!); console.log(res); toastStore.trigger({ message: 'Donation received! You really ARE the goat.', background: 'variant-filled-success', }); getBalance(); } catch (err) { console.log(err); toastStore.trigger({ message: 'Something went wrong donating. Please try again later.', background: 'variant-filled-error', }); } finally { isDonating = false; } } ``` :::info We're simplifying this function _just a bit_ for this tutorial. In the [real dapp](https://github.com/ElliotFriend/ye-olde-guestbook/blob/1a55a5238a71b624b789dfd82a6d7fd996407bd7/src/lib/components/ConnectButtons.svelte#L138), we're using a modal to retrieve the user's input. That ends up looking a bit too cluttered for here, though. ::: All in, that's a pretty easy invocation of the SAC's `transfer` function. We just pass the `from`, `to`, and `amount` fields. Then, we sign the transaction with our `account` instance, providing our passkey ID in the arguments. Finally, we send the transaction using our helper function, which will fire off the request to Launchtube, and we'll be good to go. In this case, we're not really stressed about the return value. We'll just catch any errors, and notify the user with a toast message. Enough of the account and asset things, let's get to the guestbook entries! ## Sign the guestbook First, we'll need a page that allows us to actually _sign_ the guestbook. We'll have a form that takes a `title` and `message` field, and then we'll submit the transaction with the `send` helper function, just like we did with the XLM transfer previously. The form is pretty simple, and it's barely worth mentioning. We have a text input, a textarea input, and a button. Some checks are performed to see if the button should be enabled (if a user is not logged in, for example). Otherwise, it's pretty unremarkable: ```html title="src/routes/sign/+page.svelte" ``` The `signGuestbook` function (which is executed when the button is clicked), is where the more interesting bits are. Even still, it looks quite similar to the other transactions we've submitted (account creation and XLM transfers). ```js title="src/routes/sign/+page.svelte" async function signGuestbook() { try { isLoading = true; const at = await ye_olde_guestbook.write_message({ author: $contractId, title: messageTitle, text: messageText, }); let txn = await account.sign(at.built!, { keyId: $keyId }); const { returnValue } = await send(txn.built!); const messageId = xdr.ScVal.fromXDR(returnValue, 'base64').u32(); toastStore.trigger({ message: 'Huzzah!! You signed my guestbook! Thanks.', background: 'variant-filled-success', }); goto(`/read/${messageId}`); } catch (err) { console.log(err); toastStore.trigger({ message: 'Something went wrong signing the guestbook. Please try again later.', background: 'variant-filled-error', }); } finally { isLoading = false; } } ``` The heart and soul of this function is to invoke the `write_message` function from our contract. Thanks to our generated bindings, that's really easily done. We get the message ID as the return value, and then redirect the user to the page where they can read _that_ particular entry. How does this page read the guestbook entry? Excellent timing for that question! ## Read guestbook entries ### Read a single entry The first page we'll create is one that reads and displays a single guestbook message from the smart contract storage. We'll use a server-side function for this. That way, if we were using a paid RPC provider, we could have this function run on the server and return the relevant data to the client. :::info This `+page.server.ts` is a Svelte way of saying "every time this page is requested by a user, run this function on the server, and give the data to the client." The `[id]` part of the filename tells this route that we expect to have a path-based parameter, and we can use it as `id`. ::: ```js title="src/routes/read/[id]/+page.server.ts" export const load: PageServerLoad = async ({ params }) => { try { let { result } = await guestbook.read_message({ message_id: parseInt(params.id), }); return { id: params.id, message: result.unwrap(), }; } catch (err) { error(500, { message: "Sorry, something went wrong. Most likely, the message you're looking for doesn't exist.", }); } }; ``` You can see here we're using one of the contract functions, `read_message` to get the data. This is a "read-only" function, meaning that no on-chain state is modified when it's run. So, we can just simulate the invocation, which is already done for you when the bindings-generated function is called, and just take the data from the simulation response! Pretty neat, right?! We pass the resulting message details back to the page, where it will be displayed. ```html title="src/routes/read/[id]/+page.svelte" Read Message {data.id} You're viewing just message {data.id}. You can read all of them here, as well. ``` ### Read all entries Great! If you know the ID of the entry you want to read. Most of the time, you probably wouldn't. Let's make a page that can read/display all of the guestbook entries. For this, we'll (again) keep as much of the query logic server-side as possible. These ledger entry results can be cached. And, the client doesn't need to make even more round trips just to query these entries. The route that performs this query is another `+page.server.ts` file: ```js title="src/routes/read/+page.server.ts" getAllMessages, getWelcomeMessage, } from "$lib/server/getLedgerEntries"; export const load: PageServerLoad = async () => { return { welcomeMessage: await getWelcomeMessage(), messages: await getAllMessages(), }; }; ``` We're making use of two functions that we've defined elsewhere. The `welcomeMessage` will **always** have ID 1, and we want to _always_ display it at the top of the page. The two functions are defined like this: ```js title="src/lib/server/getLedgerEntries.ts" // notice our bindings re-exports the Stellar SDK, so we don't even really need // to import any Stellar-related classes or functions from elsewhere. // First, we need a function to build these LedgerKeys so we can query the network function buildMessageLedgerKey(messageId: number) { const ledgerKey = xdr.LedgerKey.contractData( new xdr.LedgerKeyContractData({ contract: new Address(networks.testnet.contractId).toScAddress(), key: xdr.ScVal.scvVec([xdr.ScVal.scvSymbol('Message'), xdr.ScVal.scvU32(messageId)]), durability: xdr.ContractDataDurability.persistent(), }), ); return ledgerKey; } // To get our welcome message, we use the `getLedgerEntries` function // from the RPC instance. export async function getWelcomeMessage(): Promise { const result = await rpc.getLedgerEntries(buildMessageLedgerKey(1)); return scValToNative(result.entries[0].val.contractData().val()); } // Our contract stores the number of guestbook messages in its instance // storage. So, we have a function to query exactly how many messages we // need to retrieve. export async function getMessageCount() { const result = await rpc.getLedgerEntries( new Contract(networks.testnet.contractId).getFootprint(), ); const messageCount = result.entries[0].val .contractData() .val() .instance() .storage() ?.filter((item) => item.val().switch().name === 'scvU32'); return messageCount![0].val().value() as number; } // Now we can iterate and make ledger key for each relevant message, // and add that to our getLedgerEntries query. The maximum number of ledger entries // to query is 200. export async function getAllMessages(): Promise { const totalCount = await getMessageCount(); const ledgerKeysArray = []; for (let messageId = 2; messageId <= totalCount; messageId++) { ledgerKeysArray.push(buildMessageLedgerKey(messageId)); } const result = await rpc.getLedgerEntries(...ledgerKeysArray); const messages = result.entries.map((message) => { return { ...scValToNative(message.val.contractData().val()), }; }); return messages; } ``` Did you catch all that?! Well done! That's the querying part of reading all messages. Now, to _display_ those messages, we get that data into our Svelte page. ```html title="src/routes/read/+page.svelte" Read the Book Take a gander at all these messages! Showing {sortNewestFirst ? 'Newest' : 'Oldest'} First {#each messages as message, i (message.ledger)} {/each} ``` We're loading the data we retrieve from the server. We even include a little toggle switch so the user can decide if they want to see newer or older entries first. Then, it's time to display the messages. Again, we use the `GuestbookMessage` component. We display one instance of the component for each message entry. ## Edit a guestbook entry If we take a brief look inside the [`GuestbookMessage` component](https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/src/lib/components/GuestbookMessage.svelte), we can see that we have some form fields in the event the user wants to edit a message. We limit the display of these parts of the component to cases where the _logged in_ user's smart wallet `C...` address matches the guestbook entry's `author` field. The HTML of the page is outside of what we need to cover here, but suffice it to say when the user is editing an entry, the form fields behave pretty similar to the form on the "sign the guestbook" page. The functions are a bit more interesting, and more relevant to this tutorial. :::tip The benefit of including this functionality within the message-displaying component, is that the edit functions can be used wherever the user is reading the messages. Whether they're reading through _all_ the entries, or just a single entry, if they were the author of a message, the edit buttons will be available to them. ::: ```js title="src/lib/components/GuestbookMessage.svelte" // This is how we receive the "props" from the pages that instantiate this component export let message: Message; export let messageId: number; let editing: boolean; let isLoading: boolean; // Store the original values from the contract's storage. The form will be "bound" // to these values later on, when the user is modifying the entry. let messageTitle = message.title; let messageText = message.text; /** * If the user chooses to cancel the editing the message, we should revert the * message state back to the original values. */ const cancelEdit = () => { messageTitle = message.title; messageText = message.text; editing = false; }; const submitEdit = async () => { console.log('submitting message edit'); isLoading = true; try { const at = await ye_olde_guestbook.edit_message({ message_id: messageId, title: messageTitle, text: messageText, }); const txn = await account.sign(at.built!, { keyId: $keyId }); await send(txn.built!); toastStore.trigger({ message: 'Message edited successfully.', background: 'variant-filled-success', }); } catch (err) { console.log(err); toastStore.trigger({ message: 'Something went wrong editing your message. Please try again later.', background: 'variant-filled-error', }); } finally { editing = false; isLoading.set(false); } }; ``` Notice that, unlike when we signed the guestbook in the first place, we don't have to supply an `author` argument. The smart contract is designed in a way that it looks for the author (and requires authentication) from _within_ its own storage. This ensures that the _original_ author of a guestbook entry is the **only** account authorized to make modifications to it. Not even our gracious guestbook host could modify an entry! --- ## Overview(Guestbook) In this tutorial, we'll walk you through building an old-timey [internet guestbook](https://en.wikipedia.org/wiki/Guestbook)! (Trust me, they were all the rage back in the day.) We'll be examining how the project is constructed, starting with the smart contract. Then, we'll turn that deployed smart contract into a "bindings package," allowing us to seamlessly integrate it into our frontend project. To get our users authenticated, we'll be using Stellar's new passkeys capability and giving each of our users their very own smart wallet. As a bonus, this guestbook is _already_ a usable project (on Testnet) you can experiment with and use **right now**! After this tutorial, you'll have a solid understanding of how smart contracts and web applications can work together in harmony. You'll also have practical tools and examples for how you might integrate passkey-powered smart wallets into your own projects. For this tutorial, we'll walk through the steps as we build a sample application we've called [Ye Olde Guestbook](https://ye-olde-guestbook.vercel.app)[^1], which will be used to showcase various features. :::caution Although Ye Olde Guestbook is a full-fledged application on Stellar's Testnet, it has been built solely to showcase Stellar functionality for the educational benefit of the Stellar community. It should not be deployed and used on Stellar Mainnet. ::: ![Ye Olde Guestbook Dapp](/assets/guestbook/frontend.png) ## Project Setup ### Project Requirements To build this guestbook application, we'll need a few pieces. - **Application framework:** we're using SvelteKit, opting for a type-checked project using TypeScript. SvelteKit (and Svelte on its own) is quite a capable framework, and we'll be using some of its features in this project. However, we will not be diving into those Svelte-specific areas very heavily in this tutorial. The source code of the project is freely open and available and has some decent informational comments throughout if you would like to peruse it for those purposes. - **Frontend framework:** We're using [Skeleton](https://www.skeleton.dev) to simplify the use of [Tailwind CSS](https://tailwindcss.com). - **A way to interact with the network:** this is a TypeScript application, and we're using the `@stellar/stellar-sdk` for this. You could make traditional `fetch` requests if you wanted to, depending on your deployment decisions. In either case, we'll need the SDK to interact with keypairs and transactions. We'll also be using a data indexer to access historical network events, and we'll cover more of this at a later point in the tutorial. - **A way to interact with a user's account:** we're foregoing the traditional wallets here, and we'll use `passkey-kit` to give our users a smart wallet to interact with. They can interact with this smart wallet (via passkey-kit) through methods they're already familiar with (thumbprints, Face ID, etc.). :::note While we are using the above components to construct our application, we have done our best to write this tutorial in such a way that dependency on any one of these things is minimized. Ideally, you should be able to use the TypeScript code we've written and plug it into any other framework you'd like with minimal effort. ::: Some choices we've made during the course of development: - Some of the non-Stellar components lean a _bit more_ into the Svelte way of doing things, but we've worked to make it fairly easily translatable into React, Astro, etc. - This project is written so that a single deployment of the app interacts with a single deployment of the smart contract. It could be written differently, but we haven't here for the sake of simplicity. - We're rolling our own passkeys service here. That means we'll set up and use `passkey-kit` (both client- and server-side components) in our own dapp. In the long run, this may not be the necessary usage pattern. It's likely that services will crop up to act as a "wallet factory" that can create smart wallets, and facilitate adding signers for various applications. Perhaps these services will be provided by existing wallets? Perhaps these services will be unknown to the user (and maybe even developers) in the future? Who knows! The sky's the limit! (But that's not the case yet, so we're doing it ourselves.) - It should be _relatively_ responsive, no promises, though - We've chosen a theme from Skeleton, so it looks nice right away. - There's a mix of client- and server-side logic. This is due to the fact that we'll need to keep some authentication credentials secret, and we want to avoid leaking these to the user-facing code. Some of these techniques are a bit SvelteKit-specific, but it should ultimately be understandable. - We're deploying to a free-tier Vercel project. We've had really good success in getting SvelteKit and Stellar projects deployed easily and quickly, and with very little configuration. Your mileage may vary, but this should be a pretty decent starting point. - The application is likely not as performant as it could be. Neither is it as optimized as it could be. We've tried to encapsulate the various functionalities in a way that makes sense to the developer reading the codebase, so there is some code duplication and things could be done in a "better" way. - We do _some_ error handling, but not nearly as much as you would want for a real-world application. If something seems like it's not working, and you're not seeing an error, open your developer console, and you might be able to figure out what has gone wrong. - We have not implemented _any_ automated testing. You'll probably want some for your application. :::note This tutorial is probably best viewed as "_nearly_ comprehensive." We aren't going to walk you through each and every file in our codebase, and the files we do use to illustrate concepts in the tutorial may not be _entirely_ present or explained. However, we will cover the basics, and point you to more complete examples in the codebase when applicable. ::: ### Dev Helpers - [Passkey Kit](https://github.com/kalepail/passkey-kit): A TypeScript SDK for creating and managing Stellar smart wallets. - [Launchtube](https://launchtube.xyz): Similar to a [Paymaster](https://eips.ethereum.org/EIPS/eip-4337#extension-paymasters) in the EVM world, the Launchtube service aims to alleviate all of the challenges and complexities of getting a transaction on-chain by giving you an API that accepts Soroban ops and then handles getting those entries successfully submitted to the network. - [Stellar Lab](https://lab.stellar.org): An experimental playground to interact with the Stellar network. ## Getting Started Here are the steps we've taken to start building Ye Olde Guestbook. Feel free to be inspired and customize these directions as you see fit. The entire [Ye Olde Guestbook codebase](https://github.com/elliotfriend/ye-olde-guestbook) is freely open and available on GitHub for reference. ### Start from the `soroban-template` repository With the move to smart contract development, a newly emerging utility in the Stellar ecosystem is the "[Soroban template](../../guides/dapps/soroban-contract-init-template.mdx)." These templates can help alleviate the burden of writing boilerplate code, and can help adapt typical Stellar development workflows into framework-specific reference templates. We've created [just such a template](https://github.com/ElliotFriend/soroban-template-sveltekit-passkeys) that can help you get started developing with SvelteKit and passkeys from the very beginning. You can either use the template on the GitHub website: ![Github Template Project](/assets/guestbook/github_template.png) Or, you can (fork and) clone the template repository locally, and start working that way: ```shell git clone https://github.com/ElliotFriend/soroban-template-sveltekit-passkeys ye-olde-guestbook ``` This frontend template will give you some scaffolding and some (opinionated) defaults. What you do from there is up to you! This template will give you a few things to help you hit the ground running: - a starter `/contracts` directory with a `hello_world` contract in it, - a pre-configured set of dependencies and packages, including the `hello_world` bindings package, - boilerplate passkey logic and helpers already written out-of-the-box, - an initialization script to deploy contracts and generate bindings for them, and - you'll have a ready-to-customize SvelteKit site, written using TypeScript. What more could you want!? ### Set up the `.env` file The template comes with a `.env.example` file, that you will need to modify. First, copy or move this file to `.env`: ```shell cp .env.example .env ``` Then, open up the `.env` file, and begin customizing any of the entries you need. If you're planning to run on Testnet (and you _should_ start there), most of the variables will be suitable as-is. Some variables you will want to change include: ```shell PUBLIC_STELLAR_ACCOUNT=stroopy # you're welcome to use stroopy, but if you have an name you'd prefer, put that here PRIVATE_FUNDER_SECRET_KEY=S...ECRET # fund an account on Testnet and put the secret key here PUBLIC_FUNDER_PUBLIC_KEY=G...ADDRESS # put the public key from the funded account here ``` ### Install Dependencies With our pre-existing template, everything you need should be pulled in from the `package.json` and `Cargo.toml` files. All you've got to do is open up a terminal and install the dependencies: ```bash pnpm install ``` :::note We're not aiming to dictate which package manager you should use. When building a full-stack SvelteKit app using Stellar and passkeys, we've recently seen a lot of success and reliability using `pnpm`. Who's to say _why_ that's the case, and it certainly could be a fluke and limited to our own experience. In any case, we'll be using `pnpm` for the remainder of this tutorial. ::: Next up, let's look at the smart contract at the heart of the project. ## Notes [^1]: Fun fact: The "Y" character in the word "Ye" was commonly used in the Old and Middle English periods, and represented the [letter Thorn](), which is no longer part of the modern English alphabet. It was actually _not_ pronounced the way we say the modern letter "y," but was rather vocalized as the digraph "th." So, 500 years ago, you would've pronounced "Ye" exactly the same way you pronounce "the" today. Crazy, right!? --- ## Passkeys Prerequisites Passkeys are an amazing way to help dapp developers (like yourself) connect users with their projects, protocols, applications, etc. Learn more on the [passkey wallet guide](../../guides/contract-accounts/smart-wallets.mdx). We have been hard at work pioneering some tools to increase the adoption and ease-of-use for passkeys on Stellar. For this tutorial we'll be using the **incredible** [`passkey-kit` package](https://github.com/kalepail/passkey-kit), which takes SO MUCH of the headache and hassle out of the equation. Before we get into the nitty gritty on passkeys, we have some chores to do. First, we'll set up Launchtube, a service that will help get our transactions on-chain without worrying about gas fees, sequence numbers, or source accounts. Really useful. Then, we'll create a Mercury indexing program, which will be used to keep track of the public key half of a user-generated passkey and then do a reverse lookup to see which smart wallet address the passkey has been added to. ## Launchtube Let's start with [Launchtube](https://launchtube.xyz). As we mentioned earlier, Launchtube is similar to a "paymaster" service, if you're familiar with account abstraction in EVM networks. We won't actually need to interact with Launchtube _directly_. All that will be handled by the `passkey-kit` package. However, we'll need to get a JWT token that will allow us to authenticate our dapp with Launchtube. For Testnet Launchtube tokens, we can generate one any time we like. All you have to do is visit `https://testnet.launchtube.xyz/gen` to receive a JWT token that will be valid for three months, and will have 100 XLM in credits (these credits will be consumed when you submit network transactions through Launchtube). Go ahead, [give it a try](https://testnet.launchtube.xyz/gen)! :::tip We do have Mainnet Launchtube tokens available! You can request a token in the [`#launchtube` channel](https://discord.com/channels/897514728459468821/1293204627361108141) on our [Stellar Developer Discord server](https://discord.gg/stellardev). In particular, pinging `@kalepail`, `@ElliotFriend`, or `@carsten.xlm` should get you on your way pretty quickly. ::: Once you have your Launchtube token, copy/paste it into the `.env` file, as the `PRIVATE_LAUNCHTUBE_JWT` variable: ```shell PRIVATE_LAUNCHTUBE_JWT= ``` :::info The `PRIVATE_` and `PUBLIC_` environment variables are a SvelteKit convention, allowing us to access these variables in appropriate places throughout our codebase using the [`$env/static/private`](https://svelte.dev/docs/kit/$env-static-private) and [`$env/static/public`](https://svelte.dev/docs/kit/$env-static-public) modules, respectively. ::: ## Mercury Now, on to [Mercury](https://www.mercurydata.app). This is a data indexer, running on both Testnet and Mainnet. The team is developing some bleeding-edge data tools that are beginning to redefine what's possible with network data. One such development is the [ZephyrVM](https://docs.mercurydata.app/zephyr-full-customization/introduction): Mercury's cloud execution environment. In short, Zephyr allows you to write (Rust) programs that will run at the close of _every_ ledger on the Stellar network. Inside that program, you can access any kind of current or past data, interact with external web services, create serverless functions, and populate databases. Similar to Launchtube, we won't be _directly_ interacting with Mercury inside the Ye Olde Guestbook dapp. Those interactions will be handled by the `passkey-kit` package. Also similar to Launchtube, this one takes some setting up. The `passkey-kit` package doesn't "ship" with a Zephyr program in the published package, but it _does_ have all the Zephyr goodness you'll need in the source repository. Here's how you get that Zephyr program running on Mercury so you can access the indexed smart wallet events. By the way, these commands are probably best run _outside_ the directory where you're building your guestbook dapp. 1. Clone the `passkey-kit` repository from GitHub and enter the `zephyr` directory within it: ```shell git clone https://github.com/kalepail/passkey-kit cd passkey-kit/zephyr ``` 2. Get an authentication token from the Mercury website. You can login to the [Testnet dashboard](https://test.mercurydata.app) here. Click on the **Get access token** button under the "Active subscriptions" section. You'll be given a JWT which will be valid for the next seven days. ![Mercury Data JWT Token](/assets/guestbook/mercury_token.png) Copy/paste this token into the `.env` file: ```shell PRIVATE_MERCURY_JWT= ``` 3. (Optionally) You can get a [long-lasting authentication token](https://docs.mercurydata.app/get-started-with-mercury/authentication) for your account using this token, and making a request to Mercury's API: ```shell curl -X POST https://api.mercurydata.app/v2/key \ -H "authorization: Bearer ``` This will give you an API key that can also be added to your `.env` file. The benefit of this API key is that it will not expire until you generate another API key. :::info For this tutorial, you'll only need one of these. You can specify the JWT **or** the API key, and get things working exactly the same. In the `PasskeyServer`, though, make sure you specify the corresponding value. ::: 4. Compile and deploy the event indexer Zephyr program to the Testnet network. ```shell cargo install mercury-cli export MERCURY_JWT="" # Make sure you're using Rust version 1.79.0 or newer mercury-cli --jwt $MERCURY_JWT --local false --mainnet false deploy ``` If everything succeeds, you're ready to go! Well done! You're now ready to dive into the actual passkey implementation and get your users authenticated with the guestbook dapp! Let's get to it! ## Troubleshooting It's possible something has gone wrong during your execution of the processes above. Here are some general suggestions of fixes or things you can try if something goes wrong with your use of Launchtube or Mercury: 1. **Generate a new Launchtube token.** It's possible the Launchtube token you're using has run out of credits. Since we're using Testnet for this tutorial, there's no harm in generating a brand new token any time by visiting `https://testnet.launchtube.xyz/gen` in your browser. 2. **Make sure your Zephyr program successfully deployed.** I've been stuck more than once with a not-working Mercury request because the Zephyr program hadn't actually deployed successfully. Make sure the `mercury-cli deploy` command's output doesn't have any errors in it. 3. **Check the [Mercury documentation](https://docs.mercurydata.app).** It's quite good and can help you get past a lot of the hurdles you might face. In any case, feel free to ask questions or drop a chat in the [`#passkeys`](https://discord.com/channels/897514728459468821/1250851135561142423) and [`#launchtube`](https://discord.com/channels/897514728459468821/1293204627361108141) channels in the Stellar Developer Discord server. There's usually somebody around who's ready and willing to help out! --- ## Setup Passkeys Now, we've got the requisite accounts, tokens, etc. created, and we're ready to start putting the `passkey-kit` to work, getting our users connected! ## Passkey client We'll start by creating an instance of the `PasskeyKit` class. We'll call it `account`, and this `account` will be the **primary** point of interaction for the dapp and the user's passkey. Every transaction will be signed using `account.sign()`, users will signup with `account.createWallet()`, users will login with `account.connectWallet()`. This `account` is a pretty tough workhorse! Let's make it happen. We're creating this in `src/lib/passkeyClient.ts` so it's available to us in the rest of our frontend codebase. The [`$lib` import alias](https://svelte.dev/docs/kit/$lib) is a SvelteKit thing, but the important thing is we want this file (and its exports) to be available throughout **all** of our frontend files. How you make that happen for other frameworks is an exercise left to the reader. ```js title="src/lib/passkeyClient.ts" PUBLIC_STELLAR_RPC_URL, PUBLIC_STELLAR_NETWORK_PASSPHRASE, PUBLIC_WALLET_WASM_HASH, } from "$env/static/public"; export const account = new PasskeyKit({ rpcUrl: PUBLIC_STELLAR_RPC_URL, networkPassphrase: PUBLIC_STELLAR_NETWORK_PASSPHRASE, walletWasmHash: PUBLIC_WALLET_WASM_HASH, }); ``` The `PUBLIC_WALLET_WASM_HASH` variable is the Wasm hash of the smart wallet's contract code. This Wasm hash identifies the executable code that will be deployed for new smart wallets and is simply the Sha256 hash of the compiled contract executable file. This hash is returned during when a compiled contract is installed on the network. That's all there is to it! This `account` will be fully ready to authenticate users and sign transactions! (It's even easier than all the prerequisites isn't it!) Now, we've also added some useful "helpers" into the `$lib/passkeyClient.ts` file in our template. The [source code file](https://github.com/ElliotFriend/soroban-template-sveltekit-passkeys/blob/main/src/lib/passkeyClient.ts) is commented to reflect what these helpers are, and how they work. These are strictly for convenience, though. You could stop right here and come away with perfectly valid signed passkey transactions. These helpers are: - A configured instance of the `rpc.Server` class so we can make RPC requests without having to know/import the RPC's URL all the time. ```js title="src/lib/passkeyClient.ts" /** * A configured Stellar RPC server instance used to interact with the network */ export const rpc = new Server(PUBLIC_STELLAR_RPC_URL); ``` - A SAC client to interact with the native XLM asset contract. We're making an assumption that native lumens is a "good enough" asset interaction to get the tutorial working, and for playing on Testnet. You could easily export _another_ SAC client to interact with USDC, for example. The native contract address can be obtained from Stellar-CLI with the command `stellar contract id asset --asset native`. ```js title="src/lib/passkeyClient.ts" /** * A client allowing us to easily create SAC clients for any asset on the * network. */ const sac = new SACClient({ rpcUrl: PUBLIC_STELLAR_RPC_URL, networkPassphrase: PUBLIC_STELLAR_NETWORK_PASSPHRASE, }); /** * A SAC client for the native XLM asset. */ export const native = sac.getSACClient(PUBLIC_NATIVE_CONTRACT_ADDRESS); ``` ## Passkey server So, that's the client-facing passkey code (and some helpers) taken care of. What about the server-side, where we want to be cautious about leaking secrets and tokens?! We're setting this up in `src/lib/server/passkeyServer.ts`, for similar reasons we listed above. This gives us an importable `server` instance that can be accessed and used in other server-side logic. Svelte gives us the added benefit of [keeping the code in this directory safe](https://svelte.dev/docs/kit/server-only-modules#Your-modules). When we want to safeguard credentials and secrets, we can put any sensitive code in the `$lib/server` directory. ```js title="src/lib/server/passkeyServer.ts" PUBLIC_LAUNCHTUBE_URL, PUBLIC_MERCURY_URL, PUBLIC_STELLAR_RPC_URL, } from "$env/static/public"; PRIVATE_LAUNCHTUBE_JWT, PRIVATE_MERCURY_JWT, } from "$env/static/private"; export const server = new PasskeyServer({ rpcUrl: PUBLIC_STELLAR_RPC_URL, launchtubeUrl: PUBLIC_LAUNCHTUBE_URL, launchtubeJwt: PRIVATE_LAUNCHTUBE_JWT, mercuryUrl: PUBLIC_MERCURY_URL, mercuryJwt: PRIVATE_MERCURY_JWT, // mercuryKey: PRIVATE_MERCURY_KEY, // optionally }); ``` And you're done with the `PasskeyServer`! Well done! This `server` instance will be used for sending transactions (via Launchtube) and reverse-looking-up contract addresses from a known passkey ID (via Mercury). ### API routes Now, we'll need a way to utilize some of the functionality of this `server` from the client without exposing any of the sensitive information. For that, we'll set up a collection of (SvelteKit) routes to act as a backend, and _those routes_ (not the client-side code) will make use of the `server` instance. These files live in `src/routes/api/*` in the project repo. Some of the structure here is a bit Svelte-specific, but it should pretty easily make sense enough to non-Svelte developers regardless. The _one_ SvelteKit-specific thing to note is any file named `*server.{ts,svelte}` will **only** run [on the server](https://svelte.dev/docs/kit/routing#server). Your secrets, tokens, credentials, etc. are considered safe to use within these files. #### `/api/send` This API endpoint will send a transaction to the network, via Launchtube. It receives a `POST` request, whose `body` object contains a base64-encoded transaction. :::warning If you're creating a `yourdomain.com/api/send` method, you will probably need to do "something" to ensure that only the right "kinds" of transactions are actually sent to the network. I.e., make sure it's coming from your dapp, your users, etc. Otherwise, it would be possible for a bad actor to discover they could use this to send their own transactions, while you pick up the tab for the fees! The implementation of this is outside the scope of this tutorial, but be sure to consider these kinds of risks as you prepare for a more production-level deployment. ::: ```js title="src/routes/api/send/+server.ts" export const POST: RequestHandler = async ({ request }) => { const { xdr } = await request.json(); const res = await server.send(xdr); return json(res); }; ``` #### `/api/contract/[signer]` This endpoint will reverse-lookup (via Mercury) a contract address given a passkey ID. The path parameter `[signer]` is how we'll give the passkey ID to the API `GET` request. ```js title="src/routes/api/contract/[signer]/+server.ts" export const GET: RequestHandler = async ({ params }) => { const contractId = await server.getContractId(params.signer!); return new Response(String(contractId)); }; ``` #### `/api/fund/[address]` This is another helper, but on the API side of things! [Friendbot](../../../networks/README.mdx#friendbot) doesn't support `C...` addresses for Testnet funding. So, we're setting up an endpoint so we can add some funds to the dapp users' wallets. This gives them some tokens to play around with, and allows _us_ to receive those guestbook donations! This API endpoint is not strictly necessary. But, it is a useful way to see how these kinds of interactions can occur between a "regular" `G...` address and a soroban contract `C...` address. ```js title="src/routes/api/fund/[address]/+server.ts" export const GET: RequestHandler = async ({ params, fetch }) => { const fundKeypair = Keypair.fromSecret(PRIVATE_FUNDER_SECRET_KEY); const fundSigner = basicNodeSigner(fundKeypair, PUBLIC_STELLAR_NETWORK_PASSPHRASE); try { const { built, ...transfer } = await native.transfer({ from: fundKeypair.publicKey(), to: params.address, amount: BigInt(25 * 10_000_000), }); await transfer.signAuthEntries({ publicKey: fundKeypair.publicKey(), signAuthEntry: (auth) => fundSigner.signAuthEntry(auth), }); await fetch('/api/send', { method: 'POST', body: JSON.stringify({ xdr: built!.toXDR(), }), }); return json({ status: 200, message: 'Smart wallet successfully funded', }); } catch (err) { console.error(err); error(500, { message: 'Error when funding smart wallet', }); } }; ``` ### Passkey client helpers Each of those API endpoints receives a corresponding function in the `$lib/passkeyClient.ts` file, just to make it a little easier on the client-side to make use of the API routes we just made. This allows us to write the `fetch` code once, and use it consistently everywhere else. They're pretty straightforward and don't really need much explanation. We'll add them to the end of the file: ```js title="src/lib/passkeyClient.ts" /** * A wrapper function so it's easier for our client-side code to access the * `/api/send` endpoint we have created. * * @param xdr - The base64-encoded, signed transaction. This transaction * **must** contain a Soroban operation * @returns JSON object containing the RPC's response */ export async function send(xdr: string) { return fetch("/api/send", { method: "POST", body: JSON.stringify({ xdr, }), }).then(async (res) => { if (res.ok) return res.json(); else throw await res.text(); }); } /** * A wrapper function so it's easier for our client-side code to access the * `/api/contract/[signer]` endpoint we have created. * * @param signer - The passkey ID we want to find an associated smart wallet for * @returns The contract address to which the specified signer has been added */ export async function getContractId(signer: string) { return fetch(`/api/contract/${signer}`).then(async (res) => { if (res.ok) return res.text(); else throw await res.text(); }); } /** * A wrapper function so it's easier for our client-side code to access the * `/api/fund/[address]` endpoint we have created. * * @param address - The contract address to fund on the Testnet */ export async function fundContract(address: string) { return fetch(`/api/fund/${address}`).then(async (res) => { if (res.ok) return res.json(); else throw await res.text(); }); } ``` Still with us?! Incredible! You're a rock star! And, you're ready to get into the interactions with the smart contract! See you on the next page! --- ## The Guestbook Contract The heart of this project starts with our smart contract. This smart contract will, in essence, act as a database for our guestbook messages. Users will be able to write messages, read them, and edit their own previously submitted message(s). Additionally, the contract will be upgradeable, and it will require initialization. ## Required functionality All the following "business logic" will be handled by our smart contract: - **A means of writing messages.** Users can invoke this function to leave a message for the site maintainer. They will have to authenticate this function, and it must contain a `title` and `text` field (both strings). The function will return the ID number of the message, which increments sequentially. - **A means of reading messages.** This function will allow a user to "query" the contract for a guestbook message, by providing the message ID. Non-existing IDs will result in an error. - **A means of editing messages (with authentication).** If a user needs to modify their previously written message, they can use this function to do so. They must provide proper authorization to do so, and they must provide either a `title` or `text` field (both cannot be empty, but one of them could). - **A means of retrieving donations and transferring them to the "admin" address.** The hubris associated with requesting donations on a site like this speaks volumes of the maintainer's sense of self. However, providing this functionality is an excellent exercise in asset interactions within the smart contract. - We'll also need some utility functions that the contract will use internally, as well as a `__constructor` and an `upgrade` function, in case we need to upgrade our smart contract in the future. :::note We'll be diving into each of the main functions below, but if you want to see the whole smart contract uninterrupted, it can be found here: [https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/contracts/ye_olde_guestbook/src/lib.rs](https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/contracts/ye_olde_guestbook/src/lib.rs) ::: ## How it works ### Contract functions #### `__constructor` With the release and successful validator vote of Protocol 22, smart contracts are now capable of utilizing a `__constructor` function! Previously, any initialization of a smart contract had to be done in a subsequent invocation of the contract following the `deploy` action. Now, it's possible to perform that initialization at deploy-time. This prevents front-running, and keeps the contract you've deployed within your own control at all times. Constructor functions _look_ pretty much exactly the same as the previously used `init` functions. The only difference is _when_ the function is executed. ```rust /// Initializes the guestbook with a warm welcome message for prospective /// signers to read. /// /// # Arguments /// /// * `admin` - The address which will be the owner and administrator of the /// guestbook. /// * `title` - The title or subject of the welcome message. /// * `text` - The body or contents of the welcome message. /// /// # Panics /// /// * If the `title` argument is empty or missing. /// * If the `text` argument is empty or missing. pub fn __constructor( env: Env, admin: Address, title: String, text: String, ) -> Result<(), Error> { check_string_not_empty(&env, &title); check_string_not_empty(&env, &text); admin.require_auth(); env.storage().instance().set(&DataKey::Admin, &admin); let first_message = Message { author: admin, ledger: env.ledger().sequence(), title, text, }; save_message(&env, first_message); Ok(()) } ``` #### `write_message` First things first, we need a function that will allow a message to be written in the guestbook. A simple struct is created, and it's stored in the contract's persistent storage. This is a fairly simple function that takes three pieces of data from the invocation (`author`, `title`, and `text`) and then creates a struct to store in the contract's persistent storage entries. Here are some things to note: - We're using a helper function called `check_string_not_empty` to ensure that a non-empty value has been passed for both the `title` and `text` arguments. More on this function later on. - We're requiring authentication from the author's address, to ensure they've authorized the message entry to be associated with them. - We're utilizing a `save_message` utility function to do the actual storage entry reading/writing. More on the specifics of this function [later on](#save_message), but for now just know it's storing the `new_message` struct, and returning the ID of the stored message. ```rust /// Write a message to the guestbook. /// /// # Arguments /// /// * `author` - The sender of the message. /// * `title` - The title or subject of the guestbook message. /// * `text` - The body or contents of the guestbook message. /// /// # Panics /// /// * If the `title` argument is empty or missing. /// * If the `text` argument is empty or missing. pub fn write_message( env: Env, author: Address, title: String, text: String, ) -> Result { check_string_not_empty(&env, &title); check_string_not_empty(&env, &text); author.require_auth(); let new_message = Message { author, ledger: env.ledger().sequence(), title, text, }; let message_id = save_message(&env, new_message); return Ok(message_id); } ``` #### `edit_message` We'll also make it possible for a user to edit a message that's already been written. In the event that only the `text` or `title` need to be changed, we'll allow for passing of empty strings as the arguments here. We'll check to ensure _both_ aren't empty, however. We're using a `get_message` utility function here to read the data from the contract's storage entries. Retrieving a message is relatively common in this contract, so we've created a utility to minimize duplicate code. We modify the message by: - retrieving it from storage (into a mutable variable), - assigning the struct fields to the edited value (or the original if an argument is empty), and - updating the message's ledger number to the current ledger's value. It's important to note we're _not_ requiring authentication from a passed-in `Address` argument. Instead, we retrieve the message struct first, and require authentication from the stored author. The process of saving this edited message object is quite similar to the `write_message` function, except that we're not modifying the `MessageCount`, since we're only _modifying_ and not _adding_ a message. ```rust /// Edit a specified message in the guestbook. /// /// # Arguments /// /// * `message_id` - The ID number of the message to edit. /// * `title` - The title or subject of the guestbook message. /// * `text` - The body or contents of the guestbook message. /// /// # Panics /// /// * If both the `title` AND `text` arguments are empty or missing. /// * If there is no authorization from the original message author. pub fn edit_message( env: Env, message_id: u32, title: String, text: String, ) -> Result<(), Error> { if title.is_empty() { check_string_not_empty(&env, &text); } if text.is_empty() { check_string_not_empty(&env, &title); } let mut message = get_message(&env, message_id); message.author.require_auth(); let edited_title = if title.is_empty() { message.title } else { title }; let edited_text = if text.is_empty() { message.text } else { text }; message.title = edited_title; message.text = edited_text; message.ledger = env.ledger().sequence(); env.storage() .persistent() .set(&DataKey::Message(message_id), &message); return Ok(()); } ``` #### `read_message` Reading a desired message is as simple as querying the contract's persistent storage, and returning the `Message` struct from the contract. Again, we're using the `get_message` utility function, so we'll cover the [specifics of that later on](#get_message). Briefly for now, we're passing in the message ID, and returning the corresponding message, erroring along the way if the message ID doesn't exist in the contract's storage. ```rust /// Read a specified message from the guestbook. /// /// # Arguments /// /// * `message_id` - The ID number of the message to retrieve. /// /// # Panics /// /// * If the message ID is not associated with a message. pub fn read_message(env: Env, message_id: u32) -> Result { let message = get_message(&env, message_id); Ok(message) } ``` #### `read_latest` But, what if someone just wants to read the _latest_ message, and doesn't know what its ID number is? Well, we're providing a function for exactly that. No arguments to pass in. No authentication. Just pull the message from the contract's persistent storage, and return the struct (or panic, if the contract doesn't have any messages yet). Easy peasy. ```rust /// Read the latest message to be sent to the guestbook. pub fn read_latest(env: Env) -> Result { let latest_id = env .storage() .instance() .get(&DataKey::MessageCount) .unwrap(); let latest_message = get_message(&env, latest_id); Ok(latest_message) } ``` #### `claim_donations` :::info We'll set aside whether or not the maintainer of the guestbook _should_ be soliciting donations, and we'll just assume that they _want to_. It's a great way to think about asset interoperability in your smart contracts, so let's go for it! ::: The `claim_donations` function will allow the invoker of the function to send a balance of any token to the admin of the guestbook contract. We'll direct your attention to two aspects of this function, in particular. First, we're requiring an `Address` for the token that should be claimed. It may be your first instinct to hard-code and default to native XLM for these donations. This can certainly be done, but the address for that contract will be different on Mainnet, Testnet, or Futurenet, and the contract would have to be modified and re-compiled for each network you want to deploy to. A more "universally" applicable approach is to take the token address as an argument to this function, and allow the donors and admin to use whichever token they deem suitable for the situation. Second, we're not requiring any authentication for this function. It's not really necessary to add that logic into the mix, because no real harm will come if a non-admin invokes the function: - There's no risk that funds will be sent to the wrong address. The admin address is being read from the contract's instance storage. - There's no risk of the admin receiving unwanted tokens. If the token would require a trustline, and one does not exist on the account (i.e. the admin has not opted-in to holding that asset), the invocation will simply fail. If it's a Soroban-only contract token, the storage entry of the new balance will be paid by the invoker of this function. The admin is not out any funds, and the balance won't have any meaningful impact on their account. - The best-case scenario would be an altruistic person _really_ wants the admin to have the tokens they've donated, so they'll fork over the gas money to trigger the tokens held by the contract to be sent to the admin. Besides those two things, everything else is pretty straightforward. We create a token client, check the contract's balance of that token, and if there's a positive balance, then we send the tokens to the admin address. ```rust /// Claim any donations that have been made to the guestbook contract. /// /// # Panics /// /// * If the contract is not holding any donations balance. pub fn claim_donations(env: Env, token: Address) -> Result { let token_client = token::TokenClient::new(&env, &token); let contract_balance = token_client.balance(&env.current_contract_address()); if contract_balance == 0 { panic_with_error!(&env, Error::NoDonations); } let admin_address: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); token_client.transfer( &env.current_contract_address(), &admin_address, &contract_balance, ); Ok(contract_balance) } ``` ### Utility functions We have some utility functions, too that aren't exposed to be invoked by the smart contract in a transaction. These functions exist so we don't have to code the same logic over and over (i.e., reading a message from the contract's storage). This is a fairly common approach to reduce contract size and make contract logic more consistent. #### `check_string_not_empty` Here, we're simply abstracting a check that is made multiple times throughout the contract. This way, we can be confident that _every_ time we want to check a string isn't empty, we're checking in the same exact way. ```rust // Make sure the provided string is not empty. fn check_string_not_empty(env: &Env, sus_string: &String) { if sus_string.is_empty() { panic_with_error!(env, Error::InvalidMessage); } } ``` #### `get_message` This function retrieves a message entry from the guestbook contract's storage entry. If the entry is found not to exist, we panic with an error message. If it is found, we return the whole message `struct`. ```rust // Read a message from persistent storage. fn get_message(env: &Env, message_id: u32) -> Message { if !env .storage() .persistent() .has(&DataKey::Message(message_id)) { panic_with_error!(env, Error::NoSuchMessage); } let message: Message = env .storage() .persistent() .get(&DataKey::Message(message_id)) .unwrap(); return message; } ``` #### `save_message` We're abstracting away the method we're using to write a message to the contract storage because it's used in two places: the `initialize` and `write_message` functions. We want both to store messages in the same manner, so we're enforcing that by using this utility function. We're storing a `MessageCount` in the contract's instance storage, to assist us in message saves, reads, edits, etc. This could certainly be done differently, but it will be convenient for us when it comes to saving new messages, reading messages from the contract, querying for contract state in the frontend, etc. ```rust // Write a message to persistent storage. fn save_message(env: &Env, message: Message) -> u32 { let mut num_messages = env .storage() .instance() .get(&DataKey::MessageCount) .unwrap_or(0 as u32); num_messages += 1; env.storage() .persistent() .set(&DataKey::Message(num_messages), &message); env.storage() .instance() .set(&DataKey::MessageCount, &num_messages); return num_messages; } ``` ### Contract types In addition to the functions above, we have some custom types written for our smart contract. These can be seen in the [`types.rs file`](https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/contracts/ye_olde_guestbook/src/types.rs) in the source code repository. #### `Message` The `Message` type is a `struct` data structure that will hold all the information about a message that's been written to the guestbook. We keep track of the message's title, text, author, and in which ledger number it was written. Some of this is not strictly necessary here, and could probably be handled outside of the smart contract (by using a data indexer, for example). For the purposes of this tutorial, however, we'll keep these messages in persistent entries in the contract storage. ```rust #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct Message { pub author: Address, pub ledger: u32, pub title: String, pub text: String, } ``` #### `DataKey` This is a struct that's used elsewhere in the contract to define the keys for the various storage entries the contract will hold. Nothing groundbreaking or remarkable here, to be honest, but it's still worth showing. The `Message(ID_NUMBER)` will be used as the key to store a `Message` struct on-chain as the corresponding value. ```rust #[contracttype] #[derive(Clone)] pub enum DataKey { Admin, MessageCount, Message(u32), } ``` #### `Error` We're holding to the typical [contract conventions](../../guides/conventions/error-enum.mdx), and creating an `enum` to keep track of our errors. ```rust #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum Error { InvalidMessage = 1, // The provided message is malformed in some way. NoSuchMessage = 2, // The message requested does not exist. UnauthorizedToEdit = 3, // Address is not allowed to edit this message. NoDonations = 4, // Contract has no donations to claim. } ``` #### Tests We've written some tests that work through many (foreseen) usage patterns for this smart contract. It's too lengthy to dive into here, but it's worth checking out the [source code](https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/contracts/ye_olde_guestbook/src/test.rs) to understand the logic of how the various contract functions are meant to work together. Up next, we'll look at how we go from this deployed contract to an NPM package that can be imported and used in a frontend project easily, and with full type-safety. --- ## Build Custom Network Ingestion Pipeline With Stellar's [Composable Data Platform](https://stellar.org/blog/developers/composable-data-platform) you can build fast, lightweight data pipelines for ledger data. --- ## CDP Consumer Pipeline Sample Code Complete code for a small sample of a consumer pipeline of Stellar network ledger metadata using the Stellar Go [Ingest SDK](overview.mdx#the-ingestion-sdk-packages) to demonstrate data pipeline from ledger metadata to derived data model with event-driven, distributed processing to sample microservice (Python script) as subscriber. This example uses the ZeroMQ [goczmq](https://github.com/zeromq/goczmq) Go wrapper SDK, which requires a few o/s [dependent libraries to also be installed on the host machine](https://github.com/zeromq/goczmq?tab=readme-ov-file#dependencies). This example requires having access to a public ledger metadata lake that is actively populated with latest ledgers from Stellar mainnet. For purposes of the example it uses a reference data lake hosted on [AWS Open Data S3](../../../data/apis/rpc/admin-guide/data-lake-integration#1-accessing-a-data-lake) ### Step# 1 - Create example directory and copy following files to your workstation. ```bash mkdir pipeline-example; cd pipeline-example ```` ### `main.go` ```go package main "context" "encoding/json" "fmt" "io" "os" "os/signal" "github.com/pelletier/go-toml" "github.com/pkg/errors" "github.com/stellar/go-stellar-sdk/amount" "github.com/stellar/go-stellar-sdk/historyarchive" "github.com/stellar/go-stellar-sdk/ingest" "github.com/stellar/go-stellar-sdk/ingest/ledgerbackend" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/support/datastore" "github.com/stellar/go-stellar-sdk/support/storage" "github.com/stellar/go-stellar-sdk/xdr" "github.com/stellar/go-stellar-sdk/support/log" "github.com/zeromq/goczmq" ) // Application payment model type AppPayment struct { Timestamp uint BuyerAccountId string SellerAccountId string AssetCode string Amount string } // application data pipeline type Message struct { Payload interface{} } type Processor interface { Process(context.Context, Message) error } type Publisher interface { Subscribe(receiver Processor) } // Ingestion Pipeline Processors type ZeroMQOutboundAdapter struct { Publisher *goczmq.Sock } func (adapter *ZeroMQOutboundAdapter) Process(ctx context.Context, msg Message) error { _, err := adapter.Publisher.Write(msg.Payload.([]byte)) return err } type AppPaymentTransformer struct { processors []Processor networkPassPhrase string } func (transformer *AppPaymentTransformer) Subscribe(receiver Processor) { transformer.processors = append(transformer.processors, receiver) } func (transformer *AppPaymentTransformer) Process(ctx context.Context, msg Message) error { ledgerCloseMeta := msg.Payload.(xdr.LedgerCloseMeta) ledgerTxReader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(transformer.networkPassPhrase, ledgerCloseMeta) if err != nil { return errors.Wrapf(err, "failed to create reader for ledger %v", ledgerCloseMeta.LedgerSequence()) } closeTime := uint(ledgerCloseMeta.LedgerHeaderHistoryEntry().Header.ScpValue.CloseTime) // scan all transactions in a ledger for payments to derive new model from counter := 0 transaction, err := ledgerTxReader.Read() for ; err == nil; transaction, err = ledgerTxReader.Read() { for _, op := range transaction.Envelope.Operations() { switch op.Body.Type { case xdr.OperationTypePayment: networkPayment := op.Body.MustPaymentOp() myPayment := AppPayment{ Timestamp: closeTime, BuyerAccountId: networkPayment.Destination.Address(), SellerAccountId: op.SourceAccount.Address(), AssetCode: networkPayment.Asset.StringCanonical(), Amount: amount.String(networkPayment.Amount), } jsonBytes, err := json.Marshal(myPayment) if err != nil { return err } for _, processor := range transformer.processors { processor.Process(ctx, Message{Payload: jsonBytes}) } counter++ } } } if err != io.EOF { return errors.Wrapf(err, "failed to read transaction from ledger %v", ledgerCloseMeta.LedgerSequence()) } log.Infof("Published %v payments from ledger sequnce %v", counter, ledgerCloseMeta.LedgerSequence()) return nil } type LedgerMetadataInboundAdapter struct { processors []Processor historyArchiveURLs []string dataStoreConfig datastore.DataStoreConfig } func (adapter *LedgerMetadataInboundAdapter) Subscribe(receiver Processor) { adapter.processors = append(adapter.processors, receiver) } func (adapter *LedgerMetadataInboundAdapter) Run(ctx context.Context) error { // Get the lastest ledger from network. historyArchive, err := historyarchive.NewArchivePool(adapter.historyArchiveURLs, historyarchive.ArchiveOptions{ ConnectOptions: storage.ConnectOptions{ UserAgent: "payment_demo", Context: ctx, }, }) if err != nil { return errors.Wrap(err, "error creating history archive client") } latestNetworkLedger, err := historyArchive.GetLatestLedgerSequence() if err != nil { return errors.Wrap(err, "error getting latest ledger") } ledgerRange := ledgerbackend.UnboundedRange(latestNetworkLedger) pubConfig := ingest.PublisherConfig{ DataStoreConfig: adapter.dataStoreConfig, BufferedStorageConfig: ingest.DefaultBufferedStorageBackendConfig(1), } log.Infof("beginning payments stream, starting at ledger %v ...\n", latestNetworkLedger) return ingest.ApplyLedgerMetadata(ledgerRange, pubConfig, ctx, func(lcm xdr.LedgerCloseMeta) error { for _, processor := range adapter.processors { if err = processor.Process(ctx, Message{Payload: lcm}); err != nil { return err } } return nil }) } func main() { // run a data pipeline that transforms Pubnet ledger metadata into payment events ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) defer stop() log.SetLevel(log.InfoLevel) cfg, err := toml.LoadFile("config.toml") if err != nil { fmt.Printf("config.toml shoule be accessible in current directdory: %v\n", err) return } datastoreConfig := datastore.DataStoreConfig{} // Unmarshal TOML data into the Config struct if err = cfg.Unmarshal(&datastoreConfig); err != nil { fmt.Printf("error unmarshalling TOML config: %v\n", err) return } // create the inbound source of pubnet ledger metadata ledgerMetadataInboundAdapter := &LedgerMetadataInboundAdapter{ historyArchiveURLs: network.PublicNetworkhistoryArchiveURLs, dataStoreConfig: datastoreConfig, } // create the app transformer to convert network data to application data model appTransformer := &AppPaymentTransformer{networkPassPhrase: network.PublicNetworkPassphrase} // create the outbound adapter, this is the end point of the pipeline // publishes application data model as messages to a broker publisher, err := goczmq.NewPub("tcp://127.0.0.1:5555") if err != nil { log.Infof("error creating 0MQ publisher: %v\n", err) return } defer publisher.Destroy() outboundAdapter := &ZeroMQOutboundAdapter{Publisher: publisher} // wire up the ingestion pipeline and let it run appTransformer.Subscribe(outboundAdapter) ledgerMetadataInboundAdapter.Subscribe(appTransformer) log.Infof("Payment ingestion pipeline ended %v\n", ledgerMetadataInboundAdapter.Run(ctx)) } ```` ### `config.toml` The CDP configuration settings, this file defines the data storage which contains the pre-generated Ledger Metadata files. The [S3 Public Blockchain](https://registry.opendata.aws/aws-public-blockchain) bucket for [Stellar Pubnet](https://aws-public-blockchain.s3.us-east-2.amazonaws.com/index.html#v1.1/stellar/ledgers/pubnet) is used in this example. ``` type = "S3" [params] destination_bucket_path = "aws-public-blockchain/v1.1/stellar/ledgers/pubnet" region = "us-east-2" # this is the schema specific to the public s3 data lake being used [schema] ledgers_per_file = 1 files_per_partition = 64000 ``` ### `distributed_payment_subsciber.py` A Python script demonstrating how we now have distributed processing and event driven architecture by leveraging the MQ Broker to push derived application payment data model out to other microservices. Make sure to `pip install pyzmq` ```python # Socket to talk to server context = zmq.Context() socket = context.socket(zmq.SUB) print("Collecting payments from pipeline ...") socket.connect("tcp://127.0.0.1:5555") socket.subscribe("") while True: message = socket.recv() json_object = json.loads(message) json_formatted_str = json.dumps(json_object, indent=2) print(f"Received payment:\n\n{json_formatted_str}") ``` ### Step# 2 - Compile and run the ingestion pipeline example. ```bash go mod init example/pipeline go get github.com/stellar/go-stellar-sdk@latest github.com/zeromq/goczmq@v4.1.0 go mod tidy go build -o pipeline ./. AWS_SHARED_CREDENTIALS_FILE=/dev/null ./pipeline ``` ### Step# 3 - Run the distributed pipeline consumer In separate terminal, run `python distributed_payment_subsciber.py`, this will perform distributed pipeline topology, as it receives messages with payment info from the pipeline process and does additional processing(printing it to console). ``` --- ## Overview(Ingest-sdk) This tutorial walks through how an application can leverage [CDP architecture](https://stellar.org/blog/developers/composable-data-platform) to create fast, lightweight Stellar Ledger Metada data pipelines using a few select packages from the Stellar Go Repo [github.com/stellar/go-stellar-sdk](https://github.com/stellar/go-stellar-sdk) collectively known as the 'Ingestion' SDK: ## The Ingestion SDK packages - `github.com/stellar/go-stellar-sdk/amount` utility package to convert prices from network transaction operations to string - `github.com/stellar/go-stellar-sdk/historyarchive` `github.com/stellar/go-stellar-sdk/support/datastore` `github.com/stellar/go-stellar-sdk/support/storage` utility package with convenient wrappers for accessing history archives, and avoid low-level http aspects - `github.com/stellar/go-stellar-sdk/ingest` provides parsing functionality over the network ledger metadata, converts to more developer-centric `LedgerTransaction` model - `github.com/stellar/go-stellar-sdk/network` provides convenient pre-configured settings for Testnet and Mainnet networks - `github.com/stellar/go-stellar-sdk/xdr` a complete Golang binding to the Stellar network data model ## Ingestion Project Setup ### Project Requirements To use this example CDP pipeline for live Stellar network transaction data, you'll need: - A developer workstation with [Go](https://go.dev/learn) programming language runtime installed - An IDE to edit Go code, [VSCode](https://code.visualstudio.com/download) is good if one is needed - A newly initialized, empty Go project folder. `mkdir pipeline; cd pipeline; go mod init example/pipeline` - Some familiarity to the [Stellar Ledger Metadata model](../../../learn/fundamentals/stellar-data-structures/README.mdx). It is defined in an IDL format expressed in [XDR encoding](https://github.com/stellar/stellar-xdr). - Docker - Google Cloud Platform account: - a bucket created in Google Cloud Storage(GCS) - GCP [credentials in workstation environment](../../../data/indexers/build-your-own/galexie/admin_guide/setup.mdx#google-cloud-platform-gcp-credentials) Our example application is only interested in a small subset of the overall network data model related to asset transfers triggered by Payment operation and defines its own derived data model as the goal of exercise: ``` ::AppPayment Timestamp: uint BuyerAccountId: string SellerAccountId: string AssetCode: string Amount: string } ``` The example application will perform both of CDP pipelines. A minimum of two pipelines are required for a complete end to end CDP architecture. ![](/assets/ingest-sdk/cdp_pipelines.png) ### Ledger Metadata Export Pipeline This pipeline needs to be initiated first, it is responsible for exporting Stellar Ledger Metadata as files to a [CDP Datastore](https://github.com/stellar/go-stellar-sdk/blob/main/support/datastore/datastore.go). #### Determine the Datastore The Datastore in CDP is an interface, allowing for multiple implementations which represent different physical storage layers that can be 'plugged in' to export and consumer pipelines. Stellar provides the [GCS Datastore] as the first Datastore implementation, and this example chooses to use this existing implementation. There will be open source contributions for implementations on other storage layers to choose from as CDP grows. If you can't find an implementation for a storage layer you would like to use, it is also possible to develop your own [ Datastore](https://github.com/stellar/go-stellar-sdk/blob/main/support/datastore/datastore.go#L17) implementation, which is beyond scope of this example, as it entails a separate learning exercise of its own, coming soon! #### Exporting network metadata to Datastore Use Galexie, a new CDP command line program for exporting network metadata to datastores. - Follow the Galexie setup steps in [Galexie User Guide](../../../data/indexers/build-your-own/galexie/admin_guide/setup.mdx), to configure specifics of GCS bucket and target network. - Follow the [Galexie docker runtime instructions](../../../data/indexers/build-your-own/galexie/admin_guide/running.mdx) to start the export. - For one time export of historical bounded range of ledgers, use `append --start --end ` - For a continuous export of prior ledgers and all new ledgers generated on network, use `append --start `. ### Ledger Metadata Consumer Pipeline A consumer pipeline retrieves files from the GCS bucket and uses them as the origin of Ledger Metadata in a data processing pipeline. There can be many separate consumer pipelines all accessing the same Datastore at stame time. Each consumer pipeline will typically perform three distinct stream processor roles: #### Inbound Adapter The 'source of origin' for the ledger metadata in a pipeline. This processor retrieves [LedgerCloseMeta](https://github.com/stellar/go-stellar-sdk/blob/main/xdr/xdr_generated.go) files from the GCS Datastore, extracts the `LedgerCloseMeta` for each Ledger and publishes it onto the messaging pipeline. The go sdk provides consumer helper function [ApplyLedgerMetadata](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/producer.go) for automated, performant, buffered retrieval of files from the remote datastore, application code can leverage this to acquire pure `LedgerCloseMeta` data from a callback function. #### Transformer Subscribes on the pipeline to receive `LedgerCloseMeta`. Uses the Go SDK package [github.com/stellar/go-stellar-sdk/xdr](https://github.com/stellar/go-stellar-sdk/tree/main/xdr) to parse the ledger meta data model for payment operations and convert those into a new instance of application data model `AppPayment` instances. Publishes `AppPayment` to the pipeline. #### Outbound Adapter Acts as the termination of the pipeline, it subscribes to receive `ApplicationPayment` and publishes the data off the pipeline and to an external data store, a ZeroMQ Publisher Socket, which is essentially a message broker. ### Summary Refer to [Ingestion Pipeline Sample Application](./ingestion-pipeline-code.mdx) for complete consumer code example, demonstrating a live, streaming pipeline against the Stellar network, processing each new ledger's metadata as it is closed on the network. --- ## Build Wallet Applications on Stellar with the Wallet SDK in four languages # Overview Stellar is an open-source distributed ledger that you can use as a backend to power various applications and services, such as wallets, payment apps, currency exchanges, micropayment services, platforms for in-game purchases, and more — check out projects being built on Stellar: [Stellar Ecosystem Projects](https://stellar.org/ecosystem/projects#Projects). Stellar has built-in logic for key storage, creating accounts, signing transactions, tracking balances, and queries to the Stellar database, and anyone can use the network to issue, store, transfer, and trade assets. This documentation includes sections on how to build applications without smart contracts with the [Wallet SDK](./wallet/overview.mdx) or the [JS SDK](./example-application-tutorial/overview.mdx), building with smart contracts with the [dapp frontend tutorial](./dapp-frontend.mdx), and all information regarding experimentation with [passkey / contract account wallets](../guides/contract-accounts/smart-wallets.mdx). ## Anchors Many Stellar assets connect to real-world currencies, and Stellar has open protocols for integrating deposits and withdrawals of these assets via the [anchor network](https://stellar.org/learn/anchor-basics). Because of this, a Stellar-based application can take advantage of real banking rails and connect to real money. Read more about anchors in our [Anchors section](../../learn/fundamentals/anchors.mdx). Set up an anchor using the [Anchor Platform](../../platforms/anchor-platform/README.mdx). Integrate MoneyGram Ramps into an existing application with the [Integrate with MoneyGram Ramps tutorial](https://developer.moneygram.com/moneygram-developer/docs/integrate-moneygram-ramps). ## Stellar Ecosystem Proposals (SEPs) Stellar-based products and services interoperate by implementing various Stellar Ecosystem Proposals (SEPs), which are publicly created, open-source documents that live in a [GitHub repository](https://github.com/stellar/stellar-protocol/tree/master/ecosystem#stellar-ecosystem-proposals-seps) and define how asset issuers, anchors, wallets, and other service providers interact with each other. As a wallet, the most important SEPs are [SEP-24: Hosted Deposit and Withdrawal](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md), and [SEP-31: Cross Border Payments API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md), [SEP-10: Stellar Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md), [SEP-12: KYC API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md), and [SEP-38: Anchor RFQ API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md). --- ## Privacy on Stellar Stellar is a public blockchain: every transaction is recorded onchain and visible to anyone. This transparency enables permissionless validation, but many real-world use cases — payroll, institutional settlement, everyday payments — require transaction privacy. To support use cases like these, the Stellar community is building configurable, compliance-ready privacy tools. From low-level cryptographic host functions to managed private payment systems, these solutions help developers protect sensitive financial information while supporting regulatory compliance and enterprise requirements. ## Privacy Pools Privacy Pools are smart contract-based systems that let users pay each other or interact with other protocols while optionally keeping their balances, transaction amounts, and addresses private on the public blockchain. Deposits and withdrawals into the pool are visible onchain, but transactions between parties within the pool do not need to be. Privacy Pools include built-in features that allow operators to implement compliance policies and processes. Association Set Providers (ASPs) can manage allow/deny lists that ensure known bad actors cannot interact within the pool, while legitimate users, merchants, and exchanges are free to transact safely without revealing unnecessary transaction history or personal information onchain to prove integrity. Some systems also include view keys that allow authorized parties to investigate suspicious transactions or respond to law enforcement requests, without sacrificing the privacy of other legitimate pool participants. - **Whitepaper** (Buterin, Illum, Nadler, Schär, Soleimani): [privacypools.com/whitepaper.pdf](https://privacypools.com/whitepaper.pdf) ### Stellar Private Payments Nethermind's ZK team has built a proof-of-concept Privacy Pools implementation for Stellar using Circom circuits, Groth16 proofs, and Stellar smart contracts. :::caution Research prototype, not audited. Do not use in production with real assets. ::: | Component | Description | | --------------------------- | -------------------------------------------- | | Pool contract | Manages deposits, transfers, and withdrawals | | Groth16 verifier | Onchain ZK proof verification | | ASP membership contract | Merkle tree of approved addresses | | ASP non-membership contract | Sparse Merkle tree for exclusion proofs | Proofs are generated client-side in the browser via WebAssembly — user secrets never leave the device. - **Repo**: [NethermindEth/stellar-private-payments](https://github.com/NethermindEth/stellar-private-payments) - **Demo**: [nethermindeth.github.io/stellar-private-payments](https://nethermindeth.github.io/stellar-private-payments/) ## Confidential Tokens Confidential Tokens let users keep token balances and transaction amounts private while keeping the sender and receiver's addresses publicly visible onchain. Confidential tokens are designed for contexts where the parties to a transaction are known, but the amounts should not be. This makes confidential tokens well-suited for payments where parties need confidentiality without sacrificing public auditability. The [Confidential Token Association](https://www.confidentialtoken.org) — whose members include the Stellar Development Foundation, Nethermind, OpenZeppelin, and Zama — is developing an open standard for encryption-based onchain confidentiality compatible with existing token interfaces. Implementation on Stellar is in progress. :::caution Confidential tokens on Stellar are a developer preview. The contracts and demo linked below are unaudited — not yet intended for production use or real assets. ::: Learn more about our work with OpenZeppelin in the post [Developer Preview: Confidential Tokens on Stellar](https://stellar.org/blog/developers/developer-preview-confidential-tokens-on-stellar), published on June 29, 2026 during the Testnet preview, and watch the [Developer Preview stream with OpenZeppelin](https://youtu.be/nfbr3KuYqPE?si=n5jX6g3-azW4Lp1B) that explains more about the architecture and design choices. - **OpenZeppelin Confidential Token Repo**: [GitHub](https://github.com/OpenZeppelin/stellar-contracts/tree/feat/confidential-verifier-ultrahonk/packages/tokens/src/confidential) - **OpenZeppelin Confidential Token Demo**: [Demo](https://stellar-confidential-token-demo.billowing-moon-0c6f.workers.dev/), [GitHub](https://github.com/brozorec/stellar-confidential-token-demo), [Video walkthrough](https://x.com/BuildOnStellar/status/2072357829353308214) - **Confidential Token Association**: [confidentialtoken.org](https://www.confidentialtoken.org) - **Confidential Token overview/demo by Jay Geng (SDF) at Meridian 2025**: [YouTube](https://www.youtube.com/watch?v=6NnDqVQYOHM) ## Onchain ZK Verifiers An onchain verifier is a smart contract that accepts a compact zero-knowledge proof and confirms its validity without re-running the original computation. These verifiers can be used to support zero-knowledge systems that protect privacy and ensure computational integrity. These verifiers build on the cryptographic host functions introduced in Stellar's Protocol 25/26 releases, making them efficient and affordable to run. ### RISC Zero (Groth16) Verifier The Stellar Private Payments prototype includes a deployable verifier contract. The contract verifies Groth16-based proofs created with RISC Zero's zkVM coprocessor (allowing developers to write programs in Rust, rather than domain-specific zero-knowledge languages, and execute them verifiably) or ZK-specific languages like Circom. - **Repo**: [NethermindEth/stellar-risc0-verifier](https://github.com/NethermindEth/stellar-risc0-verifier) - **Article**: [Verifying RISC Zero Execution in a Stellar Smart Contract](https://stellar.org/blog/developers/risc-zero-verifier) ### UltraHonk Verifier A verifier for circuits built with Aztec's Noir language and Barretenberg backend. - **Repo**: - [Ultrahonk verifier](https://github.com/NethermindEth/rs-soroban-ultrahonk) - [ultrahonk Stellar smart contract](https://github.com/indextree/ultrahonk_soroban_contract) ## ZK Cryptographic Primitives Stellar's Protocol [22](https://stellar.org/blog/developers/announcing-protocol-22), [25 ("X-Ray")](https://stellar.org/blog/developers/announcing-stellar-x-ray-protocol-25) and [26 ("Yardstick")](https://stellar.org/blog/foundation-news/stellar-yardstick-protocol-26-upgrade-guide) releases introduced native host functions into Stellar smart contracts that underpin all ZK-based privacy on Stellar: **BLS12-381** ([CAP-59](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0059.md)) – pairing-friendly elliptic curve with 128-bit security and efficient signature aggregation, enabling zk-SNARKs. **BN254** ([CAP-74](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0074.md)) — the pairing-friendly elliptic curve used by most ZK applications in production today. Adds `bn254_g1_add`, `bn254_g1_mul`, and `bn254_multi_pairing_check` host functions, mirroring Ethereum's EIP-196/197 precompiles. Existing BN254-based circuits and tooling can be ported to Stellar without modification. **Poseidon / Poseidon2** ([CAP-75](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0075.md)) — permutation primitives for building hash functions designed for ZK circuits. Far more efficient inside proofs than SHA-256, used for commitments, Merkle trees, and nullifiers. Available as host functions (`poseidon_permutation` and `poseidon2_permutation`) from which developers construct the hash function their application needs, keeping hashing logic consistent between off-chain circuits and onchain contracts. Navigate to [this page](../apps/zk.mdx) For full details and code examples. --- ## Build a Payment App with Swift ## SwiftBasicPay SwiftBasicPay is an open-source example iOS payment application that showcases how to integrate Stellar's powerful payment infrastructure into native Swift apps using the [Stellar Wallet SDK for Swift](https://github.com/Soneso/stellar-swift-wallet-sdk) and the [Stellar iOS SDK](https://github.com/Soneso/stellar-ios-mac-sdk). [SwiftBasicPay](https://github.com/Soneso/SwiftBasicPay) is maintained by a dedicated community developer, Soneso. ## Features ### Wallet Functionality - **Authentication**: PIN-based security - **Account Management**: Stellar account creation and management - **Management of Assets**: Add/remove trust lines, asset overview - **Payments**: Send and receive Stellar assets - **Path Payments**: Cross-asset payments with automatic routing ### Anchor Integration - Programatic deposits and withdrawals via anchors - KYC compliance - Interactive deposits/withdrawals with web integration SwiftBasicPay integrates multiple Stellar Ecosystem Proposals (SEPs) through the wallet SDK: - **[SEP-1](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md)**: Stellar TOML - Provides anchor metadata and service endpoints - **[SEP-10](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md)**: Web Authentication - Proves account ownership to anchors - **[SEP-6](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md)**: Deposit/Withdrawal API - Interacts with anchors programmatically - **[SEP-9](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md)**: Standard KYC Fields - Defines standard customer information fields - **[SEP-12](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md)**: KYC API - Customer information collection and verification - **[SEP-24](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md)**: Hosted Deposit/Withdrawal - Interactive web-based transfers ## Learning Topics This app demonstrates how to: 1. **Create Stellar Wallets**: Account generation, key management 2. **Implement Management of Assets**: Add and remove trustlines to Stellar assets 3. **Implement Payments**: Simple and path payments 4. **Interact with Anchors**: SEP-1/10/6/12/24 integration ## Tutorial A comprehensive tutorial walking through the development of this Stellar payment app is available in the [tutorial directory](https://github.com/Soneso/SwiftBasicPay/tree/main/tutorial) of the repository. The tutorial covers secure data storage, authentication, account creation, assets management, payments, path payments, and anchor integration with step-by-step explanations of how the Stellar SDKs are used. ## Resources - [SwiftBasicPay GitHub Repository](https://github.com/Soneso/SwiftBasicPay) - [SwiftBasicPay Tutorial](https://github.com/Soneso/SwiftBasicPay/tree/main/tutorial) - [Stellar Wallet SDK for Swift](https://github.com/Soneso/stellar-swift-wallet-sdk) - [Stellar iOS SDK](https://github.com/Soneso/stellar-ios-mac-sdk) - [Stellar Wallet SDK Documentation](../apps/wallet/README.mdx) ## Contributing Contributions to SwiftBasicPay are welcome! Please create a pull request with a clear description of your changes in the [SwiftBasicPay GitHub Repository](https://github.com/Soneso/SwiftBasicPay). --- ## Contributing guide Thank you for contributing to the Stellar Wallet documentation! To get started, please first read [main README](../../../README.md) guide. This documentation is mainly for the Wallet SDK and it's usages to integrate with various SEPs. The document is structured to be language-agnostic, but with the components listed below we can add language-specific logic into the document. Generally, text should be applicable to all of supported programming languages, but for differences special `` component can be used (read more below) ## Wallet guide components ### Header Header is a special .mdx file that should be included on all pages. It contains: - Language buttons - A general guide on using this buttons - Optional warning for languages in progress On all new pages, Header should be the first element. Optionally, provide list of languages that are work in progress: ```mdxjs
``` ### LanguageButtons This component is a part of the header. It allows to switch between programming languages. Current language is stored as a cookie. ### WalletGuideWarn This component puts a warning if language is in progress for this section. Please use `WIPLangs` property to enable it for a language for the page. ### WalletCodeExample This is improved `CodeExample` component. It currently supports dynamic switching between TypeScript, Kotlin, Flutter and Swift code snippets (depending on the user selected language). It will also generate placeholder if code snippet is missing. Here's an example on how to use it: ````mdxjs ​```kotlin // Kotlin example here ​``` ​```ts // TypeScript example here ​``` // Flutter example is replaced with an auto-generated notice ```` For a regular code examples (non Wallet SDK) please use vanilla `CodeExample` component. ### LanguageSpecific This component allows to render parts of documentation based on selected code. To get started, crete 2 files in `component` directory: ```md // ./component/kt/hello.mdx Hello, Kotlin! ``` ```md // ./component/ts/hello.mdx Hello, TypeScript! ``` Then, in the main document import both files and LanguageSpecific component: ```mdxjs // main.mdx } ts={} / ``` When user selects Kotlin, "Hello, Kotlin!" is going to be rendered, when TypeScript is selected — "Hello, TypeScript!". Finally, for Flutter and Swift, nothing would be rendered. --- ## Build a Wallet with the Wallet SDK Use the Wallet SDK to integrate with the Stellar blockchain and connect to anchors using your preferred programming language. --- ## ConfigClient ### Configuring the Client The Flutter Wallet SDK uses the standard Client from the [http package](https://pub.dev/packages/http) for all network requests (excluding Horizon, where the Flutter Stellar SDK's HTTP client is used). Optionally, you can set your own client from [http package](https://pub.dev/packages/http) to be used across the app. The client can be globally configured: ```dart // ... // init and configure your HTTP client // var myClient = ... // set as default HTTP client var appConfig = ApplicationConfiguration(defaultClient: myClient); var walletCustomClient = Wallet(StellarConfiguration.testNet, applicationConfiguration: appConfig); ``` Some [test cases](https://github.com/Soneso/stellar_wallet_flutter_sdk/tree/main/test) of this SDK use for example the `MockClient`. --- ## Install ```dart // pubspec.yaml stellar_wallet_flutter_sdk: ^1.0.6 stellar_flutter_sdk: ^2.1.3 ``` You can get the latest available version on the [project GitHub page](https://github.com/Soneso/stellar_wallet_flutter_sdk) --- ## Header :::info This guide is available on four different programming languages: Typescript, Kotlin, Flutter (Dart) and Swift. You can change the shown version on each page via the buttons above. ::: --- ## ConfigClient(Kt) ### Configuring the Client The Kotlin wallet SDK uses the [ktor client](https://ktor.io/docs/getting-started-ktor-client.html) for all network requests (excluding Horizon, where the Stellar SDK's HTTP client is used). Currently, the okhttp engine is configured to be used with the client. You can read more about how to configure the ktor client [here](https://ktor.io/docs/create-client.html#configure-client). For example, the client can be globally configured: ```kotlin val walletCustomClient = Wallet( StellarConfiguration.Testnet, ApplicationConfiguration( defaultClientConfig = { engine { this.config { this.connectTimeout(Duration.ofSeconds(10)) } } install(HttpRequestRetry) { retryOnServerErrors(maxRetries = 5) exponentialDelay() } } ) ) ``` This Kotlin code will set the connect timeout to ten seconds via the [okhttp configuration](https://ktor.io/docs/http-client-engines.html#okhttp) and also installs the [retry plugin](https://ktor.io/docs/client-retry.html). You can also specify client configuration for specific wallet SDK classes. For example, to change connect timeout when connecting to some anchor server: ```kotlin val anchorCustomClient = walletCustomClient.anchor("example.com") { engine { this.config { this.connectTimeout(Duration.ofSeconds(30)) } } } ``` ### Closing Resources After the wallet class is no longer used, it's necessary to close all clients used by it. While in some applications it may not be required (e.g. the wallet lives for the whole lifetime of the application), in other cases it can be required. If your wallet class is short-lived, it's recommended to close client resources using a close function: ```kotlin fun closeWallet() { wallet.close() } ``` --- ## GlobalSigner Finally, with the approach above we define the signer and client domain per request. If you want to define it once and use it for every authentication call your application is making, you can do so via changing the configuration: ```kotlin val appCfg = ApplicationConfiguration(WalletSigner.DomainSigner("https://my-domain.com/sign"), "my-domain.com") ``` This is particularly useful for integrating with multiple anchors. --- ## HttpConfig There is one more available configuration for a wallet that allows it to configure internal logic of the SDK. For example, to test with local servers on an HTTP protocol, HTTP can be manually enabled. ```kotlin val walletCustom = Wallet( StellarConfiguration.Testnet, ApplicationConfiguration { defaultRequest { url { protocol = URLProtocol.HTTP } } } ) ``` --- ## Install(Kt) ```kotlin // gradle.kts implementation("org.stellar:wallet-sdk:[version]") ``` You can get the latest available version on the [project GitHub page](https://github.com/stellar/kotlin-wallet-sdk) --- ## Watcher Next, let's get the channel provided by `WatcherResult` to receive events. ```kt do { val event = result.channel.receive() when (event) { is StatusChange -> println("Status changed to ${event.status}. Transaction: ${event.transaction}") is ExceptionHandlerExit -> println("Exception handler exited the job") is ChannelClosed -> println("Channel closed. Job is done") } } while (event !is ChannelClosed) ``` This code example will consume all events coming from the channel until it's closed. There are three types of events: - `StatusChange`: indicates that transaction status has changed. - `ExceptionHandlerExit`: indicates that the exception handler exited the processing loop. With default retry handler it happens when retries are exhausted. - `ChannelClosed`: indicates that the channel is closed and no more events will be emitted. This event will always fire. If `ExceptionHandlerExit` happened, channel will close right after. Otherwise, (under normal circumstances) it will stop when all transactions reach terminal statuses. :::info Events are stored in the channel until they are received, and calling the `receive()` method will block the channel until a message is received. You can read more about how channels work in the [channel documentation](https://kotlinlang.org/docs/coroutines-and-channels.html#channels). ::: --- ## GlobalSigner(Swift) Finally, with the approach above we define the signer and client domain per request. If you want to define it once and use it for every authentication call your application is making, you can do so via changing the configuration: ```swift let appCfg = AppConfig(defaultSigner: try DomainSigner(url: "https://my-domain.com/sign"), defaultClientDomain: "my-domain.com") ``` This is particularly useful for integrating with multiple anchors. --- ## Install(Swift) Add the repository (https://github.com/Soneso/stellar-swift-wallet-sdk) as a Package Dependency in your XCode project. Two new Package dependencies will appear: "stellar-wallet-sdk" and "stellarsdk". --- ## AllowHttpInfo :::info If the anchor home domain uses http, then you need to set the `allowHttp` flag when creating the anchor: ```typescript let anchor = wallet.anchor({ homeDomain: "example.com", allowHttp: true }); ``` This can only be used on Testnet. ::: --- ## ConfigClient(Ts) ### Configuring the Client The Typescript wallet SDK uses the [axios client](https://axios-http.com/docs/intro) for all network requests. You can read more about how to configure the axios client [here](https://axios-http.com/docs/instance). For example, we can configure our axios client to be globally configured with a timeout: ```typescript const customClient: AxiosInstance = axios.create({ timeout: 1000, }); let appConfig = new ApplicationConfiguration(DefaultSigner, customClient); let wal = new Wallet({ stellarConfiguration: StellarConfiguration.TestNet(), applicationConfiguration: appConfig, }); ``` You can find more [configure options here.](https://axios-http.com/docs/req_config) --- ## CreateKeypairInfo :::info If using react-native, `createKeypair` won't work. Instead use the helper method `createKeypairFromRandom` like this: ```typescript const rand = Random.randomBytes(32); const kp = account.createKeypairFromRandom(Buffer.from(rand)); ``` ::: --- ## GlobalSigner(Ts) Finally, with the approach above we define the signer and client domain per request. If you want to define it once and use it for every authentication call your application is making, you can do so via changing the configuration: ```typescript const appCfg = new ApplicationConfiguration( new DomainSigner("https://my-domain.com/sign", { ...headers }), undefined, "my-domain.com", ); ``` This is particularly useful for integrating with multiple anchors. --- ## Install(Ts) ```bash yarn add @stellar/typescript-wallet-sdk ``` --- ## The First Step to Building a Wallet App on Stellar with the Wallet SDK # Getting Started
## Installation First, you need to add the SDK dependency to your project. } ts={} flutter={} swift={} /> ## Working with the SDK Let's start with the main class that provides all SDK functionality. It's advised to have a singleton wallet object shared across the application. Creating a wallet with a default configuration connected to Stellar's Testnet is simple: ```kotlin val wallet = Wallet(StellarConfiguration.Testnet) ``` ```ts let wallet = walletSdk.Wallet.TestNet(); ``` ```dart var wallet = Wallet.testNet; ``` ```swift let wallet = Wallet.testNet ``` The wallet instance can be further configured. For example, to connect to the public network: ```kotlin val walletTestnet = Wallet(StellarConfiguration(Network.TESTNET, "https://horizon-testnet.stellar.org")) ``` ```typescript let wallet = new Wallet({ stellarConfiguration: StellarConfiguration.MainNet(), }); ``` ```dart var wallet = Wallet(StellarConfiguration.publicNet); ``` ```swift let wallet = Wallet(stellarConfig: StellarConfig.publicNet) ``` } /> } ts={} flutter={} /> ## Stellar Basics The wallet SDK provides some extra functionality on top of the existing [Horizon SDK]. For interaction with the Stellar network, the wallet SDK covers only the basics used in a typical wallet flow. For more advanced use cases, the underlying [Horizon SDK] should be used instead. To interact with the Horizon instance configured in the previous steps, simply do: ```kotlin val stellar = wallet.stellar() ``` ```typescript const stellar = wallet.stellar(); ``` ```dart var stellar = wallet.stellar(); ``` ```swift let stellar = wallet.stellar ``` This example will create a Stellar class that manages the connection to Horizon service. :::note Default configuration connects to the public Stellar Horizon instance. You can change this behavior as described [above](#working-with-the-sdk). ::: You can read more about working with the Stellar network in the [respective section](./stellar.mdx). ## Anchor Basics Primary use of the SDK is to provide an easy way to connect to anchors via sets of protocols known as SEPs. Let's look into connecting to the Stellar test anchor: ```kotlin val anchor = wallet.anchor("https://testanchor.stellar.org") ``` ```typescript let anchor = wallet.anchor({ homeDomain: "testanchor.stellar.org" }); ``` ```dart var anchor = wallet.anchor("testanchor.stellar.org") ``` ```swift let anchor = wallet.anchor(homeDomain: "testanchor.stellar.org") ``` } /> And the most basic interaction of fetching a [SEP-1]: Stellar Info File: ```kotlin suspend fun anchorToml(): TomlInfo { return anchor.getInfo() } ``` ```typescript let resp = await anchor.sep1(); ``` ```dart var resp = await anchor.sep1(); ``` ```swift let resp = try await anchor.sep1 ``` Below you can find all the SEPs the anchor class currently supports: - [SEP-1]: Stellar Info File as shown above - [SEP-10]: explored on [Stellar Authentication] section - [SEP-24]: explored on [Hosted Deposit and Withdrawal] section - [SEP-6]: explored on [Programmatic Deposit and Withdrawal] section - [SEP-12]: explored on [Providing KYC info] subsection - [SEP-38]: explored on [Quote] section Besides the SEPs supported by the anchor class the wallet SDK also has support for the [SEP-7] protocol which is explored on [URI Scheme to facilitate delegated signing] section. [horizon sdk]: /docs/tools/sdks [sep-1]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md [sep-6]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md [sep-7]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md [sep-10]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md [sep-12]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md [sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md [sep-38]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md [stellar authentication]: /docs/build/apps/wallet/sep10 [hosted deposit and withdrawal]: /docs/build/apps/wallet/sep24 [programmatic deposit and withdrawal]: /docs/build/apps/wallet/sep6 [providing kyc info]: /docs/build/apps/wallet/sep6#providing-kyc-info [quote]: /docs/build/apps/wallet/sep38 [uri scheme to facilitate delegated signing]: /docs/build/apps/wallet/sep7 --- ## Overview(Wallet)
In this guide we will use the Wallet SDK to integrate with the Stellar blockchain and connect to anchors. :::note This documentation walks you through how to build a wallet without using smart contracts. To build with smart contracts, navigate to the [Smart Contracts section](../../smart-contracts/overview.mdx). ::: --- ## Stellar Authentication
Wallets connect to anchors using a standard way of authentication via the Stellar network defined by the [SEP-10] standard. This guide will cover all ways to use SEP-10 to authenticate with an anchor. ## Creating Authentication Key :::info[Custodial wallets only] ::: First, let's create an authentication key. While you can use the same key for authentication and sending funds, it's recommended to split the responsibilities. In your application, you will have one or more fund keypairs (keypairs for the accounts that hold funds and initiate and receive transactions) and one authentication key. The authentication key is only used for authentication purposes and doesn't need to hold any funds. Note that you don't need to create an account for this keypair either. Go to the [Stellar Lab] and generate a keypair. The secret key must be handled securely, because it will be used for authentication. ## Basic Authentication Let's do a basic authentication. In this example, we will use wallet SDK to create an authentication token. First, let's create an `anchor` object to work with the anchor you are integrating with. In this example, we will be using a reference anchor implementation with the home domain `testanchor.stellar.org` ```kotlin val anchor = wallet.anchor("https://testanchor.stellar.org") ``` ```typescript const anchor = wallet.anchor({ homeDomain: "https://testanchor.stellar.org" }); ``` ```dart final anchor = wallet.anchor("testanchor.stellar.org"); ``` ```swift let anchor = wallet.anchor(homeDomain: "testanchor.stellar.org") ``` Next, authenticate with the `authKey` created earlier: ```kotlin val authKey = SigningKeyPair.fromSecret("my secret key") suspend fun getAuthToken(): AuthToken { return anchor.sep10().authenticate(authKey) } ``` ```typescript const authKey = SigningKeypair.fromSecret("my secret key"); const sep10 = await anchor.sep10(); const authToken = await sep10.authenticate({ accountKp: authKey }); ``` ```dart final authKey = SigningKeyPair.fromSecret("my secret key"); final sep10 = await anchor.sep10(); final authToken = await sep10.authenticate(authKey); ``` ```swift let authKey = try SigningKeyPair(secretKey: "my secret key") let sep10 = try await anchor.sep10 let authToken = try await sep10.authenticate(userKeyPair: authKey) ``` For non-custodial wallets, you want to use the user's private key as an `authKey`. ## Home Domain (Optional) The home domain is the optional parameter for SEP-10 authentication, when a single auth server is shared between multiple domains. Some anchors may require you to provide this argument. The SDK automatically sets the `home_domain` parameter in all SEP-10 requests. ## Client Domain (Optional) :::info[Non-custodial wallets only] ::: :::caution Some anchors may require the `client_domain` to always be present as part of the request, even for non-custodial wallets. ::: Client domain is used by anchors to verify the origin of user's request (which wallet this user is using?). This is particularly useful for anchors for integrating with non-custodial wallets. Supporting `client_domain` comes in two parts, the wallet's client and the wallet's server implementations. In this setup, we will have an extra authentication key. This key will be stored remotely on the server. Using the SEP-1 info file, the anchor will be able to query this key and verify the signature. As such, the anchor would be able to confirm that the request is coming from your wallet, belonging to wallet's `client_domain`. ### Client Side First, let's implement the client side. In this example we will connect to a remote signer that signs transactions on the endpoint `https://demo-wallet-server.stellar.org/sign` for the client domain `demo-wallet-server.stellar.org`. ```kotlin val signer = WalletSigner.DomainSigner("https://demo-wallet-server.stellar.org/sign") {} suspend fun getAuthToken(): AuthToken { return anchor .sep10() .authenticate(userKeyPair, signer, clientDomain = "demo-wallet-server.stellar.org") } ``` ```typescript const signer = new DomainSigner( "https://demo-wallet-server.stellar.org/sign", {}, ); const getAuthToken = async () => { return anchor.sep10().authenticate({ accountKp, walletSigner: signer, clientDomain: "demo-wallet-server.stellar.org", }); }; ``` ```dart final signer = DomainSigner("https://demo-wallet-server.stellar.org/sign"); final sep10 = await anchor.sep10(); final authToken = await sep10.authenticate(userKeyPair, clientDomainSigner: signer, clientDomain: "demo-wallet-server.stellar.org"); ``` ```swift let signer = try DomainSigner(url: "https://demo-wallet-server.stellar.org/sign") let sep10 = try await anchor.sep10 let authToken = try await sep10.authenticate(userKeyPair: authKey, clientDomain: "demo-wallet-server.stellar.org", clientDomainSigner: signer) ``` :::danger The demo-wallet signing endpoint is not protected for anybody to use. Your production URL must be protected, otherwise anybody could impersonate your wallet's user. ::: Let's add authentication with a bearer token. Simply update the request transformer: ```kotlin val signer = WalletSigner.DomainSigner("https://demo-wallet-server.stellar.org/sign") { bearerAuth("authToken") } ``` ```typescript const signer = new DomainSigner("https://demo-wallet-server.stellar.org/sign", { Authorization: `Bearer ${authToken}`, }); ``` ```dart Map requestHeaders = { "Authorization": "Bearer $authToken", "Content-Type": "application/json" }; var signer = DomainSigner("https://demo-wallet-server.stellar.org/sign", requestHeaders: requestHeaders); ``` ```swift let requestHeaders = ["Authorization" : "Bearer \(authToken)"] let signer = try DomainSigner(url: "https://demo-wallet-server.stellar.org/sign", requestHeaders: requestHeaders) ``` } ts={} swift={} /> ### Server Side Next, let's implement the server side. First, generate a new authentication key that will be used as a `client_domain` authentication key. Next, create a `SEP-1` toml file placed under `/.well-known/stellar.toml` with the following content: ```toml ACCOUNTS = [ "Authentication public key (address)" ] VERSION = "0.1.0" SIGNING_KEY = "Authentication public key (address)" NETWORK_PASSPHRASE = "Test SDF Network ; September 2015" ``` Don't forget to change the network passphrase for Mainnet deployment. Finally, let's add server implementation. This sample implementation uses express framework: ```javascript app.post("/sign", (req, res) => { const envelope_xdr = req.body.transaction; const network_passphrase = req.body.network_passphrase; const transaction = new Transaction(envelope_xdr, network_passphrase); if (Number.parseInt(transaction.sequence, 10) !== 0) { res.status(400); res.send("transaction sequence value must be '0'"); return; } transaction.sign(Keypair.fromSecret(SERVER_SIGNING_KEY)); res.set("Access-Control-Allow-Origin", "*"); res.status(200); res.send({ transaction: transaction.toEnvelope().toXDR("base64"), network_passphrase: network_passphrase, }); }); ``` You can see examples of both the wallet and server implementations [here](https://github.com/stellar/typescript-wallet-sdk/tree/main/%40stellar/typescript-wallet-sdk/examples/sep10). As mentioned before, this server sample implementation doesn't have any protection against unauthorized requests, so you must add authorization checks as part of the request. [sep-1]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md [sep-10]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md [stellar lab]: https://lab.stellar.org/ --- ## Hosted Deposit and Withdrawal
The [SEP-24] standard defines the standard way for anchors and wallets to interact on behalf of users. Wallets use this standard to facilitate exchanges between on-chain assets (such as stablecoins) and off-chain assets (such as fiat, or other network assets such as BTC). During the flow, a wallet makes several requests to the anchor, and finally receives an interactive URL to open in iframe. This URL is used by the user to provide an input (such as KYC) directly to the anchor. Finally, the wallet can fetch transaction information using query endpoints. ## Get Anchor Information Let's start with getting an instance of `Sep24` class, responsible for all SEP-24 interactions: ```kotlin val sep24 = anchor.sep24() ``` ```typescript const sep24 = await anchor.sep24(); ``` ```dart final sep24 = await anchor.sep24(); ``` ```swift let sep24 = anchor.sep24 ``` First, let's get the information about the anchor's support for [SEP-24]. This request doesn't require authentication, and will return generic info, such as supported currencies, and features supported by the anchor. You can get a full list of returned fields in the [SEP-24 specification](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#info). ```kt suspend fun getAnchorServices(): AnchorServiceInfo { return sep24.getServicesInfo() } ``` ```typescript const getAnchorServices = async (): Promise => { return await anchor.getServicesInfo(); }; ``` ```dart final servicesInfo = await sep24.getServiceInfo(); ``` ```swift let servicesInfo = try await sep24.info ``` ## Interactive Flows Before getting started, make sure you have connected to the anchor and received an authentication token, as described in the [Stellar Authentication] wallet guide. We will use the `authToken` object in the examples below as the [SEP-10] authentication token, obtained earlier. To initiate an operation, we need to know an asset. You may want to hard-code it, or get it dynamically from the anchor's info file, like shown below (for USDC): ```kt val asset = info.currencies.first { it.code == "USDC" }.assetId ``` ```typescript const assetCode = "USDC"; const info = await anchor.getInfo(); const currency = info.currencies.find(({ code }) => code === assetCode); if (!currency?.code || !currency?.issuer) { throw new Error( `Anchor does not support ${assetCode} asset or is not correctly configured on TOML file`, ); } const asset = new IssuedAssetId(currency.code, currency.issuer); ``` ```dart final asset = info.currencies.firstWhere((it)=>it.code=='USDC').assetId; ``` ```swift let info = try await anchor.info let asset = try info.currencies?.first(where: {$0.code == "USDC"})?.assetId ``` :::info Before starting with the deposit flow, make sure that the user account has [established a trustline](./stellar.mdx#modify-assets-trustlines) for the asset you are working with. ::: ### Basic Flow Let's start with a basic deposit: ```kt val deposit = sep24.deposit(asset, authToken) ``` ```typescript const deposit = await sep24.deposit({ assetCode: asset.code, authToken, }); ``` ```dart final deposit = await sep24.deposit(asset, authToken); ``` ```swift let deposit = try await sep24.deposit(assetId: asset, authToken: authToken) ``` As a result, you will get an [interactive response](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#deposit-and-withdraw-shared-responses) from the anchor. Open the received URL in an iframe and save the transaction ID for future reference: ```kt val url = deposit.url val id = deposit.id ``` ```typescript const url = deposit.url; const id = deposit.id; ``` ```dart final url = deposit.url; final id = deposit.id; ``` ```swift let url = deposit.url let id = deposit.id ``` Similarly to the deposit flow, a basic withdrawal flow has the same method signature and response type: ```kt val withdrawal = sep24.withdraw(asset, authToken) val url = withdrawal.url val id = withdrawal.id ``` ```typescript const withdrawal = await sep24.withdraw({ assetCode: asset.code, authToken, }); const url = withdrawal.url; const id = withdrawal.id; ``` ```dart final withdrawal = await sep24.withdraw(asset, authToken); final url = withdrawal.url; final id = withdrawal.id; ``` ```swift let withdrawal = try await sep24.withdraw(assetId: asset, authToken: authToken) let url = withdrawal.url let id = withdrawal.id ``` ### Providing KYC Info To improve the user experience, the [SEP-24] standard supports passing user KYC to the anchor via [SEP-9]. In turn, the anchor will pre-fill this information in the interactive popup. :::info While [SEP-9] supports passing binary data, the current version of the SDK doesn't offer such functionality. ::: :::note At the time, accepted [SEP-9] is not strictly typed yet. Improved typing will be offered in future versions. ::: ```kt val sep9 = mapOf("email_address" to "mail@example.com") val deposit = sep24.deposit(asset, authToken, sep9) ``` ```typescript const deposit = await sep24.deposit({ assetCode: asset.code, authToken, extraFields: { email_address: "mail@example.com" }, }); ``` ```dart final deposit = await sep24.deposit(asset, authToken, extraFields: {"email_address": "mail@example.com"}); ``` ```swift let deposit = try await sep24.deposit(assetId: asset, authToken: authToken, extraFields: ["email_address" : "mail@example.com"]) ``` ### Changing Stellar Transfer Account By default, the Stellar transfer will be sent to the authenticated account (with a memo) that initiated the deposit. While in most cases it's acceptable, some wallets may split their accounts. To do so, pass additional account (and optionally a memo): ```kt suspend fun depositDifferentAccount(): InteractiveFlowResponse { val recipientAccount = "G..." val memo = "my memo" to MemoType.TEXT return sep24.deposit(asset, authToken, destinationAccount = recipientAccount, destinationMemo = memo) } ``` ```typescript const recipientAccount = "G..."; const depositDifferentAccount = async (): Promise => { return await sep24.deposit({ destinationAccount: recipientAccount, destinationMemo: new Memo(MemoText, "some memo"), assetCode: asset.code, authToken, }); }; ``` ```dart final recipientAccount = "G..."; final deposit = await sep24.deposit(asset, authToken, destinationAccount: recipientAccount, destinationMemo: "my memo", destinationMemoType: MemoType.text); ``` ```swift let recipientAccount = "G..." let deposit = try await sep24.deposit(assetId: asset, authToken: authToken, destinationAccount: recipientAccount, destinationMemo: "my memo", destinationMemoType: MemoType.text) ``` Similarly, for a withdrawal, the origin account of the Stellar transaction could be changed: ```kt val originAccount = "G..." val withdrawal = sep24.withdraw(asset, authToken, withdrawalAccount = originAccount) ``` ```typescript const originAccount = "G..."; const withdrawal = await sep24.withdraw({ withdrawalAccount: originAccount, assetCode: asset.code, authToken, }); ``` ```dart final originAccount = "G..."; final withdrawal = await sep24.withdraw(asset, authToken, withdrawalAccount: originAccount); ``` ```swift let originAccount = "G..." let withdrawal = try await sep24.withdraw(assetId: asset, authToken: authToken, withdrawalAccount: originAccount) ``` ## Getting Transaction Info On the typical flow, the wallet would get transaction data to notify users about status updates. This is done via the [SEP-24] `GET /transaction` and `GET /transactions` endpoint. Alternatively, some anchors support webhooks for notifications. Note that this feature is not widely adopted yet. ### Tracking Transaction Let's look into how to use the wallet SDK to track transaction status changes. We will use `Watcher` class for this purpose. First, let's initialize watcher and start tracking a transaction. ```kt val watcher = sep24.watcher() val result = watcher.watchOneTransaction(authToken, "transaction id") ``` ```typescript const watcher = sep24.watcher(); const { stop, refresh } = watcher.watchOneTransaction({ authToken, assetCode: asset.code, id: successfulTransaction.id, onMessage, onSuccess, onError, }); ``` ```dart final watcher = sep24.watcher(); final result = watcher.watchOneTransaction(authToken, "transaction id"); ``` ```swift let watcher = sep24.watcher() let result = watcher.watchOneTransaction(authToken: authToken, id: "transaction id") ``` Alternatively, we can track multiple transactions for the same asset. ```kt val watcher = sep24.watcher() val result = watcher.watchAsset(getAuthToken(), asset) ``` ```typescript const watcher = sep24.watcher(); const { stop, refresh } = watcher.watchAllTransactions({ authToken, assetCode: asset.code, onMessage, onError, }); ``` ```dart final watcher = sep24.watcher(); final result = watcher.watchAsset(authToken, asset); ``` ```swift let watcher = sep24.watcher() let result = watcher.watchAsset(authToken: authToken, asset: asset) ``` } /> ### Fetching Transaction While `Watcher` class offers powerful tracking capabilities, sometimes it's required to just fetch a transaction (or transactions) once. The `Anchor` class allows you to fetch a transaction by ID, Stellar transaction ID, or external transaction ID: ```kt // "id" is the actual Anchor transaction id, all transactions should have it. val transaction = sep24.getTransactionBy(authToken, id = "transaction id") // "stellarTransactionId" (aka "stellar_transaction_id" on the SEP spec) // is the hash of the Stellar network transaction payment related to this // Anchor transaction. // The "stellarTransactionId" has a SHA256 hash format like the below: // - "a35135d8ed4b29b66d821444f6760f8ca1e77bea1fb49541bebeb2c3d844364a" // E.g. we'll only have this transaction id field AFTER the wallet sends funds // to Anchor on the withdrawal flow or receives funds from Anchor on the // deposit flow. val transaction = sep24.getTransactionBy(authToken, stellarTransactionId = "transaction id") // "externalTransactionId" (aka "external_transaction_id" on the SEP spec) // could refer to some ID of transaction on external network. // E.g. this could be some "reference number" displayed to the user on // the last step of the Interactive Flow UI which the user could use in some // other external place to complete the deposit or withdraw operation. val transaction = sep24.getTransactionBy(authToken, externalTransactionId = "transaction id") ``` ```typescript const transaction = await sep24.getTransactionBy({ authToken, id: transactionId, }); // "id" is the actual Anchor transaction id, all transactions should have it. const transaction = await anchor.sep24().getTransactionBy({ authToken, id: transactionId, }); // "stellarTransactionId" (aka "stellar_transaction_id" on the SEP spec) // is the hash of the Stellar network transaction payment related to this // Anchor transaction. // The "stellarTransactionId" has a SHA256 hash format like the below: // - "a35135d8ed4b29b66d821444f6760f8ca1e77bea1fb49541bebeb2c3d844364a" // E.g. we'll only have this transaction id field AFTER the wallet sends funds // to Anchor on the withdrawal flow or receives funds from Anchor on the // deposit flow. const transaction = await anchor.sep24().getTransactionBy({ authToken, stellarTransactionId, }); // "externalTransactionId" (aka "external_transaction_id" on the SEP spec) // could refer to some ID of transaction on external network. // E.g. this could be some "reference number" displayed to the user on // the last step of the Interactive Flow UI which the user could use in some // other external place to complete the deposit or withdraw operation. const transaction = await anchor.sep24().getTransactionBy({ authToken, externalTransactionId, }); ``` ```dart final transaction = await sep24.getTransaction("transaction id", authToken); ``` ```swift // "transactionId" is the actual Anchor transaction id, all transactions should have it. let transaction = try await anchor.sep24.getTransactionBy(authToken: authToken, transactionId: "transaction id") // "stellarTransactionId" (aka "stellar_transaction_id" on the SEP spec) // is the hash of the Stellar network transaction payment related to this // Anchor transaction. // The "stellarTransactionId" has a SHA256 hash format like the below: // - "a35135d8ed4b29b66d821444f6760f8ca1e77bea1fb49541bebeb2c3d844364a" // E.g. we'll only have this transaction id field AFTER the wallet sends funds // to Anchor on the withdrawal flow or receives funds from Anchor on the // deposit flow. let transaction = try await anchor.sep24.getTransactionBy(authToken: authToken, stellarTransactionId: "stellar transaction id") // "externalTransactionId" (aka "external_transaction_id" on the SEP spec) // could refer to some ID of transaction on external network. // E.g. this could be some "reference number" displayed to the user on // the last step of the Interactive Flow UI which the user could use in some // other external place to complete the deposit or withdraw operation. let transaction = try await anchor.sep24.getTransactionBy(authToken: authToken, externalTransactionId: "external transaction id") ``` It's also possible to fetch transaction by the asset ```kt val transactions = sep24.getTransactionsForAsset(asset, authToken) ``` ```typescript const transactions = await sep24.getTransactionsForAsset({ authToken, assetCode: asset.code, }); ``` ```dart final transactions = await sep24.getTransactionsForAsset(asset, authToken); ``` ```swift let transactions = try await sep24.getTransactionsForAsset(authToken: authToken, asset: asset) ``` ## Submitting Withdrawal Transfer Previously, we took a look at starting the withdrawal flow. Now, let's take a look at a full example. First, start the withdrawal: ```kt val withdrawal = sep24.withdraw(asset, authToken) ``` ```typescript const withdrawal = await sep24.withdraw({ assetCode: asset.code, authToken, }); ``` ```dart final withdrawal = await sep24.withdraw(asset, authToken); ``` ```swift let withdrawal = try await sep24.withdraw(assetId: asset, authToken: authToken) ``` Next, open an interactive url : ```kt val url = withdrawal.url // open the url ``` ```typescript const url = withdrawal.url; // open the url ``` ```dart final url = withdrawal.url // open the url ``` ```swift let url = withdrawal.url // open the url ``` After that we need to wait until the anchor is ready to receive funds. To do so, we will be waiting until transaction reaches `pending_user_transfer_start` status. This code uses a simple watching (polling) mechanism with no bail-out condition. The application’s code should be more robust. ```kt val withdrawalWatcher = sep24.watcher().watchOneTransaction(authToken, withdrawal.id) var statusChange: StatusUpdateEvent // Wait for user input do { statusChange = withdrawalWatcher.channel.receive() } while ( ((statusChange as? StatusChange) ?: throw Exception("Channel unexpectedly closed")) .status != TransactionStatus.PENDING_USER_TRANSFER_START ) ``` ```typescript const watcher = sep24.watcher(); const onMessage = (transaction) => { if (transaction.status === "pending_user_transfer_start") { // begin transfer code } }; const onSuccess = (transaction) => { // transaction comes back as completed / refunded / expired }; const onError = (transaction) => { // runtime error, or the transaction comes back as // no_market / too_small / too_large / error }; // We can watch for a particular transaction. const { refresh, stop } = watcher.watchOneTransaction({ authToken, assetCode: asset.code, id: successfulTransaction.id, onMessage, onSuccess, onError, }); // Or watch for ALL transactions of a particular asset. const { refresh, stop } = watcher.watchAllTransactions({ authToken, assetCode: asset.code, onMessage, onError, }); ``` ```dart final withdrawalWatcher = sep24.watcher().watchOneTransaction(authToken, withdrawal.id); withdrawalWatcher.controller.stream.listen( (event) { if (event is StatusChange && TransactionStatus.pendingUserTransferStart == event.status) { // begin transfer } }, onError: (error) { // handle error }, ); ``` ```swift let watcher = anchor.sep24.watcher() let result = watcher.watchOneTransaction(authToken: authToken, id: withdrawal.id) NotificationCenter.default.addObserver(self, selector: #selector(handleEvent(_:)), name: result.notificationName, object: nil) /// ... @objc public func handleEvent(_ notification: Notification) { if let statusChange = notification.object as? StatusChange { print("Status change to \(statusChange.status.rawValue). Transaction: \(statusChange.transaction.id)") if statusChange.status == TransactionStatus.pendingUserTransferStart { // begin transfer } } else if let _ = notification.object as? ExceptionHandlerExit { print("Exception handler exited the job") } else if let _ = notification.object as? NotificationsClosed { print("Notifications closed. Job is done") } } ``` Next, sign and submit the Stellar transfer: ```kt val anchorTransaction = (statusChange.transaction as WithdrawalTransaction) val transfer = stellar.transaction(keypair).transferWithdrawalTransaction(anchorTransaction, asset).build() transfer.sign(keypair) stellar.submitTransaction(transfer) ``` ```typescript // Import Horizon to get the result codes for error handling // This creates a transaction builder which we'll be using to assemble // our transfer withdrawal transaction as shown below. const txBuilder = await stellar.transaction({ sourceAddress: keypair, baseFee: 10000, // this is 0.001 XLM timebounds: 180, // in seconds }); // We can use the transaction object received on the onMessage callback from // the watcher, or, we can also fetch the transaction object using either // getTransactionBy or getTransactionsForAsset as illustrated in previous step. onMessage: (transaction) => { if (transaction.status === "pending_user_transfer_start") { // Use the builder to assemble the transfer transaction. Behind the scenes // it extracts the Stellar account (withdraw_anchor_account), memo (withdraw_memo) // and amount (amount_in) to use in the Stellar payment transaction that will // be submitted to the Stellar network. const transferTransaction = txBuilder .transferWithdrawalTransaction(transaction, asset) .build(); // Signs it with the account key pair transferTransaction.sign(keypair); // Finally submits it to the Stellar network. This stellar.submitTransaction() // function handles '504' status codes (timeout) by keep retrying it until // submission succeeds or we get a different error. try { const response = await stellar.submitTransaction(transferTransaction); console.log("Stellar-generated transaction ID: ", response.id); } catch (error) { /* In case it's not a 504 (timeout) error, the application could try some resolution strategy based on the error kind. On Stellar docs you can find a page dedicated to error handling: https://developers.stellar.org/docs/learn/encyclopedia/errors-and-debugging And status/result codes: https://developers.stellar.org/docs/data/apis/horizon/api-reference/errors */ // Let's illustrate here how we could handle an 'invalid sequence number' error. // We can access all possible result codes through Horizon's API. const sdkResultCodes = Horizon.HorizonApi.TransactionFailedResultCodes; // We can access error's response data to check for useful error details. const errorData = error.response?.data; /* Sample of errorData object returned by the Wallet SDK: { type: 'https://stellar.org/horizon-errors/transaction_failed', title: 'Transaction Failed', status: 400, detail: 'The transaction failed when submitted to the Stellar network. The `extras.result_codes` field on this response contains further details. Descriptions of each code can be found at: https://developers.stellar.org/docs/data/apis/horizon/api-reference/errors/http-status-codes/horizon-specific/transaction-failed', extras: { envelope_xdr: 'AAAAAgAAAADBjF7n9gfByOwlnyaJH...k4BRagf/////////8AAAAAAAAAAA==', result_codes: { transaction: 'tx_bad_seq' }, result_xdr: 'AAAAAAAAAGT////6AAAAAA==' } } */ /* Example scenario: invalid sequence numbers. These errors typically occur when you have an outdated view of an account. This could be because multiple devices are using this account, you have concurrent submissions happening, or other reasons. The solution is relatively simple: retrieve the account details and try again with an updated sequence number. */ if ( errorData?.status == 400 && errorData?.extras?.result_codes?.transaction === sdkResultCodes.TX_BAD_SEQ ) { // Creating a new transaction builder means retrieving an updated sequence number. const txBuilder2 = await stellar.transaction({ sourceAddress: keypair, baseFee: 10000, timebounds: 180, }); // ... // Repeat all the steps until submitting the transaction again. // ... const response2 = await stellar.submitTransaction(transferTransaction); console.log( "Stellar-generated transaction ID on retry: ", response2.id, ); // The application should take care to not resubmit the same transaction // blindly with an updated sequence number as it could result in more than // one payment being made when only one was intended. } } } }; ``` ```dart final tx = event.transaction as WithdrawalTransaction; final paymentBuilder = flutter_sdk.PaymentOperationBuilder( tx.withdrawAnchorAccount!, flutter_sdk.Asset.createNonNativeAsset(asset.code, asset.issuer), tx.amountIn!, ); final transactionBuilder = flutter_sdk.TransactionBuilder(sourceAccount) ..addOperation(paymentBuilder.build()); flutter_sdk.Memo? memo; if ("text" == tx.withdrawalMemoType) { memo = flutter_sdk.MemoText(tx.withdrawalMemo!); } else if ("hash" == tx.withdrawalMemoType) { memo = flutter_sdk.MemoHash(base64Decode(tx.withdrawalMemo!)); } // ... etc. if (memo != null) { transactionBuilder.addMemo(memo); } flutter_sdk.KeyPair kp = flutter_sdk.KeyPair.fromSecretSeed(userKeyPair.secretKey); final transaction = transactionBuilder.build()..sign(kp, network); final paymentResult = await sdk.submitTransaction(transaction); ``` ```swift if let tx = statusChange.transaction as? WithdrawalTransaction { let paymentOperation = try PaymentOperation(sourceAccountId: sourceAccountId, destinationAccountId: tx.withdrawAnchorAccount!, asset: asset.toAsset(), amount: Decimal(string:tx.amountIn!)!) var memo:Memo? = nil if "text" == tx.withdrawalMemoType { memo = Memo.text(tx.withdrawalMemo!) } else if "hash" == tx.withdrawalMemoType { memo = Memo.hash(Data(base64Encoded: tx.withdrawalMemo!)!) } // ...etc let transaction = try Transaction(sourceAccount: sourceAccount, operations: [paymentOperation], memo: memo) let keypair = try KeyPair(secretSeed: userKeyPair.secretKey) try transaction.sign(keyPair: keypair, network: Network.public) let server = wallet.stellar.server let paymentResult = await server.transactions.submitTransaction(transaction: transaction) } ``` Where `keypair` is the SEP-10 authenticated account. If you want to transfer funds from a different address, refer to [Changing Stellar Transfer Account](#changing-stellar-transfer-account) section. Code for submitting transactions to Stellar should be developed thoughtfully. The SDF has a documentation page dedicated to [submitting transactions and handling errors gracefully]. Here are a few things you need to keep in mind: - Offer a high fee. Your fee should be as high as you would offer before deciding the transaction is no longer worth sending. Stellar will only charge you the minimum necessary to be included in the ledger -- you won't be charged the amount you offer unless everyone else is offering the same amount or greater. Otherwise, you’ll pay the smallest fee offered in the set of transactions included in the ledger. - Set a maximum timebound on the transaction. This ensures that if your transaction is not included in a ledger before the set time, you can reconstruct the transaction with a higher offered fee and submit it again with better chances of inclusion. - Resubmit the transaction when you get 504 status codes. 504 status codes are just telling you that your transaction is still pending -- not that it has been canceled or that your request was invalid. You should simply make the request again with the same transaction to get a final status (either included or expired). Finally, let's track transaction status updates. In this example we simply check if the transaction has been completed: ```kt var terminalStatus: TransactionStatus? = null do { statusChange = withdrawalWatcher.channel.receive() when (statusChange) { is StatusChange -> { if (statusChange.status.isTerminal()) { terminalStatus = statusChange.status } } is ChannelClosed -> println("Transaction tracking finished") is ExceptionHandlerExit -> println("Retries exhausted trying obtain transaction data, giving up.") } } while (statusChange !is ChannelClosed) if (terminalStatus != TransactionStatus.COMPLETED) { println("Transaction was not completed") } ``` ```typescript const watcher = sep24.watcher(); const onSuccess = (transaction) => { // transaction came back as completed / refunded / expired console.log("Transaction is completed"); }; const onError = (transaction) => { // runtime error, or the transaction comes back as // no_market / too_small / too_large / error }; const { refresh, stop } = watcher.watchOneTransaction({ authToken, assetCode: asset.code, id: successfulTransaction.id, onMessage, onSuccess, onError, }); ``` ```dart final watcher = sep24.watcher().watchOneTransaction(authToken, withdrawal.id); watcher.controller.stream.listen( (event) { if (event is StatusChange && event.status.isTerminal()) { if (TransactionStatus.completed != event.status) { print("Transaction was not completed!"); } else { print("Success"); } } else if (event is ExceptionHandlerExit) { print("Retries exhausted trying obtain transaction data, giving up."); } else if (event is StreamControllerClosed) { print("Transaction tracking finished"); } }, onError: (error) { // handle error }, ); ``` ```swift let watcher = anchor.sep24.watcher() let result = watcher.watchOneTransaction(authToken: authToken, id: withdrawal.id) NotificationCenter.default.addObserver(self, selector: #selector(handleEvent(_:)), name: result.notificationName, object: nil) /// ... @objc public func handleEvent(_ notification: Notification) { if let statusChange = notification.object as? StatusChange { if statusChange.status != TransactionStatus.completed { print("Transaction was not completed!") } else { print("Success") } } else if let _ = notification.object as? ExceptionHandlerExit { print("Exception handler exited the job") } else if let _ = notification.object as? NotificationsClosed { print("Notifications closed. Job is done") } } ``` [sep-9]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md [sep-10]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md [sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md [stellar authentication]: ./sep10.mdx [submitting transactions and handling errors gracefully]: ../../../data/apis/horizon/api-reference/errors/error-handling.mdx --- ## Recovery
The [Sep-30] standard defines the standard way for an individual (e.g., a user or wallet) to regain access to their Stellar account after losing its private key without providing any third party control of the account. During this flow the wallet communicates with one or more recovery signer servers to register the wallet for a later recovery if it's needed. ## Create Recoverable Account First, let's create an account key, a device key, and a recovery key that will be attached to the account. ```typescript const accountKp = wallet.stellar().account().createKeypair(); const deviceKp = wallet.stellar().account().createKeypair(); const recoveryKp = wallet.stellar().account().createKeypair(); ``` ```kotlin val accountKp = wallet.stellar().account().createKeyPair() val deviceKp = wallet.stellar().account().createKeyPair() val recoveryKp = wallet.stellar().account().createKeyPair() ``` ```dart var accountKp = wallet.stellar().account().createKeyPair(); var deviceKp = wallet.stellar().account().createKeyPair(); var recoveryKp = wallet.stellar().account().createKeyPair(); ``` ```swift let accountKp = wallet.stellar.account.createKeyPair() let deviceKp = wallet.stellar.account.createKeyPair() let recoveryKp = wallet.stellar.account.createKeyPair() ``` The `accountKp` is the wallet's main account. The `deviceKp` we will be adding to the wallet as a signer so a device (eg. a mobile device a wallet is hosted on) can take control of the account. And the `recoveryKp` will be used to identify the key with the recovery servers. Next, let's identify the recovery servers and create our recovery object: ```typescript const server1Key = "server1"; const server1 = { endpoint: "recovery-example.com", authEndpoint: "auth-example.com", homeDomain: "test-domain", }; const server2Key = "server2"; const server2 = { endpoint: "recovery-example2.com", authEndpoint: "auth-example2.com", homeDomain: "test-domain2", }; const recovery = wallet.recovery({ servers: { [server1Key]: server1, [server2Key]: server2 }, }); ``` ```kotlin val first = RecoveryServerKey("first") val second = RecoveryServerKey("second") val firstServer = RecoveryServer("recovery.example.com", "auth.example.com", "example.com") val secondServer = RecoveryServer("recovery2.example.com", "auth2.example.com", "example.com") val servers = mapOf(first to firstServer, second to secondServer) val recovery = wallet.recovery(servers) ``` ```dart var first = RecoveryServerKey("first"); var second = RecoveryServerKey("second"); var firstServer = RecoveryServer("https://recovery.example1.com", "https://auth.example1.com", "recovery.example1.com"); var secondServer = RecoveryServer("https://recovery.example2.com", "https://auth.example2.com", "recovery.example2.com"); var servers = {first:firstServer, second:secondServer}; var recovery = wallet.recovery(servers); ``` ```swift let first = RecoveryServerKey(name: "first") let second = RecoveryServerKey(name: "second") let firstServer = RecoveryServer(endpoint:"https://recovery.example1.com", authEndpoint:"https://auth.example1.com", homeDomain:"recovery.example1.com") let secondServer = RecoveryServer(endpoint:"https://recovery.example2.com", authEndpoint:"https://auth.example2.com", homeDomain:"recovery.example2.com") let servers = [first: firstServer, second:secondServer] let recovery = wallet.recovery(servers: servers) ``` Next, we need to define SEP-30 identities. In this example we are going to create an identity for both servers. Registering an identity tells the recovery server what identities are allowed to access the account. ```typescript const identity1 = { role: RecoveryRole.OWNER, authMethods: [ { type: RecoveryType.STELLAR_ADDRESS, value: recoveryKp.publicKey, }, ], }; const identity2 = { role: RecoveryRole.OWNER, authMethods: [ { type: RecoveryType.EMAIL, value: "my-email@example.com", }, ], }; ``` ```kotlin val identity1 = listOf(RecoveryAccountIdentity( RecoveryRole.OWNER, listOf(RecoveryAccountAuthMethod(RecoveryType.STELLAR_ADDRESS, recoveryKp.address)) ) ) val identity2 = listOf(RecoveryAccountIdentity( RecoveryRole.OWNER, listOf(RecoveryAccountAuthMethod(RecoveryType.EMAIL, "my-email@example.com")) ) ) ``` ```dart var identity1 = [ RecoveryAccountIdentity(RecoveryRole.owner, [ RecoveryAccountAuthMethod(RecoveryType.stellarAddress, recoveryKp.address) ]) ]; var identity2 = [ RecoveryAccountIdentity(RecoveryRole.owner, [RecoveryAccountAuthMethod(RecoveryType.email, "my-email@example.com")]) ]; ``` ```swift let identity1 = [ RecoveryAccountIdentity(role:RecoveryRole.owner, authMethods: [RecoveryAccountAuthMethod(type:RecoveryType.stellarAddress, value:recoveryKp.address)]) ] let identity2 = [ RecoveryAccountIdentity(role:RecoveryRole.owner, authMethods: [RecoveryAccountAuthMethod(type:RecoveryType.email, value:"my-email@example.com")]) ] ``` Here, stellar key and email are used as recovery methods. Other recovery servers may support phone as a recovery method as well. You can read more about SEP-30 identities [here](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0030.md#common-request-fields) Next, let's create a recoverable account: ```typescript const config = { accountAddress: accountKp, deviceAddress: deviceKp, accountThreshold: { low: 10, medium: 10, high: 10 }, accountIdentity: { [server1Key]: [identity1], [server2Key]: [identity2] }, signerWeight: { device: 10, recoveryServer: 5 }, }; const recoverableWallet = await recovery.createRecoverableWallet(config); ``` ```kotlin val recoverableWallet = recovery.createRecoverableWallet( RecoverableWalletConfig( accountKp, deviceKp, AccountThreshold(10, 10, 10), mapOf(first to identity1, second to identity2), SignerWeight(10, 5) ) ) ``` ```dart var recoverableWallet = await recovery.createRecoverableWallet( RecoverableWalletConfig( accountKp, deviceKp, AccountThreshold(10, 10, 10), {first: identity1, second: identity2}, SignerWeight(10, 5) ) ); ``` ```swift let config = RecoverableWalletConfig(accountAddress: accountKp, deviceAddress: deviceKp, accountThreshold: AccountThreshold(low: 10, medium: 10, high: 10), accountIdentity: [first : identity1, second: identity2], signerWeight: SignerWeight(device: 10, recoveryServer: 5)) let recoverableWallet = try await recovery.createRecoverableWallet(config: config) ``` With the given parameters, this function will create a transaction that will: 1. Set `deviceKp` as the primary account key. Please note that the master key belonging to `accountKp` will be locked. `deviceKp` should be used as a primary signer instead. 2. Set all operation thresholds to 10. You can read more about threshold in the [documentation](../../../learn/fundamentals/transactions/signatures-multisig.mdx#thresholds) 3. Use identities that were defined earlier on both servers. (That means, both server will accept SEP-10 authentication via `recoveryKp` as an auth method) 4. Set device key weight to 10, and recovery server weight to 5. Given these account thresholds, both servers must be used to recover the account, as transaction signed by one will only have weight of 5, which is not sufficient to change account key. Finally, sign and submit transaction to the network: ```typescript recoverableWallet.transaction.sign(accountKp.keypair); await stellar.submitTransaction(recoverableWallet.transaction); ``` ```kotlin val tx = recoverableWallet.transaction.sign(accountKp) wallet.stellar().submitTransaction(tx) ``` ```dart var transaction = recoverableWallet.transaction; transaction.sign(accountKp.keyPair, flutter_sdk.Network.TESTNET); await wallet.stellar().submitTransaction(transaction); ``` ```swift let transaction = recoverableWallet.transaction try transaction.sign(keyPair: accountKp.keyPair, network: Network.testnet) try await wallet.stellar.submitTransaction(signedTransaction: transaction) ``` ## Get Account Info You can fetch account info from one or more servers. To do so, first we need to authenticate with a recovery server using the SEP-10 authentication method: ```typescript const authToken = await recovery .sep10Auth(server1Key) .authenticate({ accountKp: recoveryKp }); ``` ```kotlin val auth1 = recovery.sep10Auth(first).authenticate(recoveryKp) ``` ```dart var sep10 = await recovery.sep10Auth(first); var authToken = await sep10.authenticate(recoveryKp); ``` ```swift let sep10 = try await recovery.sep10Auth(key: first) let authToken = try await sep10.authenticate(userKeyPair: recoveryKp) ``` Next, get account info using auth tokens: ```typescript const accountResp = await recovery.getAccountInfo(accountKp, { [server1Key]: authToken, }); ``` ```kotlin val accountInfo = recovery.getAccountInfo(account, mapOf(first to auth1)) println("Recoverable info: $accountInfo") ``` ```dart var accountInfo = await recovery.getAccountInfo(accountKp, {first: authToken.jwt}); ``` ```swift let accountInfo = try await recovery.getAccountInfo(accountAddress: accountKp, auth: [first:auth1Token.jwt]) ``` Our second identity uses an email as an auth method. For that we can't use a [SEP-10] auth token for that server. Instead we need to use a token that ties the email to the user. For example, Firebase tokens are a good use case for this. To use this, the recovery signer server needs to be prepared to handle these kinds of tokens. Getting account info using these tokens is the same as before. ```typescript // get token from firebase const firebaseToken = AuthToken.from() const accountResp = await recovery.getAccountInfo(accountKp, { [server2Key]: firebaseToken, }); ``` ```kotlin // get token from firebase val firebaseToken = AuthToken.from() val accountInfo = recovery.getAccountInfo(account, mapOf(second to firebaseToken)) println("Recoverable info: $accountInfo") ``` ```dart var accountInfo = await recovery.getAccountInfo(accountKp, {second: }); ``` ```swift let accountInfo = try await recovery.getAccountInfo(accountAddress: accountKp, auth: [second:]) ``` ## Recover Wallet Let's say we've lost our device key and need to recover our wallet. First, we need to authenticate with both recovery servers: ```typescript const authToken1 = await recovery .sep10Auth(server1Key) .authenticate({ accountKp: recoveryKp }); // get firebase token using firebase const firebaseToken = AuthToken.from() ``` ```kotlin val auth1 = recovery.sep10Auth(first).authenticate(recoveryKp) // get firebase token using firebase val firebaseToken = AuthToken.from() ``` ```dart var sep10 = await recovery.sep10Auth(first); var authToken = await sep10.authenticate(recoveryKp); var auth1 = authToken.jwt; var auth2 = "..."; // get other token e.g. firebase token ``` ```swift let sep10 = try await recovery.sep10Auth(key: first) let authToken = try await sep10.authenticate(userKeyPair: recoveryKp) let auth1 = authToken.jwt let auth2 = "..."; // get other token e.g. firebase token ``` We need to know the recovery signer addresses that will be used to sign the transaction. You can get them from either the recoverable wallet object we created earlier (`recoverableWallet.signers`), or via fetching account info from recovery servers. ```typescript const recoverySignerAddress1 = recoverableWallet.signers[0]; const recoverySignerAddress2 = recoverableWallet.signers[1]; ``` ```kotlin val recoverySigners = recoverableWallet.signers ``` ```dart var recoverySigners = recoverableWallet.signers; ``` ```swift let recoverySigners = recoverableWallet.signers ``` Next, create a new device key and retrieve a signed transaction that replaces the device key: ```typescript const newDeviceKp = accountService.createKeypair(); const serverAuth = { [server1Key]: { signerAddress: recoverySignerAddress1, authToken1, }, [server2Key]: { signerAddress: recoverySignerAddress2, firebaseToken, }, }; const recoverTxn = await recovery.replaceDeviceKey( accountKp, newDeviceKp, serverAuth, ); ``` ```kotlin val newKey = wallet.stellar().account().createKeyPair() val serverAuth = mapOf( first to RecoveryServerSigning(recoverySigners[0], auth1), second to RecoveryServerSigning(recoverySigners[1], firebaseToken) ) val signedReplaceKeyTransaction = recovery.replaceDeviceKey( accountKp, newKey, serverAuth ) ``` ```dart var newKey = wallet.stellar().account().createKeyPair(); var serverAuth = { first: RecoveryServerSigning(recoverySigners[0], auth1), second: RecoveryServerSigning(recoverySigners[1], auth2) }; var signedReplaceKeyTx = await recovery.replaceDeviceKey(accountKp, newKey, serverAuth); ``` ```swift let newKey = wallet.stellar.account.createKeyPair() let serverAuth = [ first: RecoveryServerSigning(signerAddress: recoverySigners[0] , authToken: auth1), second: RecoveryServerSigning(signerAddress: recoverySigners[1] , authToken: auth2), ] let signedReplaceKeyTx = try await recovery.replaceDeviceKey(account: accountKp, newKey: newKey, serverAuth: serverAuth) ``` Calling this function will create a transaction that locks the previous device key and replaces it with your new key (having the same weight as the old one). Both recovery signers will have signed the transaction. The lost device key is deduced automatically if not given. A signer will be considered a device key, if one of these conditions matches: 1. It's the only signer that's not in `serverAuth`. 2. All signers in `serverAuth` have the same weight, and the potential signer is the only one with a different weight. :::note The account created above will match the first criteria. If a 2-3 schema were used, then the second criteria would match. (In a 2-3 schema, 3 servers are used and 2 of them are enough to recover the key. This is a recommended approach.) You can also use more low-level `signWithRecoveryServers` functions to sign arbitrary transactions. ::: Finally, it's time to submit the transaction: ```typescript await stellar.submitTransaction(recoverTxn); ``` ```kotlin wallet.stellar().submitTransaction(signedReplaceKeyTransaction) ``` ```dart await wallet.stellar().submitTransaction(signedReplaceKeyTx); ``` ```swift try await wallet.stellar.submitTransaction(signedTransaction: signedReplaceKeyTx) ``` [sep-30]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0030.md --- ## Quote
The [SEP-38] standard defines a way for anchors to provide quotes for the exchange of an off-chain asset and a different on-chain asset, and vice versa. Quotes may be [indicative](https://www.investopedia.com/terms/i/indicativequote.asp) or [firm](https://www.investopedia.com/terms/f/firmquote.asp) ones. When either is used is explained in the sections below. ## Creating SEP-38 Object Let's start with creating a sep38 object, which we'll use for all SEP-38 interactions. Authentication is optional for these requests, and depends on the anchor implementation. For our example we will include it. Authentication is done using [Sep-10], and we add the authentication token to the sep38 object. ```typescript const accountKp = ... // our account keypair const auth = await anchor.sep10(); const authToken = await auth.authenticate({ accountKp }); const sep38 = anchor.sep38(authToken); ``` ```dart var accountKp = ... // our account keypair var auth = await anchor.sep10(); var authToken = await auth.authenticate(accountKp); var sep38 = anchor.sep38(authToken: authToken); ``` ```swift let accountKp = ... // our account keypair let sep10 = try await anchor.sep10 let authToken = try await sep10.authenticate(userKeyPair: accountKp) let sep38 = try await anchor.sep38(authToken: authToken) ``` ## Get Anchor Information First, let's get information about the anchor's support for [SEP-38]. The response gives what stellar on-chain assets and off-chain assets are available for trading. ```typescript const resp = await sep38.info(); ``` ```dart var resp = await sep38.info(); ``` ```swift let resp = try await sep38.info ``` For example a response will look like this. The asset identifiers are described below in [Asset Identification Format](#asset-identification-format). ``` { assets: [ { asset: 'stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B' }, { asset: 'iso4217:USD', country_codes: [Array], sell_delivery_methods: [Array], buy_delivery_methods: [Array] } ] } ``` ## Asset Identification Format Before calling other endpoints we should understand the scheme used to identify assets in this protocol. The following format is used: ``` : ``` The currently accepted scheme values are `stellar` for Stellar assets, and `iso4217` for fiat currencies. For example to identify USDC on Stellar we would use: ``` stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN ``` And to identify fiat USD we would use: ``` iso4217:USD ``` Further explanation can be found in [SEP-38 specification](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md#asset-identification-format). ## Get Prices Now let's get [indicative](https://www.investopedia.com/terms/i/indicativequote.asp) prices from the anchor in exchange for a given asset. This is an indicative price. The actual price will be calculated at conversion time once the Anchor receives the funds from a user. In our example we're getting prices for selling 5 fiat USD. ```typescript const resp = await sep38.prices({ sell_asset: "iso4217:USD", sell_amount: "5", }); ``` ```dart var resp = await sep38.prices( sellAsset: "iso4217:USD", sellAmount: "5", ); ``` ```swift let response = try await sep38.prices(sellAsset: "iso4217:USD", sellAmount: "5") ``` The response gives the asset prices for exchanging the requested sell asset. For example, a response look like this: ``` { "buy_assets": [ { "asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", "price": "5.42", "decimals": 7 } ] } ``` ## Get Price Next, let's get an [indicative](https://www.investopedia.com/terms/i/indicativequote.asp) price for a certain pair. Once again this is an indicative value. The actual price will be calculated at conversion time once the Anchor receives the funds from a User. Either a `sellAmount` or `buyAmount` value must be given, but not both. And `context` refers to what Stellar SEP context this will be used for (ie. `sep6`, `sep24`, or `sep31`). ```typescript const resp = await sep38.price({ sellAsset: "iso4217:USD", buyAsset: "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", sellAmount: "5", context: "sep6", }); ``` ```dart var resp = await sep38.price( sellAsset: "iso4217:USD", buyAsset: "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", sellAmount: "5", context: "sep6", ); ``` ```swift let resp = try await sep38.price( context: "sep6", sellAsset: "iso4217:USD", buyAsset: "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", sellAmount: "5") ``` The response gives information for exchanging these assets. For example, a response will look like this: ``` { price: '1.18', sell_amount: '5.00', buy_amount: '4.24' } ``` ## Post Quote Now let's get a [firm](https://www.investopedia.com/terms/f/firmquote.asp) quote from the anchor. As opposed to the earlier endpoints, this quote is stored by the anchor for a certain period of time. We will show how we can grab the quote again later. The request body is similar to the `.price()` call we made earlier. ```typescript const requestResp = await sep38.requestQuote({ sell_asset: "iso4217:USD", buy_asset: "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", sell_amount: "5", context: "sep6", }); ``` ```dart var requestResp = await sep38.requestQuote( sellAsset: "iso4217:USD", buyAsset: "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", sellAmount: "5", context: "sep6", ); ``` ```swift let resp = try await sep38.requestQuote( context: "sep6", sellAsset: "iso4217:USD", buyAsset: "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", sellAmount: "5") ``` However now the response gives an `id` that we can use to identify the quote. The `expires_at` field tells us how long the anchor will wait to receive funds for this quote. An example response looks like this: ``` { id: '019417b3-91ce-473a-929f-15e19470733a', price: '0.81', expires_at: '2024-01-03T20:34:48.190481Z', sell_asset: 'iso4217:USD', buy_asset: 'stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B', sell_amount: '5.00', buy_amount: '6.17' } ``` ## Get Quote Now let's get the previously requested quote. To do that we use the `id` from the `.requestQuote()` response. ```typescript const quoteId = requestResp.id; const getResp = await sep38.getQuote(quoteId); ``` ```dart var quoteId = requestResp.id; var getResp = await sep38.getQuote(quoteId); ``` ```swift let quoteId = requestResp.id let getResp = try await sep38.getQuote(quoteId: quoteId) ``` The response should match the one given from `.requestQuote()` we made earlier. [sep-10]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md [sep-38]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md --- ## Programmatic Deposit and Withdrawal
The [SEP-6] standard defines a way for anchors and wallets to interact on behalf of users. Wallets use this standard to facilitate exchanges between on-chain assets (such as stablecoins) and off-chain assets (such as fiat, or other network assets such as BTC). Please note, this is for _programmatic_ deposits and withdrawals. For hosted deposits and withdrawals, where the anchor interacts with wallets interactively using a popup, please see [Hosted Deposit and Withdrawal](./sep24.mdx). ## Get Anchor Information Let's start with creating a sep6 object, which we'll use for all SEP-6 interactions: ```typescript const sep6 = anchor.sep6(); ``` ```dart var sep6 = anchor.sep6(); ``` ```swift let sep6 = anchor.sep6 ``` First, let's get information about the anchor's support for [SEP-6]. This request doesn't require authentication, and will return generic info, such as supported currencies, and features supported by the anchor. You can get a full list of returned fields in the [SEP-6 specification](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md#info). ```typescript const info = await sep6.info(); ``` ```dart var info = await sep6.info(); ``` ```swift let info = try await sep6.info() ``` ## Start a deposit Before getting started, make sure you have connected to the anchor and received an authentication token, as described in the [Stellar Authentication] wallet guide. We will use `authToken` in the examples below as the [SEP-10] authentication token, obtained earlier. To initiate an operation, we need to know an asset. You can hardcode this. Ensure it is one that is included in the info response above. :::info Before starting with the deposit flow, make sure that the user account has [established a trustline](./stellar.mdx#modify-assets-trustlines) for the asset you are working with. ::: Let's start with a basic deposit. We will use `account` to represent our account's public key. ```typescript const deposit = await sep6.deposit({ authToken, params: { asset_code, account, }, }); ``` ```dart var deposit = await anchor.sep6().deposit( Sep6DepositParams(assetCode: assetCode, account: accountId), authToken, ); ``` ```swift let params = Sep6DepositParams(assetCode: assetCode, account: account) let depositResponse = try await anchor.sep6.deposit(params: params, authToken: authToken) switch depositResponse { case .depositSuccess(let how, let id, let eta, let minAmount, let maxAmount, let feeFixed, let feePercent, let extraInfo, let instructions): // ... } ``` There are several kinds of responses, depending on if the anchor needs more information. All deposits and withdrawals will have these same response types: ### 1. Success response If the response is successful (HTTP 200), then the anchor is processing the deposit. If it needs additional information, it will communicate it when providing the [transaction info](#getting-transaction-info). ### 2. Still processing, or denied If an HTTP 403 with data: `customer_info_status` is returned, it means the action is still processing, or not accepted. In this case the `more_info_url` field should have a link describing next steps. An example response: ``` { "type": "customer_info_status", "status": "denied", "more_info_url": "https://api.example.com/kycstatus?account=GACW7NONV43MZIFHCOKCQJAKSJSISSICFVUJ2C6EZIW5773OU3HD64VI" } ``` ### 3. Needs more KYC info Another common response is an HTTP 403, with the response: `non_interactive_customer_info_needed`. In this case the anchor needs more KYC information via [SEP-12]. An example response: ``` { "type": "non_interactive_customer_info_needed", "fields" : ["family_name", "given_name", "address", "tax_id"] } ``` Let's show how a wallet can handle this situation. First, we get the deposit response object. If the response includes this error, then we can see which missing fields the anchor is requiring. ```typescript if (deposit.type === "non_interactive_customer_info_needed") { // handle displaying the missing fields to the user console.log(deposit.fields); } ``` ```dart if (deposit is Sep6MissingKYC) { print(deposit.fields); } ``` ```swift switch depositResponse { case .missingKYC(let fields): //... } ``` The wallet will need to handle displaying to the user which fields are missing. And then to add those fields, we can use the sep12 class like so. ```typescript const sep12 = await anchor.sep12(authToken); // adding the missing kyc info (sample data) await sep12.add({ sep9Info: { family_name: "smith", given_name: "john", address: "123 street", tax_id: "123", }, }); ``` ```dart var sep12 = await anchor.sep12(authToken); // adding the missing kyc info (sample data) var sep9Info = { 'family_name': 'smith', 'given_name': 'john', 'address': '123 street', 'tax_id': '123', }; var addResponse = await sep12.add(sep9Info); ``` ```swift let sep12 = try await anchor.sep12(authToken: authToken) // adding the missing kyc info (sample data) let sep9Info = [ "family_name" : "smith", "given_name" : "john", "address" : "123 street", "tax_id": "123", ] let addResponse = try await sep12.add(sep9Info: sep9Info) ``` Then, we can re-call the deposit method like before and it should be successful. More information about sending KYC info using [SEP-12] can be found in [Providing KYC info](#providing-kyc-info). ## Providing KYC info An anchor may respond to a deposit or withdrawal request saying they need additional KYC info. To faciliate this, [SEP-6] supports adding a customer's KYC info via [SEP-12]. The user can send in the required info using the sep12 object like below. Let's add some KYC info for our account using sample data. Binary data (eg. image data), needs to be sent in a separate field. The fields allowed to send to the anchor are described in [SEP-9]. ```typescript const sep12 = await anchor.sep12(authToken); await sep12.add({ sep9Info: { first_name: "john", last_name: "smith", email_address: "123@gmail.com", bank_number: "12345", bank_account_number: "12345", }, sep9BinaryInfo: { photo_id_front: Buffer.from("./path/to/image/front"), photo_id_back: Buffer.from("./path/to/image/back"), }, }); ``` ```dart var sep12 = await anchor.sep12(authToken); var sep9Info = { 'first_name': 'john', 'last_name': 'smith', 'email_address': '123@gmail.com', 'bank_number': '12345', 'bank_account_number': '12345', }; var photoIdFront = await Util.readFile('./path/to/image/front'); var photoIdBack = await Util.readFile('./path/to/image/back'); var sep9Files = { 'photo_id_front': photoIdFront, 'photo_id_back': photoIdBack, }; await sep12.add(sep9Info, sep9Files: sep9Files); ``` ```swift let sep12 = try await anchor.sep12(authToken: authToken) let sep9Info = [ "first_name" : "john", "last_name" : "smith", "email_address" : "123@gmail.com", "bank_number" : "12345", "bank_account_number" : "12345", ] let photoIdFront:Data = // ... let photoIdBack:Data = // ... let sep9Files = [ "photo_id_front": photoIdFront, "photo_id_back": photoIdBack, ] let addResponse = try await sep12.add(sep9Info: sep9Info, sep9Files: sep9Files) ``` ## Start a withdrawal Starting a withdrawal is similar to deposit, and has the same response types as described earlier. ```typescript const resp = await sep6.withdraw({ authToken, params: { asset_code: "SRT", account: accountKp.publicKey, type: "bank_account", dest: "123", dest_extra: "12345", }, }); ``` ```dart var params = Sep6WithdrawParams( assetCode: 'SRT', account: accountId, type: 'bank_account', dest: '123', destExtra: '12345', ); var resp = await anchor.sep6().withdraw(params, authToken); ``` ```swift let params = Sep6WithdrawParams( assetCode: "SRT", account: accountId, type: "bank_account", dest: "123", destExtra: "12345") let resp = try await anchor.sep6.withdraw(params: withdrawParams, authToken: authToken) ``` ## Get exchange info If the anchor supports [SEP-38] quotes, it can support deposits that make a bridge between non-equivalent assets. For example, an anchor recieves BRL via bank transfer and in return sends USDC (of equivalent value minus fees) to the user on Stellar. The sep6 exchange functions allow a user to start an exchange deposit or withdrawal with the anchor, and the anchor can communicate back next steps to the user. First, let's start a deposit exchange with sample data. Assets are described using the [SEP-38] scheme. ```typescript const resp = await sep6.depositExchange({ authToken, params: { destination_asset: "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", source_asset: "iso4217:USD", amount: "10", }, }); ``` ```dart var params = Sep6DepositExchangeParams( destinationAssetCode: 'SRT', sourceAssetId: FiatAssetId('USD'), amount: '10', ); var resp = await anchor.sep6().depositExchange(params, authToken); ``` ```swift let params = Sep6DepositExchangeParams( destinationAssetCode: "SRT", sourceAssetId: FiatAssetId(id: "USD"), amount: "10") let resp = try await anchor.sep6.depositExchange(params: params, authToken: authToken) ``` The response follows the same types as all the deposits and withdrawals for SEP-6. Now let's create a withdrawal exchange, which follows the same format as the deposit exchange. We also specify it's a bank account withdrawal using the `type` field. ```typescript const resp = await sep6.withdrawExchange({ authToken, params: { destination_asset: "iso4217:USD", source_asset: "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", amount: "10", type: "bank_account", }, }); ``` ```dart var params = Sep6WithdrawExchangeParams( sourceAssetCode: 'SRT', destinationAssetId: FiatAssetId('USD'), amount: '10', type: 'bank_account', ); var resp = await anchor.sep6().withdrawExchange(params, authToken); ``` ```swift let params = Sep6WithdrawExchangeParams( sourceAssetCode: "SRT", destinationAssetId: FiatAssetId("USD"), amount: "10", type: "bank_account") let resp = try await anchor.sep6.withdrawExchange(params: params, authToken: authToken) ``` The response follows the same types as all the deposits and withdrawals for SEP-6. ## Getting Transaction Info On the typical flow, the wallet would get transaction data to notify users about status updates. This is done via the [SEP-6] `GET /transaction` and `GET /transactions` endpoint. ### Tracking Transaction Let's look into how to use the sdk to track transaction status changes. We will use the `Watcher` class for this purpose. First, let's initialize it and start tracking a transaction. ```typescript const watcher = anchor.sep6().watcher(); const { stop, refresh } = watcher.watchOneTransaction({ authToken, assetCode, id: txId, onSuccess, onMessage, onError, }); ``` ```dart var watcher = anchor.sep6.watcher(); var result = watcher.watchOneTransaction(authToken, 'transaction id'); result.controller.stream.listen( (event) { if (event is StatusChange) { print('Status changed to ${event.status}. Transaction: ${event.transaction.id}'); } else if (event is ExceptionHandlerExit) { print('Exception handler exited the job'); } else if (event is StreamControllerClosed) { print('Stream controller closed. Job is done'); } } ); ``` ```swift let watcher = anchor.sep6.watcher() let result = watcher.watchOneTransaction(authToken: token, id: "transaction id") NotificationCenter.default.addObserver(self, selector: #selector(handleEvent(_:)), name: result.notificationName, object: nil) /// ... @objc public func handleEvent(_ notification: Notification) { if let statusChange = notification.object as? StatusChange { print("Status change to \(statusChange.status.rawValue). Transaction: \(statusChange.transaction.id)") } else if let _ = notification.object as? ExceptionHandlerExit { print("Exception handler exited the job") } else if let _ = notification.object as? NotificationsClosed { print("Notifications closed. Job is done") } } ``` Alternatively, we can track multiple transactions for the same asset. ```typescript const watcher = anchor.sep6().watcher(); const { stop, refresh } = watcher.watchAllTransactions({ authToken, assetCode, onMessage, onError, }); ``` ```dart var watcher = anchor.sep6.watcher(); var result = watcher.watchAsset(authToken, asset); result.controller.stream.listen( (event) { if (event is StatusChange) { print('Status changed to ${event.status}. Transaction: ${event.transaction.id}'); } else if (event is ExceptionHandlerExit) { print('Exception handler exited the job'); } else if (event is StreamControllerClosed) { print('Stream controller closed. Job is done'); } } ); ``` ```swift let watcher = anchor.sep6.watcher() let result = watcher.watchAsset(authToken: token, asset: asset) NotificationCenter.default.addObserver(self, selector: #selector(handleEvent(_:)), name: result.notificationName, object: nil) /// ... @objc public func handleEvent(_ notification: Notification) { if let statusChange = notification.object as? StatusChange { print("Status change to \(statusChange.status.rawValue). Transaction: \(statusChange.transaction.id)") } else if let _ = notification.object as? ExceptionHandlerExit { print("Exception handler exited the job") } else if let _ = notification.object as? NotificationsClosed { print("Notifications closed. Job is done") } } ``` ### Fetching Transaction While `Watcher` class offers powerful tracking capabilities, sometimes it's required to just fetch a transaction (or transactions) once. The `Anchor` class allows you to fetch a transaction by ID, Stellar transaction ID, or external transaction ID: ```typescript const transaction = await anchor .sep6() .getTransactionBy({ authToken, id: transactionId }); ``` ```dart var transaction = await anchor.sep6().getTransactionBy( authToken: authToken, id: transactionId, ); ``` ```swift let transaction = try await anchor.sep6.getTransactionBy( authToken: authToken, transactionId:transactionId) ``` It's also possible to fetch transactions by the asset. ```typescript const transactions = await anchor.sep6().getTransactionsForAsset({ authToken, assetCode, }); ``` ```dart var transactions = await anchor.sep6().getTransactionsForAsset( authToken: authToken, assetCode: assetCode, ); ``` ```swift let transactions = try await anchor.sep6.getTransactionsForAsset( authToken: authToken, assetCode: assetCode) ``` [sep-6]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md [sep-9]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md [sep-10]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md [sep-12]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md [sep-38]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md [stellar authentication]: ./sep10.mdx --- ## URI Scheme to facilitate delegated signing
The [Sep-7] standard defines a way for a non-wallet application to construct a URI scheme that represents a specific transaction for an account to sign. The scheme used is `web+stellar`, followed by a colon. Example: `web+stellar:?=&=` ## Tx Operation The tx operation represents a request to sign a specific transaction envelope, with [some configurable parameters]. ```typescript const sourceAccountKeyPair = "G..."; const destinationAccountKeyPair = "G..."; const txBuilder = await stellar.transaction({ sourceAddress: sourceAccountKeyPair, }); const tx = txBuilder.createAccount(destinationAccountKeyPair).build(); const xdr = encodeURIComponent(tx.toEnvelope().toXDR().toString("base64")); const callback = encodeURIComponent("https://example.com/callback"); const txUri = `web+stellar:tx?xdr=${xdr}&callback=${callback}`; const uri = wallet.parseSep7Uri(txUri); // uri can be parsed and transaction can be signed/submitted by an application that implements Sep-7 ``` ```dart final sourceAccountKeyPair = PublicKeyPair.fromAccountId('G...'); final destinationAccountKeyPair = PublicKeyPair.fromAccountId('G...'); var txBuilder = await stellar.transaction(sourceAccountKeyPair); final tx = txBuilder.createAccount(destinationAccountKeyPair).build(); final xdr = Uri.encodeComponent(tx.toEnvelopeXdrBase64()); final callback = Uri.encodeComponent('https://example.com/callback'); final txUri = 'web+stellar:tx?xdr=$xdr&callback=$callback'; final uri = wallet.parseSep7Uri(txUri); // uri can be parsed and transaction can be signed/submitted by an application that implements Sep-7 ``` ```swift let sourceAccountKeyPair = try PublicKeyPair(accountId: "G...") let destinationAccountKeyPair = try PublicKeyPair(accountId: "G...") let txBuilder = try await stellar.transaction(sourceAddress: sourceAccountKeyPair) let tx = try txBuilder.createAccount(newAccount: destinationAccountKeyPair).build() let xdr = try tx.encodedEnvelope().addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) // url encoded let callback = "https://example.com/callback".addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) // url encoded let txUri = "web+stellar:tx?xdr=\(xdr!)&callback=\(callback!)" let uri = try wallet.parseSep7Uri(uri: txUri) // uri can be parsed and transaction can be signed/submitted by an application that implements Sep-7 ``` You can set replacements to be made in the xdr for specific fields by the application, these will be added in the [Sep-11 transaction representation format] to the URI. ```typescript const uri = new Sep7Tx(txUri); uri.addReplacement({ id: "X", path: "sourceAccount", hint: "account from where you want to pay fees", }); ``` ```dart final uri = wallet.parseSep7Uri(txUri); if (uri is Sep7Tx) { uri.addReplacement(Sep7Replacement( id: 'X', path: 'sourceAccount', hint: 'account from where you want to pay fees')); } ``` ```swift let uri = try wallet.parseSep7Uri(uri: txUri) if let uri = uri as? Sep7Tx { let replacement = Sep7Replacement(id:"X", path:"sourceAccount", hint: "account from where you want to pay fees") uri.addReplacement(replacement: replacement) } ``` You can assign parameters after creating the initial instance using the appropriate setter for the parameter. ```typescript const sourceAccountKeyPair = "G..."; const destinationAccountKeyPair = "G..."; const txBuilder = await stellar.transaction({ sourceAddress: sourceAccountKeyPair, }); const tx = txBuilder.createAccount(destinationAccountKeyPair).build(); const uri = wallet.Sep7Tx.forTransaction(tx); uri.callback = "https://example.com/callback"; uri.msg = "here goes a message"; uri.toString(); // encodes everything and converts to a uri string ``` ```dart final sourceAccountKeyPair = PublicKeyPair.fromAccountId('G...'); final destinationAccountKeyPair = PublicKeyPair.fromAccountId('G...'); var txBuilder = await stellar.transaction(sourceAccountKeyPair); final tx = txBuilder.createAccount(destinationAccountKeyPair).build(); final uri = Sep7Tx.forTransaction(tx); uri.setCallback('https://example.com/callback'); uri.setMsg('here goes a message'); uri.toString(); // encodes everything and converts to a uri string ``` ```swift let sourceAccountKeyPair = try PublicKeyPair(accountId: "G...") let destinationAccountKeyPair = try PublicKeyPair(accountId: "G...") let txBuilder = try await stellar.transaction(sourceAddress: sourceAccountKeyPair) let tx = try txBuilder.createAccount(newAccount: destinationAccountKeyPair).build() let uri = try Sep7Tx(transaction: tx) uri.setCallback(callback: "https://example.com/callback") try uri.setMsg(msg: "here goes a message") let uriStr = uri.toString() // encodes everything and converts to a uri string ``` ## Pay Operation The pay operation represents a request to pay a specific address with a specific asset, regardless of the source asset used by the payer. You can [configure parameters] to build the payment operation. ```typescript const destination = "G..."; const assetIssuer = "G..."; const assetCode = "USDC"; const amount = "120.1234567"; const memo = "memo"; const message = encodeURIComponent("pay me with lumens"); const originDomain = "example.com"; const payUri = `web+stellar:pay?destination=${destination}&amount=${amount}&memo=${memo}&msg=${message}&origin_domain=${originDomain}&asset_issuer=${assetIssuer}&asset_code=${assetCode}`; const uri = parseSep7Uri(payUri); // uri can be parsed and transaction can be built/signed/submitted by an application that implements Sep-7 ``` ```dart const destination = 'G...'; const assetIssuer = 'G...'; const assetCode = 'USDC'; const amount = '120.1234567'; const memo = 'memo'; final message = Uri.encodeComponent('pay me with lumens'); const originDomain = 'example.com'; final payUri = 'web+stellar:pay?destination=$destination&amount=$amount&memo=$memo&msg=$message&origin_domain=$originDomain&asset_issuer=$assetIssuer&asset_code=$assetCode'; final uri = Sep7.parseSep7Uri(payUri); // uri can be parsed and transaction can be built/signed/submitted by an application that implements Sep-7 ``` ```swift let destination = "G..." let assetIssuer = "G..." let assetCode = "USDC" let amount = "120.1234567" let memo = "memo" let message = "pay me with lumens".addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) // url encoded let originDomain = "example.com" let payUri = "web+stellar:pay?destination=\(destination)&amount=\(amount)&memo=\(memo)&msg=\(message!)&origin_domain=\(originDomain)&asset_issuer=\(assetIssuer)&asset_code=\(assetCode)" uri = try Sep7.parseSep7Uri(uri: payUri) // uri can be parsed and transaction can be built/signed/submitted by an application that implements Sep-7 ``` You can assign parameters after creating the initial instance using the appropriate setter for the parameter. ```typescript const uri = wallet.Sep7Pay.forDestination("G..."); uri.callback = "https://example.com/callback"; uri.msg = "here goes a message"; uri.assetCode = "USDC"; uri.assetIssuer = "G..."; uri.amount = "10"; uri.toString(); // encodes everything and converts to a uri string ``` ```dart final uri = Sep7Pay.forDestination('G...'); uri.setCallback('https://example.com/callback'); uri.setMsg('here goes a message'); uri.setAssetCode('USDC'); uri.setAssetIssuer('G...'); uri.setAmount('10'); uri.toString(); // encodes everything and converts to a uri string ``` ```swift let uri = Sep7Pay(destination: "G...") uri.setCallback(callback: "https://example.com/callback") try uri.setMsg(msg: "here goes a message") uri.setAssetCode(assetCode: "USDC") uri.setAssetIssuer(assetIssuer: "G...") uri.setAmount(amount: "10") uri.toString() // encodes everything and converts to a uri string ``` The last step after building a `Sep7Tx` or `Sep7Pay` is to add a signature to your uri. This will create a payload out of the transaction and sign it with the provided keypair. ```typescript const uri = wallet.Sep7Pay.forDestination("G..."); uri.originDomain = "example.com"; const keypair = wallet.stellar().account().createKeypair(); uri.addSignature(Keypair.fromSecret(keypair.secretKey)); console.log(uri.signature); // signed uri payload ``` ```dart final uri = Sep7Pay.forDestination('G...'); uri.setOriginDomain('example.com'); final keypair = wallet.stellar().account().createKeyPair(); uri.addSignature(keypair); print(uri.getSignature()); // signed uri payload ``` ```swift uri = Sep7Pay(destination: "G..") uri.setOriginDomain(originDomain: "example.com") let keyPair = wallet.stellar.account.createKeyPair() try uri.addSignature(keyPair: keyPair) print(uri.getSignature()) // signed uri payload ``` The signature can then be verified by fetching the [Stellar toml file] from the origin domain in the uri, and using the included signing key to verify the uri signature. This is all done as part of the `verifySignature` method. ```typescript const passesVerification = await uri.verifySignature(); // true or false ``` ```dart final passesVerification = await uri.verifySignature(); // true or false ``` ```swift let passesVerification = await uri.verifySignature() // true or false ``` [Sep-7]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md [Sep-11 transaction representation format]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0011.md [some configurable parameters]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md#operation-tx [configure parameters]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md#operation-pay [Stellar toml file]: ../../apps/example-application-tutorial/anchor-integration/sep1.mdx --- ## Stellar Network
In the previous section we learned how to create a wallet and a `Stellar` object that provides a connection to Horizon. In this section, we will look at the usages of this class. ## Accounts The most basic entity on the Stellar network is an account. Let's look into AccountService that provides the capability to work with accounts: ```kt val account = wallet.stellar().account() ``` ```typescript let account = wal.stellar().account(); ``` ```dart var account = wallet.stellar().account(); ``` ```swift let account = wallet.stellar.account ``` Now we can create a keypair: ```kt val accountKeyPair = account.createKeyPair() ``` ```typescript let accountKeyPair = account.createKeypair(); ``` ```dart var accountKeyPair = account.createKeyPair(); ``` ```swift let accountKeyPair = account.createKeyPair() ``` } /> ## Build Transaction The transaction builder allows you to create various transactions that can be signed and submitted to the Stellar network. Some transactions can be sponsored. ### Building Basic Transactions First, let's look into building basic transactions. #### Create Account The create account transaction activates/creates an account with a starting balance of XLM (1 XLM by default). ```kotlin suspend fun createAccount(): Transaction { return stellar.transaction(sourceAccountKeyPair).createAccount(destinationAccountKeyPair).build() } ``` ```typescript const txBuilder = await stellar.transaction({ sourceAddress: sourceAccountKeyPair, }); const tx = txBuilder.createAccount(destinationAccountKeyPair).build(); ``` ```dart var txBuilder = await stellar.transaction(sourceAccountKeyPair); var tx = txBuilder.createAccount(destinationAccountKeyPair).build(); ``` ```swift let txBuilder = try await stellar.transaction(sourceAddress: sourceAccountKeyPair) let tx = try txBuilder.createAccount(newAccount: destinationAccountKeyPair).build() ``` #### Modify Account You can lock the master key of the account by setting its weight to 0. Use caution when locking the account's master key. Make sure you have set the correct signers and weights. Otherwise, you will lock the account irreversibly. ```kotlin suspend fun lockMasterKey(): Transaction { return stellar.transaction(sourceAccountKeyPair).lockAccountMasterKey().build() } ``` ```typescript const txBuilder = await stellar.transaction({ sourceAddress: sourceAccountKeyPair, }); const tx = txBuilder.lockAccountMasterKey().build(); ``` ```dart var txBuilder = await stellar.transaction(sourceAccountKeyPair); var tx = txBuilder.lockAccountMasterKey().build(); ``` ```swift let txBuilder = try await stellar.transaction(sourceAddress: sourceAccountKeyPair) let tx = try txBuilder.lockAccountMasterKey().build() ``` Add a new signer to the account. Use caution when adding new signers and make sure you set the correct signer weight. Otherwise, you will lock the account irreversibly. ```kotlin val newSignerKeyPair = account.createKeyPair() suspend fun addSigner(): Transaction { return stellar.transaction(sourceAccountKeyPair).addAccountSigner(newSignerKeyPair, 10).build() } ``` ```typescript const newSignerKeyPair = account.createKeypair(); const tx = txBuilder.addAccountSigner(newSignerKeyPair, 10).build(); ``` ```dart var newSignerKeyPair = account.createKeyPair(); var txBuilder = await stellar.transaction(sourceAccountKeyPair); var tx = txBuilder.addAccountSigner(newSignerKeyPair, 10).build(); ``` ```swift let newSignerKeyPair = account.createKeyPair() let txBuilder = try await stellar.transaction(sourceAddress: sourceAccountKeyPair) let tx = try txBuilder.addAccountSigner(signerAddress: newSignerKeyPair, signerWeight: 10).build() ``` Remove a signer from the account. ```kotlin suspend fun removeSigner(): Transaction { return stellar.transaction(sourceAccountKeyPair).removeAccountSigner(newSignerKeyPair).build() } ``` ```typescript const tx = txBuilder.removeAccountSigner(newSignerKeyPair).build(); ``` ```dart var txBuilder = await stellar.transaction(sourceAccountKeyPair); var tx = txBuilder.removeAccountSigner(newSignerKeyPair).build(); ``` ```swift let txBuilder = try await stellar.transaction(sourceAddress: sourceAccountKeyPair) let tx = try txBuilder.removeAccountSigner(signerAddress: newSignerKeyPair).build() ``` Modify account thresholds (useful when multiple signers are assigned to the account). This allows you to restrict access to certain operations when the limit is not reached. ```kotlin suspend fun setThreshold(): Transaction { return stellar.transaction(sourceAccountKeyPair).setThreshold(low = 1, medium = 10, high = 30).build() } ``` ```typescript const tx = txBuilder.setThreshold({ low: 1, medium: 10, high: 30 }).build(); ``` ```dart var txBuilder = await stellar.transaction(sourceAccountKeyPair); var tx = txBuilder.setThreshold(low: 1, medium: 10, high: 20).build(); ``` ```swift let txBuilder = try await stellar.transaction(sourceAddress: sourceAccountKeyPair) let tx = try txBuilder.setThreshold(low: 1, medium: 10, high: 20).build() ``` #### Modify Assets (Trustlines) Add an asset (trustline) to the account. This allows the account to receive transfers of the asset. ```kotlin val asset = IssuedAssetId("USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5") suspend fun addAsset(): Transaction { return stellar.transaction(sourceAccountKeyPair).addAssetSupport(asset).build() } ``` ```typescript const asset = new IssuedAssetId( "USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", ); const tx = txBuilder.addAssetSupport(asset).build(); ``` ```dart var asset = IssuedAssetId( code: "USDC", issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"); var txBuilder = await stellar.transaction(sourceAccountKeyPair); var tx = txBuilder.addAssetSupport(asset).build(); ``` ```swift let asset = try IssuedAssetId( code: "USDC", issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5") let txBuilder = try await stellar.transaction(sourceAddress: sourceAccountKeyPair) let tx = try txBuilder.addAssetSupport(asset: asset).build() ``` Remove an asset from the account (the asset's balance must be 0). ```kotlin suspend fun removeAsset(): Transaction { return stellar.transaction(sourceAccountKeyPair).removeAssetSupport(asset).build() } ``` ```typescript const tx = txBuilder.removeAssetSupport(asset).build(); ``` ```dart var txBuilder = await stellar.transaction(sourceAccountKeyPair); var tx = txBuilder.removeAssetSupport(asset).build(); ``` ```swift let txBuilder = try await stellar.transaction(sourceAddress: sourceAccountKeyPair) let tx = try txBuilder.removeAssetSupport(asset: asset).build() ``` #### Swap Exchange an account's asset for a different asset. The account must have a trustline for the destination asset. ```typescript const txBuilder = await stellar.transaction({ sourceAddress: sourceKp, }); const usdcAsset = new IssuedAssetId( "USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", ); const txn = txBuilder.swap(new NativeAssetId(), usdcAsset, ".1").build(); ``` ```dart var txBuilder = await stellar.transaction(sourceKp); final usdcAsset = IssuedAssetId( code: 'USDC', issuer: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5', ); var txn = txBuilder.swap( fromAsset: NativeAssetId(), toAsset: usdcAsset, amount: "0.1", ).build(); ``` ```swift let txBuilder = try await stellar.transaction(sourceAddress: sourceKp) let usdcAsset = try IssuedAssetId( code: "USDC", issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5") let txn = try txBuilder.swap(fromAsset: NativeAssetId(), toAsset: usdcAsset, amount: 0.1).build() ``` #### Path Pay Send one asset from the source account and receive a different asset in the destination account. ```typescript const txBuilder = await stellar.transaction({ sourceAddress: sourceKp, }); const usdcAsset = new IssuedAssetId( "USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", ); const txn = txBuilder .pathPay({ destinationAddress: receivingKp.publicKey, sendAsset: new NativeAssetId(), destAsset: usdcAsset, sendAmount: "5", }) .build(); ``` ```dart var txBuilder = await stellar.transaction(sourceKp); final usdcAsset = IssuedAssetId( code: 'USDC', issuer: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5', ); var txn = txBuilder.pathPay( destinationAddress: receivingKp.address, sendAsset: NativeAssetId(), destinationAsset: usdcAsset, sendAmount: "5", ).build(); ``` ```swift let txBuilder = try await stellar.transaction(sourceAddress: sourceKp) let usdcAsset = try IssuedAssetId( code: "USDC", issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5") let txn = try txBuilder.pathPay(destinationAddress: receivingKp.address, sendAsset: NativeAssetId(), destinationAsset: usdcAsset, sendAmount: 5).build() ``` #### Set Memo Set a memo on the transaction. The memo object can be imported from ["@stellar/stellar-sdk"](https://www.npmjs.com/package/@stellar/stellar-sdk). ```typescript const tx = txBuilder.setMemo(new Memo("text", "Memo string")).build(); ``` ```dart var memo = flutter_sdk.MemoText("Memo string"); var tx = txBuilder.setMemo(memo).build(); ``` ```swift let memo = Memo.text("Memo string") let tx = try txBuilder.setMemo(memo: memo).build() ``` #### Account Merge Merges account into a destination account. ```typescript const txBuilder = await stellar.transaction({ sourceAddress: accountKp, baseFee: 1000, }); const mergeTxn = txBuilder .accountMerge(accountKp.publicKey, sourceKp.publicKey) .build(); ``` ```dart var txBuilder = await stellar.transaction(accountKp, baseFee: 1000); var mergeTxn = txBuilder.accountMerge( destinationAddress: accountKp.address, sourceAddress: sourceKp.address, ).build(); ``` ```swift let txBuilder = try await stellar.transaction(sourceAddress: accountKp, baseFee: 1000) let mergeTxn = try txBuilder.accountMerge(destinationAddress: accountKp.address, sourceAddress: sourceKp.address).build() ``` #### Fund Testnet Account Fund an account on the Stellar test network ```typescript wallet.stellar().fundTestnetAccount(accountKp.publicKey); ``` ```dart await wallet.stellar().fundTestNetAccount(accountKp.address); ``` ```swift try await wallet.stellar.fundTestNetAccount(address: accountKp.address) ``` ### Building Advanced Transactions In some cases a private key may not be known prior to forming a transaction. For example, a new account must be funded to exist and the wallet may not have the key for the account so may request the create account transaction to be sponsored by a third party. ```kt // Third-party key that will sponsor creating new account val externalKeyPair = "MySponsorAddress".toPublicKeyPair() val newKeyPair = account.createKeyPair() ``` ```typescript // Third-party key that will sponsor creating new account const externalKeyPair = new PublicKeypair.fromPublicKey("GC5GD..."); const newKeyPair = account.createKeypair(); ``` ```dart // Third-party key that will sponsor creating new account var externalKeyPair = PublicKeyPair.fromAccountId("GC5GD..."); var newKeyPair = account.createKeyPair(); ``` ```swift // Third-party key that will sponsor creating new account let externalKeyPair = try PublicKeyPair(accountId: "GC5GD...") let newKeyPair = account.createKeyPair() ``` First, the account must be created. ```kotlin suspend fun makeCreateTx(): Transaction { return stellar.transaction(externalKeyPair).createAccount(newKeyPair).build() } ``` ```typescript const createTxn = txBuilder.createAccount(newKeyPair).build(); ``` ```dart var txBuilder = await stellar.transaction(externalKeyPair); var createTxn = txBuilder.createAccount(newKeyPair).build(); ``` ```swift let txBuilder = try await stellar.transaction(sourceAddress: externalKeyPair) let createTxn = try txBuilder.createAccount(newAccount: newKeyPair).build() ``` This transaction must be sent to external signer (holder of `externalKeyPair`) to be signed. ```kt suspend fun remoteSignTransaction(transaction: Transaction) { val xdrString = transaction.toEnvelopeXdrBase64() // Send xdr encoded transaction to your backend server to sign val xdrStringFromBackend = sendTransactionToBackend(xdrString) // Decode xdr to get the signed transaction val signedTransaction = stellar.decodeTransaction(xdrStringFromBackend) } ``` ```typescript const xdrString = createTxn.toXDR(); // Send xdr encoded transaction to your backend server to sign const xdrStringFromBackend = await sendTransactionToBackend(xdrString); // Decode xdr to get the signed transaction const signedTransaction = stellar.decodeTransaction(xdrStringFromBackend); ``` ```dart var xdrString = createTxn.toEnvelopeXdrBase64(); // Send xdr encoded transaction to your backend server to sign var xdrStringFromBackend = await sendTransactionToBackend(xdrString); // Decode xdr to get the signed transaction var signedTransaction = stellar.decodeTransaction(xdrStringFromBackend); ``` ```swift let xdrString = createTxn.toEnvelopeXdrBase64() // Send xdr encoded transaction to your backend server to sign let xdrStringFromBackend = await sendTransactionToBackend(xdrString) // Decode xdr to get the signed transaction let signedTransaction = stellar.decodeTransaction(xdr: xdrStringFromBackend) ``` :::note You can read more about passing XDR transaction to the server in the [chapter below](#using-xdr-to-send-transaction-data). ::: Signed transaction can be submitted by the wallet. ```kt suspend fun submitCreateTx(signedCreateTx: Transaction) { wallet.stellar().submitTransaction(signedCreateTx) } ``` ```typescript await wallet.stellar().submitTransaction(signedTransaction); ``` ```dart bool success = await stellar.submitTransaction(signedTransaction); ``` ```swift try await stellar.submitTransaction(signedTransaction: signedTransaction) ``` Now, after the account is created, it can perform operations. For example, we can disable the master keypair and replace it with a new one (let's call it the device keypair) atomically in one transaction: ```kotlin suspend fun addDeviceKeyPair() { val deviceKeyPair = account.createKeyPair() val modifyAccountTransaction = stellar .transaction(newKeyPair) .addAccountSigner( deviceKeyPair, signerWeight = 1, ) .lockAccountMasterKey() .build() .sign(newKeyPair) wallet.stellar().submitTransaction(modifyAccountTransaction) } ``` ```typescript const deviceKeyPair = account.createKeypair(); const txBuilder = await stellar.transaction({ sourceAddress: newKeyPair }); const modifyAccountTransaction = txBuilder .addAccountSigner(deviceKeyPair, 1) .lockAccountMasterKey() .build(); newKeyPair.sign(modifyAccountTransaction); await wallet.stellar().submitTransaction(modifyAccountTransaction); ``` ```dart var deviceKeyPair = account.createKeyPair(); var txBuilder = await stellar.transaction(newKeyPair); var modifyAccountTransaction = txBuilder .addAccountSigner(deviceKeyPair, 1) .lockAccountMasterKey() .build(); stellar.sign(modifyAccountTransaction, newKeyPair); bool success = await stellar.submitTransaction(modifyAccountTransaction); ``` ```swift let deviceKeyPair = account.createKeyPair() let txBuilder = try await stellar.transaction(sourceAddress: newKeyPair) let modifyAccountTransaction = try txBuilder .addAccountSigner(signerAddress: deviceKeyPair, signerWeight: 1) .lockAccountMasterKey() .build() stellar.sign(tx: modifyAccountTransaction, keyPair: newKeyPair) try await stellar.submitTransaction(signedTransaction: modifyAccountTransaction) ``` #### Adding an Operation Add a custom Operation to a transaction. This can be any [Operation](../../../learn/fundamentals/transactions/list-of-operations) supported by the Stellar network. The Operation object can be imported from ["@stellar/stellar-sdk"](https://www.npmjs.com/package/@stellar/stellar-sdk). ```typescript const txBuilder = await stellar.transaction({ sourceAddress: sourceAccountKeyPair, }); const tx = txBuilder.addOperation( Operation.manageData({ name: "web_auth_domain", value: new URL(authServer).hostname, source: sourceAccountKeyPair, }), ); ``` ```dart var txBuilder = await stellar.transaction(sourceAccountKeyPair); var key = "web_auth_domain"; var value = "https://testanchor.stellar.org"; var valueBytes = Uint8List.fromList(value.codeUnits); var manageDataOperation = flutter_sdk.ManageDataOperationBuilder( key, valueBytes, ).build(); var tx = txBuilder.addOperation( manageDataOperation, ).build(); ``` ```swift let txBuilder = try await stellar.transaction(sourceAddress: sourceAccountKeyPair) let key = "web_auth_domain" let value = "testanchor.stellar.org" let manageDataOperation = ManageDataOperation(sourceAccountId: sourceAccountKeyPair.address, name: key, data: value.data(using: .utf8)) let tx = try txBuilder.addOperation(operation: manageDataOperation).build() ``` ### Sponsoring Transactions #### Sponsor Operations Some operations, that modify account reserves can be [sponsored](../../guides/transactions/sponsored-reserves.mdx). For sponsored operations, the sponsoring account will be paying for the reserves instead of the account that being sponsored. This allows you to do some operations, even if account doesn't have enough funds to perform such operations. To sponsor a transaction, sponsoring block:} ts={simply create a building function (describing which operations are to be sponsored) and pass it to the sponsoring method:}/> ```kotlin suspend fun sponsorOperation() { val transaction = stellar .transaction(sponsoredKeyPair) .sponsoring(sponsorKeyPair) { addAssetSupport(asset) } .build() transaction.sign(sponsorKeyPair).sign(sponsoredKeyPair) } ``` ```typescript const txBuilder = await stellar.transaction({ sourceAddress: sponsoredKeyPair, }); const buildingFunction = (bldr) => bldr.addAssetSupport(asset); const transaction = txBuilder .sponsoring(sponsorKeyPair, buildingFunction) .build(); sponsoredKeyPair.sign(transaction); sponsorKeyPair.sign(transaction); ``` ```dart var txBuilder = await stellar.transaction(sponsoredKeyPair); var transaction = txBuilder .sponsoring(sponsorKeyPair, (builder) => builder.addAssetSupport(asset)) .build(); stellar.sign(transaction, sponsorKeyPair); stellar.sign(transaction, sponsoredKeyPair); ``` ```swift let txBuilder = try await stellar.transaction(sourceAddress: sponsoredKeyPair) let transaction = try txBuilder.sponsoring( sponsorAccount: sponsorKeyPair, buildingFunction:{ (builder) in builder.addAssetSupport(asset: asset)}).build() stellar.sign(tx: transaction, keyPair: sponsorKeyPair) stellar.sign(tx: transaction, keyPair: sponsoredKeyPair) ``` :::info Only some operations can be sponsored, and a sponsoring has a slightly different set of functions available compared to the regular `TransactionBuilder`. Note, that a transaction must be signed by both the sponsor account (`sponsoringKeyPair`) and the account being sponsored (`sponsoredKeyPair`). ::: #### Sponsoring Account Creation One of the things that can be done via sponsoring is to create an account with a 0 starting balance. This account creation can be created by simply writing: ```kt suspend fun sponsorAccountCreation() { val newKeyPair = account.createKeyPair() val transaction = stellar .transaction(sponsorKeyPair) .sponsoring(sponsorKeyPair, sponsoredAccount = newKeyPair) { createAccount(newKeyPair) } .build() transaction.sign(sponsorKeyPair).sign(newKeyPair) } ``` ```typescript const txBuilder = await stellar.transaction({ sourceAddress: sponsorKeyPair }); const newKeyPair = account.createKeypair(); const buildingFunction = (bldr) => bldr.createAccount(newKeyPair); const transaction = txBuilder .sponsoring(sponsorKeyPair, buildingFunction, newKeyPair) .build(); newKeyPair.sign(transaction); sponsorKeyPair.sign(transaction); ``` ```dart var newKeyPair = account.createKeyPair(); var txBuilder = await stellar.transaction(sponsorKeyPair); var transaction = txBuilder .sponsoring(sponsorKeyPair, sponsoredAccount: newKeyPair, (builder) => builder.createAccount(newKeyPair)) .build(); stellar.sign(transaction, sponsorKeyPair); stellar.sign(transaction, newKeyPair); ``` ```swift let newKeyPair = account.createKeyPair() let txBuilder = try await stellar.transaction(sourceAddress: sponsorKeyPair) let transaction = try txBuilder.sponsoring( sponsorAccount: sponsorKeyPair, buildingFunction:{ (builder) in builder.createAccount(newAccount: newKeyPair)}, sponsoredAccount: newKeyPair).build() stellar.sign(tx: transaction, keyPair: sponsorKeyPair) stellar.sign(tx: transaction, keyPair: newKeyPair) ``` Note how in the first example the transaction source account is set to `sponsoredKeyPair`. Due to this, we did not need to pass a sponsored account value to the `sponsoring` . Since when ommitted, the sponsored account defaults to the transaction source account (`sponsoredKeyPair`). However, this time, the sponsored account (freshly created `newKeyPair`) is different from the transaction source account. Therefore, it's necessary to specify it. Otherwise, the transaction will contain a malformed operation. As before, the transaction must be signed by both keys. #### Sponsoring Account Creation and Modification If you want to create an account and modify it in one transaction, it's possible to do so with passing a `sponsoredAccount` optional argument to the (`newKeyPair` below). If this argument is present, all operations inside the sponsored block will be sourced by this `sponsoredAccount`. (Except account creation, which is always sourced by the sponsor). ```kotlin suspend fun sponsorAccountCreationAndModification() { val newKeyPair = account.createKeyPair() val replaceWith = account.createKeyPair() val transaction = stellar .transaction(sponsorKeyPair) .sponsoring(sponsorKeyPair, newKeyPair) { createAccount(newKeyPair) // source account for below operations will be newKeyPair addAccountSigner(replaceWith, 1) lockAccountMasterKey() } .build() transaction.sign(sponsorKeyPair).sign(newKeyPair) } ``` ```typescript const txBuilder = await stellar.transaction({ sourceAddress: sponsorKeyPair }); const newKeyPair = account.createKeypair(); const replaceWith = account.createKeypair(); const buildingFunction = (bldr) => bldr .createAccount(newKeyPair) // source account for below operations will be newKeyPair .addAccountSigner(replaceWith, 1) .lockAccountMasterKey(); const transaction = txBuilder .sponsoring(sponsorKeyPair, buildingFunction, newKeyPair) .build(); newKeyPair.sign(transaction); sponsorKeyPair.sign(transaction); ``` ```dart var newKeyPair = account.createKeyPair(); var replaceWith = account.createKeyPair(); var txBuilder = await stellar.transaction(sponsorKeyPair); var transaction = txBuilder .sponsoring( sponsorKeyPair, sponsoredAccount: newKeyPair, (builder) => builder .createAccount(newKeyPair) .addAccountSigner(replaceWith, 1) .lockAccountMasterKey()) .build(); stellar.sign(transaction, sponsorKeyPair); stellar.sign(transaction, newKeyPair); ``` ```swift let newKeyPair = account.createKeyPair() let replaceWith = account.createKeyPair() let txBuilder = try await stellar.transaction(sourceAddress: sponsorKeyPair) let transaction = try txBuilder.sponsoring( sponsorAccount: sponsorKeyPair, buildingFunction:{ (builder) in builder .createAccount(newAccount: newKeyPair) .addAccountSigner(signerAddress: replaceWith, signerWeight: 1) .lockAccountMasterKey() }, sponsoredAccount: newKeyPair).build() stellar.sign(tx: transaction, keyPair: sponsorKeyPair) stellar.sign(tx: transaction, keyPair: newKeyPair) ``` ### Fee-Bump Transaction If you wish to modify a newly created account with a 0 balance, it's also possible to do so via `FeeBump`. It can be combined with a sponsoring to achieve the same result as in the example above. However, with `FeeBump` it's also possible to add more operations (that don't require sponsoring), such as a transfer. First, let's create a transaction that will replace the master key of an account with a new keypair. ```kt val replaceWith = account.createKeyPair() val transaction = stellar .transaction(sponsoredKeyPair) .sponsoring(sponsorKeyPair) { lockAccountMasterKey() addAccountSigner(replaceWith, signerWeight = 1) } .build() ``` ```typescript const txBuilder = await stellar.transaction({ sourceAddress: sponsoredKeyPair, }); const replaceWith = account.createKeypair(); const buildingFunction = (bldr) => bldr.lockAccountMasterKey().addAccountSigner(replaceWith, 1); const transaction = txBuilder .sponsoring(sponsorKeyPair, buildingFunction) .build(); ``` ```dart var replaceWith = account.createKeyPair(); var txBuilder = await stellar.transaction(sponsoredKeyPair); var transaction = txBuilder .sponsoring(sponsorKeyPair, (builder) => builder.lockAccountMasterKey().addAccountSigner(replaceWith, 1)) .build(); ``` ```swift let replaceWith = account.createKeyPair() let txBuilder = try await stellar.transaction(sourceAddress: sponsoredKeyPair) let transaction = try txBuilder.sponsoring( sponsorAccount: sponsorKeyPair, buildingFunction:{ (builder) in builder .lockAccountMasterKey() .addAccountSigner(signerAddress: replaceWith, signerWeight: 1) }).build() ``` Second, sign transaction with both keys. ```kt transaction.sign(sponsoredKeyPair).sign(sponsorKeyPair) ``` ```typescript sponsorKeyPair.sign(transaction); sponsoredKeyPair.sign(transaction); ``` ```dart stellar.sign(transaction, sponsorKeyPair); stellar.sign(transaction, sponsoredKeyPair); ``` ```swift stellar.sign(tx: transaction, keyPair: sponsorKeyPair) stellar.sign(tx: transaction, keyPair: sponsoredKeyPair) ``` Next, create a fee bump, targeting the transaction. ```kt val feeBump = stellar.makeFeeBump(sponsorKeyPair, transaction) feeBump.sign(sponsorKeyPair) ``` ```typescript const feeBump = stellar.makeFeeBump({ feeAddress: sponsorKeyPair, transaction, }); sponsorKeyPair.sign(feeBump); ``` ```dart var feeBump = stellar.makeFeeBump(sponsorKeyPair, transaction); stellar.sign(feeBump, sponsorKeyPair); ``` ```swift let feeBump = try stellar.makeFeeBump(feeAddress: sponsorKeyPair, transaction: transaction) stellar.sign(tx: transaction, keyPair: sponsorKeyPair) ``` Finally, submit a fee-bump transaction. Executing this transaction will be fully covered by the `sponsorKeyPair` and `sponsoredKeyPair` and may not even have any XLM funds on its account. ```kt wallet.stellar().submitTransaction(feeBump) ``` ```typescript await wallet.stellar().submitTransaction(feeBump); ``` ```dart bool success = await stellar.submitTransaction(feeBump); ``` ```swift try await stellar.submitTransaction(signedFeeBumpTransaction: feeBump) ``` ### Using XDR to Send Transaction Data Note, that a wallet may not have a signing key for `sponsorKeyPair`. In that case, it's necessary to convert the transaction to XDR, send it to the server, containing `sponsorKey` and return the signed transaction back to the wallet. Let's use the previous example of sponsoring account creation, but this time with the sponsor key being unknown to the wallet. The first step is to define the public key of the sponsor keypair: ```kt val sponsorKeyPair = "SponsorAddress".toPublicKeyPair() ``` ```typescript const sponsorKeyPair = new PublicKeypair.fromPublicKey("GC5GD..."); ``` ```dart var sponsorKeyPair = PublicKeyPair.fromAccountId("GC5GD..."); ``` ```swift let sponsorKeyPair = try PublicKeyPair(accountId: "GC5GD...") ``` Next, create an account in the same manner as before and sign it with `newKeyPair`. This time, convert the transaction to XDR: ```kt suspend fun sponsorAccountCreation(): String { val newKeyPair = account.createKeyPair() return stellar .transaction(sponsorKeyPair) .sponsoring(sponsorKeyPair) { createAccount(newKeyPair) } .build() .sign(newKeyPair) .toEnvelopeXdrBase64() } ``` ```typescript const txBuilder = await stellar.transaction({ sourceAddress: sponsorKeyPair }); const newKeyPair = account.createKeypair(); const transaction = txBuilder .sponsoring(sponsorKeyPair, (bldr) => bldr.createAccount(newKeyPair)) .build(); const xdrString = newKeyPair.sign(transaction).toXDR(); ``` ```dart var newKeyPair = account.createKeyPair(); var txBuilder = await stellar.transaction(sponsorKeyPair); var transaction = txBuilder .sponsoring( sponsorKeyPair, (builder) => builder.createAccount(newKeyPair), sponsoredAccount: newKeyPair) .build(); stellar.sign(transaction, newKeyPair); var xdrString = transaction.toEnvelopeXdrBase64(); ``` ```swift let newKeyPair = account.createKeyPair() let txBuilder = try await stellar.transaction(sourceAddress: sponsorKeyPair) let transaction = try txBuilder.sponsoring( sponsorAccount: sponsorKeyPair, buildingFunction:{ (builder) in builder.createAccount(newAccount: newKeyPair)}, sponsoredAccount: newKeyPair).build() stellar.sign(tx: transaction, keyPair: newKeyPair) let xdrString = transaction.toEnvelopeXdrBase64() ``` It can now be sent to the server. On the server, sign it with a private key for the sponsor address: ```kt // On the server fun signTransaction(xdrString: String): String { val sponsorPrivateKey = SigningKeyPair.fromSecret("MySecret") val signedTransaction = stellar.decodeTransaction(xdrString).sign(sponsorPrivateKey) return signedTransaction.toEnvelopeXdrBase64() } ``` ```typescript // On the server const sponsorPrivateKey = SigningKeyPair.fromSecret("SD3LH4..."); const signedTransaction = sponsorPrivateKey.sign( stellar.decodeTransaction(xdrString), ); return signedTransaction.toXDR(); ``` ```dart String signTransaction(String xdrString) { var sponsorPrivateKey = SigningKeyPair.fromSecret("SD3LH4..."); var transaction = stellar.decodeTransaction(xdrString); stellar.sign(transaction, sponsorPrivateKey); return transaction.toEnvelopeXdrBase64(); } ``` ```swift internal func signTransaction(xdr:String) throws -> String { let sponsorPrivateKey = try SigningKeyPair(secretKey: "SD3LH4...") let transactionEnum = stellar.decodeTransaction(xdr: xdr) switch transactionEnum { case .transaction(let tx): stellar.sign(tx: tx, keyPair: sponsorPrivateKey) return tx.toEnvelopeXdrBase64() case .feeBumpTransaction(let feeBumpTx): stellar.sign(feeBumpTx: feeBumpTx, keyPair: sponsorPrivateKey) return feeBumpTx.toEnvelopeXdrBase64() case .invalidXdrErr: throw ValidationError.invalidArgument(message: "invalid xdr") } } ``` When the client receives the fully signed transaction, it can be decoded and sent to the Stellar network: ```kt suspend fun recoverSigned(xdrString: String) { val signedTransaction = stellar.decodeTransaction(xdrString) stellar.submitTransaction(signedTransaction) } ``` ```typescript const signedTransaction = stellar.decodeTransaction(xdrString); await wallet.stellar().submitTransaction(signedTransaction); ``` ```dart var signedTransaction = stellar.decodeTransaction(xdrStringFromBackend); bool success = await stellar.submitTransaction(signedTransaction); ``` ```swift let signedTransactionEnum = stellar.decodeTransaction(xdr: xdrStringFromBackend) switch signedTransactionEnum { case .transaction(let tx): try await stellar.submitTransaction(signedTransaction: tx) case .feeBumpTransaction(let feeBumpTx): try await stellar.submitTransaction(signedFeeBumpTransaction: feeBumpTx) case .invalidXdrErr: throw ValidationError.invalidArgument(message: "invalid xdr") } ``` ## Submit Transaction :::info It's strongly recommended to use the wallet SDK transaction submission functions instead of Horizon alternatives. The wallet SDK gracefully handles timeout and out-of-fee exceptions. ::: Finally, let's submit a signed transaction to the Stellar network. Note that a sponsored transaction must be signed by both the account and the sponsor. The transaction is automatically re-submitted on the Horizon 504 error (timeout), which indicates a sudden network activity increase. ```kotlin suspend fun signAndSubmit() { val signedTxn = createAccount().sign(sourceAccountKeyPair) wallet.stellar().submitTransaction(signedTxn) } ``` ```typescript const signedTxn = transaction.sign(sourceAccountKeyPair); await wallet.stellar().submitTransaction(signedTxn); ``` ```dart stellar.sign(transaction, sourceAccountKeyPair); bool success = await stellar.submitTransaction(transaction); ``` ```swift stellar.sign(tx: transaction, keyPair: sourceAccountKeyPair) try await stellar.submitTransaction(signedTransaction: transaction) ``` However, the method above doesn't handle fee surge pricing in the network gracefully. If the required fee for a transaction to be included in the ledger becomes too high and transaction expires before making it into the ledger, this method will throw an exception. So, instead, the alternative approach is to: ```kotlin suspend fun submitWithFeeIncrease() { wallet.stellar().submitWithFeeIncrease(sourceAccountKeyPair, Duration.ofSeconds(30), 100u) { this.createAccount(destinationAccountKeyPair) } } ``` ```typescript const buildingFunction = (builder) => builder.transfer(kp.publicKey, new NativeAssetId(), "2"); await stellar.submitWithFeeIncrease({ sourceAddress: kp, timeout: 30, baseFeeIncrease: 100, buildingFunction, }); ``` ```dart bool success = await stellar.submitWithFeeIncrease( sourceAddress: sourceAccountKeyPair, timeout: const Duration(seconds: 30), baseFeeIncrease: 100, maxBaseFee: 2000, buildingFunction: (builder) => builder.transfer(destinationAccountKeyPair.address, NativeAssetId(), "10.0")); ``` ```swift try await stellar.submitWithFeeIncrease( sourceAddress: sourceAccountKeyPair, timeout: 30, baseFeeIncrease: 100, maxBaseFee: 2000, buildingFunction: { (builder) in try! builder .transfer(destinationAddress: destinationAccountKeyPair.address, assetId: NativeAssetId(), amount: 10.0) }) ``` This will create and sign the transaction that originated from the `sourceAccountKeyPair`. Every 30 seconds this function will re-construct this transaction with a new fee (increased by 100 stroops), repeating signing and submitting. Once the transaction is successful, the function will return the transaction body. Note, that any other error will terminate the retry cycle and an exception will be thrown. ## Accessing Horizon SDK It's very simple to use the Horizon SDK connecting to the same Horizon instance as a `Wallet` class. To do so, simply call: ```kotlin val server = wallet.stellar().server ``` ```typescript const server = wallet.stellar().server; ``` ```dart var server = wallet.stellar().server; ``` ```swift let server = wallet.stellar.server ``` And you can work with Horizon Server instance: ```kt val stellarTransaction = server.transactions().transaction("transaction_id") ``` ```typescript const stellarTransaction = server .transactions() .forAccount("account_id") .call(); ``` ```dart var transactions = await server.transactions.forAccount("accountId").execute(); ``` ```swift let transactionsEnum = await server.transactions.getTransactions(forAccount: "accountId") switch transactionsEnum { case .success(let page): let transactions = page.records case .failure(let error): throw error } ``` --- ## ZK Proofs on Stellar X-Ray (Protocol 25) introduced native host functions for zero-knowledge-friendly primitives (BN254 and Poseidon/Poseidon2), marking an important milestone in a long-term strategy to equip developers with the execution-environment infrastructure needed to build compliance-forward, privacy-preserving applications using zero-knowledge cryptography. These primitives are foundational building blocks and do not, on their own, provide end-to-end private payments without additional higher-level protocol or application logic. For more details on X-Ray, see this [blog post](https://stellar.org/blog/developers/announcing-stellar-x-ray-protocol-25). ## BN254 BN254 is a pairing-friendly elliptic curve defined over a 254-bit prime field, commonly used in zero-knowledge proof systems because it supports efficient bilinear pairings. These pairings enable succinct proof constructions where complex statements can be verified quickly on-chain or in constrained environments. BN254 is especially popular in blockchain ecosystems because its arithmetic and pairing operations are relatively efficient to implement and well supported by existing libraries and tooling. While BN254 host functions provide the cryptographic operations needed for proof verification, developers must still generate proofs using higher-level systems (such as circuits written in Noir or Risc0 methods) and deploy verifier smart contracts on Stellar to implement complete zero-knowledge workflows. ### BN254 host functions - `g1_add` — adds two elliptic-curve points in the G1 group, producing a new point. This is commonly used to combine proof or verification values. - `g1_mul` — multiplies a G1 elliptic-curve point by an integer, returning a new point. This operation is a core building block in many proof verification calculations. - `pairing_check` — verifies a pairing equation over lists of G1 and G2 points. This is typically the final step when checking the validity of a BN254 pairing-based proof. ### Resources - [P25 Preview examples](https://github.com/jayz22/soroban-examples/tree/p25-preview/p25-preview) - [Soroban SDK BN254 documentation - types and functions](https://docs.rs/soroban-sdk/latest/soroban_sdk/_migrating/v25_bn254/index.html) - [CAP-74 proposal](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0074.md) - [Noir Ultrahonk Soroban Verifier Contract](https://github.com/indextree/ultrahonk_soroban_contract) - [Noir documentation (circuits)](https://noir-lang.org/docs) - [Risc0 documentation (circuits)](https://dev.risczero.com) ## Poseidon Poseidon is a cryptographic hash function specifically designed for zero-knowledge proof systems, where efficiency inside arithmetic circuits is critical. Unlike traditional hashes such as SHA-256, Poseidon is optimized to minimize the number of constraints required in zero-knowledge circuits by operating natively over finite fields used by zk-SNARKs. This makes it significantly faster and cheaper to prove and verify statements involving hashing, which is why Poseidon is widely used for commitments, Merkle trees, and nullifiers in zero-knowledge applications. The Poseidon host functions expose the underlying _permutation primitives_, not complete hash functions: developers construct the hash function they need on top of a permutation (for example, with a sponge construction), then incorporate it into higher-level proof systems and pair it with Stellar verifier contracts to build end-to-end zero-knowledge application flows. Exposing the permutation, rather than a fixed hash, lets developers configure state size and round parameters to stay interoperable with other ZK systems. ### Poseidon host functions In the Rust `soroban-sdk`, these functions are exposed through the `CryptoHazmat` interface and require enabling the SDK's `hazmat-crypto` feature — they are not accessible with the default SDK features. - `poseidon_permutation` - performs the Poseidon permutation on an input vector of field elements - `poseidon2_permutation` - performs the Poseidon2 permutation on an input vector of field elements ### Resources - [P25 Preview examples](https://github.com/jayz22/soroban-examples/tree/p25-preview/p25-preview) - [Soroban SDK Poseidon documentation](https://docs.rs/soroban-sdk/latest/soroban_sdk/_migrating/v25_poseidon/index.html) - [CAP-75 proposal](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0075.md) :::note Ready-made Poseidon hash functions (built on top of these permutation primitives) live in a separate Rust SDK: [rs-soroban-poseidon](https://github.com/stellar/rs-soroban-poseidon). ::: --- ## Building with AI RavenConnectTabs, SkillsInstallTabs, } from "@site/src/components/AgentSetup"; # Building with AI Stellar provides resources to help AI assistants and Large Language Models (LLMs) understand our documentation, making it easier for you to get accurate answers about Stellar development. ## Raven (MCP server) **Raven** is a remote **Model Context Protocol (MCP)** server for AI agents, hosted at [`raven.stellar.buzz`](https://raven.stellar.buzz). It is open source at the [Raven repository](https://github.com/stellar-experimental/stellar-raven). This is the recommended path: connect your agent once and it gets Stellar docs plus live ecosystem data, cross-referenced into single answers on demand. Raven exposes two MCP tools: - **`search`** — find relevant Stellar docs and ecosystem information. - **`execute`** — run queries against live ecosystem data. Under the hood it wraps Stellar Docs (via Algolia), Lumenloop, Stellar Light, and ecosystem skills, so your agent gets one cross-referenced answer instead of several disconnected sources. Unlike the static [`llms.txt`](#using-llmstxt) file and the installable [Stellar Skills](#stellar-skills) below, Raven is a live, hosted server your agent talks to. All three are useful, and they work well together. :::tip[Try it in your browser] [raven.stellar.buzz/playground](https://raven.stellar.buzz/playground) is a hosted chat UI where you can ask Raven questions directly in the browser, no agent setup required (sign-in required). ::: ### Connect your agent Add the server, then sign in. Sign-in happens inside your own client (Raven uses OAuth), so the first request opens your browser to authorize. For full setup details see [raven.stellar.buzz](https://raven.stellar.buzz). {/* Rendered from src/data/agentTools.ts, shared with the "For agents" panel. */} ## Stellar Skills [**skills.stellar.org**](https://skills.stellar.org/) is the home for Stellar Skills—a collection of AI agent skills that give your coding assistant the right Stellar context before it writes code. They work with any AI agent, and you can browse every skill on the website with no installation required. Skills are open source and maintained in the [stellar/stellar-dev-skill](https://github.com/stellar/stellar-dev-skill) repository. ### Official skills The official skills cover the core areas of Stellar development: | Skill | What it covers | | --- | --- | | **Soroban Smart Contracts** | Writing, testing, securing, and shipping Rust smart contracts | | **Frontend & Wallets** | Building dApps with the JavaScript SDK, wallet integration (Freighter, Stellar Wallets Kit), and smart accounts with passkeys | | **Stellar Assets & SAC** | Classic assets, trustlines, and the Soroban Asset Contract bridge | | **RPC & Horizon APIs** | Querying chain data via modern Stellar RPC or the legacy Horizon endpoints | | **Agent Payments** | Monetizing AI APIs with x402 and the Machine Payments Protocol (MPP) | | **ZK Proofs** | Verifying zero-knowledge proofs on-chain | | **SEPs, CAPs & Ecosystem** | Standards and ecosystem integration guidance | ### Community skills The ecosystem also contributes skills for popular protocols and tools. Browse the full, up-to-date list at [skills.stellar.org](https://skills.stellar.org/). Community skills are maintained by their respective authors, so review each one to confirm it meets your security and maintenance requirements before using it. ### stellar-build For a more hands-on approach, the community-maintained [stellar-build](https://github.com/kaankacar/stellar-build) installer adds 46 skills plus a set of AI personas to Claude Code or Codex with a single command, guiding you through the whole journey—not just building, but also ideation and validation, all the way to launch. ```bash curl -fsSL https://raw.githubusercontent.com/kaankacar/stellar-build/main/install.sh | bash ``` ### Installation Skills can be installed on any AI coding tool that supports the [Agent Skills](https://github.com/anthropics/agent-skills) standard. Each tool reads skills from its own directory: | Platform | Skills directory | | --- | --- | | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | `~/.claude/skills/` | | [OpenCode](https://opencode.ai) | `~/.config/opencode/skills/` | | [OpenAI Codex](https://openai.com/index/openai-codex) | `~/.codex/skills/` | {/* Rendered from src/data/agentTools.ts, shared with the "For agents" panel. */} #### Manual installation [Clone the repository](https://github.com/stellar/stellar-dev-skill) and copy the contents of the `skills/` directory into your agent's skills location (see table above). Once installed, your AI coding assistant will automatically have access to up-to-date Stellar development knowledge when you work on Stellar projects. For other AI coding tools like Cursor, Windsurf, or Aider, you can consider the community-maintained [OpenSkills](https://github.com/numman-ali/openskills) CLI, which aims to bring the Agent Skills system to a wide range of AI coding agents. OpenSkills is a third-party project and not an official Stellar offering, so be sure to review the repository and its documentation to verify that it meets your security and maintenance requirements before using it. ## Using llms.txt [`llms.txt`](https://developers.stellar.org/llms.txt) is a standardized way to provide documentation context to AI systems. This file contains a structured overview of Stellar's developer documentation optimized for LLM consumption. When you ask an AI assistant about Stellar, it can reference this file to: - Understand the structure of our documentation - Find relevant pages for your questions - Provide more accurate, up-to-date answers ## Other AI tools Any AI assistant with custom context features can benefit from Stellar's resources. You can add `llms.txt` to your tool's context settings, or paste its contents directly into conversations to give the AI knowledge about Stellar development. Popular tools like ChatGPT, Claude, Gemini, and Cursor all support adding custom context through their settings. We're actively working on dedicated integrations for more AI tools—check back for updates or follow the [Stellar Skills repository](https://github.com/stellar/stellar-dev-skill) for the latest supported platforms. ## Additional resources - [Raven (stellar-raven)](https://github.com/stellar-experimental/stellar-raven) - Open source remote MCP server for Stellar docs plus live ecosystem data - [Stellar Skills](https://skills.stellar.org) - Browse AI agent skills for Stellar development - [stellar-dev-skill repository](https://github.com/stellar/stellar-dev-skill) - Source and installation for the official skills - [stellar-build](https://github.com/kaankacar/stellar-build) - Community one-command installer with 46 skills and AI personas covering the full journey from ideation and validation to launch - [Stellar Developer Discord](https://discord.gg/stellardev) - Ask questions and get help from the community --- ## How-To Guides This section provides step-by-step instructions to help users complete specific tasks associated with developing on Stellar. These tasks can include instructions for goals related to writing contracts, interacting with contracts, building applications, using Stellar operations, setting up infrastructure, and more. --- ## State Archival Soroban's novel strategy to combat state bloat can present a learning curve for developers. Here are some quick guides that will help you through the process. Read more in the [State Archival section](../../../learn/fundamentals/contract-development/storage/state-archival.mdx). --- ## Create a restoration footprint manually to restore archived data using the JavaScript SDK :::info The manual operation (`RestoreFootprintOp`) is, for the most part, no longer needed starting in Protocol 23, as archived entries included in a transaction's restore list (usually populated during transaction simulation) are [automatically restored](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#contract-data-automatic-restoration) when the `InvokeHostFunctionOp` executes. `RestoreFootprintOp` can be used in rare use cases [described in this section](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#restorefootprintop). ::: In this guide, we'll create a utility method named `createRestorationFootprint` used for manually creating a restoration footprint and restore archived data in a contract. First let's revisit how data is stored in a smart contract with the three different state archival types: | Temporary | Instance | Persistent | | --- | --- | --- | | Cheapest option | Expensive option | Expensive option | | Deleted after TTL reaches `0` | Archived when TTL reaches `0` | Archived when TTL reaches `0` | | Cannot be restored | Can be restored using the `RestoreFootprintOp` operation | Can be restored using the `RestoreFootprintOp` operation | | Unlimited amount of storage | Limited amount of storage available | Unlimited amount of storage | As seen above, all contract data is automatically archived when TTL reaches `0`, except for `Temporary` entries, which are deleted permanently from the ledger. Both types `Instance` and `Persistent` are suitable for storing data that cannot be easily recreated, with the nuance that `Instance` shares the same TTL as the contract instance while `Persistent` does not and, if the contract instance is not archived, `Persistent` data may be archived and need to be restored before invoking the contract. For a detailed explanation of contract data archival, check out the [State Archival section](./../../../learn/fundamentals/contract-development/storage/state-archival.mdx). ```typescript Contract, Networks, Keypair, Operation, TransactionBuilder, xdr, Address, Account, Transaction, } from "@stellar/stellar-sdk"; async function createRestorationFootprint( account: Account, contractAddress: string, fee: string, dataKey: xdr.ScVal, signer: Keypair, ) { // Initialise contract & address const contract: Contract = new Contract(contractAddress); const address: Address = Address.fromString(contract.contractId()); // Setup contract data const contractDataXDR = xdr.LedgerKey.contractData( new xdr.LedgerKeyContractData({ contract: address.toScAddress(), key: dataKey, durability: xdr.ContractDataDurability.persistent(), }), ); // Prepare transaction data const restoreData: xdr.SorobanTransactionData = new xdr.SorobanTransactionData({ resources: new xdr.SorobanResources({ footprint: new xdr.LedgerFootprint({ readOnly: [], readWrite: [contractDataXDR], }), instructions: 0, diskReadBytes: 0, writeBytes: 0, }), resourceFee: xdr.Int64.fromString("0"), // @ts-ignore ext: new xdr.SorobanTransactionDataExt(0), }); // Restore transaction with created restoration footprint const restoreTx: Transaction = new TransactionBuilder(account, { fee: fee }) .setNetworkPassphrase(Networks.TESTNET) .setSorobanData(restoreData) .addOperation(Operation.restoreFootprint({})) .build(); restoreTx.sign(signer); } ``` ## Code walkthrough As we're making use of [Stellar SDK for JavaScript](https://stellar.github.io/js-stellar-sdk) `js-stellar-sdk`, first we import the module. Our function will require the following parameters: - `account`: The Stellar account from which the transaction will be sent. - `contractAddress`: The address of the contract to be restored. - `fee`: The fee for the transaction. - `dataKey`: The key for the contract data to be restored. - `signer`: The keypair that will sign the transaction. After we initialize contract and address instances, we create an XDR representation of the contract data which includes: - `contract`: The contract address. - `key`: The key for the contract data. - `durability`: Specifies that the contract data is persistent - we assume it's `Persistent`. Next up we are preparing the (Soroban) transaction data with: - `resources`: Specifies the resources needed for the transaction. - `footprint`: Defines which parts of the ledger will be read and written. - `instructions`, `diskReadBytes`, `writeBytes`: Sets the resource limits (all set to 0 here). - `resourceFee`: Sets the resource fee to 0 (placeholder). - `ext`: Extension point for future use (set to 0). Note that for restoration footprints we only need to fill in `readWrite`. :::caution The `diskReadBytes`, `writeBytes`, and `resourceFee` values above are zeroed **placeholders** to keep the example focused on footprint construction. A real restoration will not succeed with these left at `0` — Core compares the restored entry's serialized size against the declared resources and rejects the operation (`RESTORE_FOOTPRINT_RESOURCE_LIMIT_EXCEEDED`) when they are too low. Before signing, populate valid resource values by running the transaction through [simulation](../../../learn/fundamentals/contract-development/contract-interactions/transaction-simulation.mdx) (for example with `server.prepareTransaction`), which fills in the required resources and fee for you. ::: The transaction can now be submitted to the Stellar (test) network & signed to restore the specified contract’s data. --- ## Extend a persistent entry and contract using the JavaScript SDK Persistent storage gives you a durable place to keep contract data on the network for a long time. Anything you can't cheaply recreate (user balances, configuration, and the like) belongs there. A contract's instance entry and the Wasm code entry it points to are separate ledger entries, but they live under the same archival rules, so they need the same care. Every persistent entry on the ledger is a key-value pair made up of: - a key (the identifier for the data), - a value (the data itself), and - a [Time To Live](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#ttl) (TTL), which is how many ledgers remain until the entry is no longer live. (The absolute cutoff is stored on the entry itself as its [`liveUntilLedger`](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#live-until-ledger).) Once an entry's TTL runs out, the network archives it. Archived persistent data isn't lost (it can be [restored](./restore-data-js.mdx)), but nothing can read it until that happens. Keeping the TTL topped up before it expires is the cheaper, less exciting path, and that's what we'll walk through here. By the end of this guide you'll have four helper functions you can drop into a project: one that extends a single persistent entry, one for a contract's instance, one for the Wasm code that instance points to, and one that extends all three together in a single operation. ## The extension process 1. Build the ledger key for the entry you want to extend. 2. Build a transaction containing an `extendFootprintTtl` operation with your target TTL, and attach the Soroban transaction data. 3. Prepare the transaction (this runs simulation to fill in the resources and fee for you), sign it with your secret key, and submit it to the network. :::note `extendFootprintTtl` sets a _floor_, not an exact value. Per the [`ExtendFootprintTTLOp` semantics](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#extendfootprintttlop), the operation guarantees each entry in the read-only footprint lives for _at least_ `extendTo` ledgers from now. Entries that already live longer than your target are left alone, so a repeated extension is harmless. There's also a ceiling: an entry can only be extended up to the network's [maximum TTL](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#maximum-ttl), which is a network parameter (check the [network limits](https://lab.stellar.org/network-limits) in the Stellar Lab for the current value). ::: ## Extending a persistent entry The example below assumes your contract keys its data with a symbol, which is the common case. If your contract uses a vec, a map, or a struct as its storage key, build the `ScVal` that matches instead of reaching for `scvSymbol`. ```typescript async function extendPersistentEntryTTL( contractId: string, storageKey: string | Buffer, sourceKeypair: StellarSdk.Keypair, ) { const server = new Server("https://soroban-testnet.stellar.org"); const account = await server.getAccount(sourceKeypair.publicKey()); const fee = "100"; // BASE_FEE of 100 stroops // Create an identifier for the persistent entry const persistentEntry = StellarSdk.xdr.LedgerKey.contractData( new StellarSdk.xdr.LedgerKeyContractData({ contract: StellarSdk.Address.fromString(contractId).toScAddress(), key: StellarSdk.xdr.ScVal.scvSymbol(storageKey), durability: StellarSdk.xdr.ContractDataDurability.persistent(), }), ); // Build the transaction let transaction: StellarSdk.Transaction | StellarSdk.TransactionBuilder = new StellarSdk.TransactionBuilder(account, { fee, networkPassphrase: StellarSdk.Networks.TESTNET, }) .addOperation( StellarSdk.Operation.extendFootprintTtl({ extendTo: 100_000, // The number of ledgers past the LCL (last closed ledger) by which to extend. Roughly 5 days }), ) .setTimeout(30); // Attach Soroban transaction data const sorobanData = new StellarSdk.SorobanDataBuilder() .setReadOnly([persistentEntry]) .build(); transaction.setSorobanData(sorobanData); // Prepare the transaction for signing. transaction = await server.prepareTransaction(transaction.build()); transaction.sign(sourceKeypair); return await server.sendTransaction(transaction); } ``` ## Extending the contract instance `Contract.getFootprint()` hands you the ledger key for the deployed contract instance, so you don't have to assemble that one by hand. :::note This extends the contract _instance_ only. The Wasm code the instance points to is a separate ledger entry with its own TTL, and an `extendFootprintTtl` operation only touches the keys you actually put in the read-only footprint. A live instance backed by archived code still leaves your contract unusable, so the next section covers the code entry too. If you're coming from the Rust side, this is a real difference worth internalizing: inside a contract, `env.storage().instance().extend_ttl()` extends the instance and the code together. The operation used here does not. ::: ```typescript async function extendContractInstanceTTL( contractId: string, sourceKeypair: StellarSdk.Keypair, ) { const server = new Server("https://soroban-testnet.stellar.org"); const account = await server.getAccount(sourceKeypair.publicKey()); const fee = "100"; // Get the ledger key for the contract instance const contract = new StellarSdk.Contract(contractId); const footprint = contract.getFootprint(); // Build the transaction let transaction: StellarSdk.Transaction | StellarSdk.TransactionBuilder = new StellarSdk.TransactionBuilder(account, { fee, networkPassphrase: StellarSdk.Networks.TESTNET, }) .addOperation( StellarSdk.Operation.extendFootprintTtl({ extendTo: 100_000, //The number of ledgers past the LCL by which to extend }), ) .setTimeout(30); // Attach Soroban transaction data const sorobanData = new StellarSdk.SorobanDataBuilder() .setReadOnly([footprint]) .build(); transaction.setSorobanData(sorobanData); // Prepare the transaction transaction = await server.prepareTransaction(transaction.build()); transaction.sign(sourceKeypair); return await server.sendTransaction(transaction); } ``` ## Extending the contract code The code entry is keyed by the Wasm hash rather than by the contract address, and there's no local way to derive one from the other. That means one extra round trip: read the contract's instance entry from the network, pull the Wasm hash out of its executable, and build a `contractCode` ledger key from it. It's worth knowing every contract deployed from the same Wasm shares that single code entry, so extending it benefits all of them at once. ```typescript // Look up the hash of the Wasm code that a deployed contract runs. async function getWasmHash(server: Server, contractId: string) { const contract = new StellarSdk.Contract(contractId); const { entries } = await server.getLedgerEntries(contract.getFootprint()); if (!entries || entries.length === 0) { throw new Error(`No instance entry found for ${contractId}`); } const instance = entries[0].val.contractData().val().instance(); return instance.executable().wasmHash(); } async function extendContractCodeTTL( contractId: string, sourceKeypair: StellarSdk.Keypair, ) { const server = new Server("https://soroban-testnet.stellar.org"); const account = await server.getAccount(sourceKeypair.publicKey()); const fee = "100"; // Create an identifier for the Wasm code entry const codeEntry = StellarSdk.xdr.LedgerKey.contractCode( new StellarSdk.xdr.LedgerKeyContractCode({ hash: await getWasmHash(server, contractId), }), ); // Build the transaction let transaction: StellarSdk.Transaction | StellarSdk.TransactionBuilder = new StellarSdk.TransactionBuilder(account, { fee, networkPassphrase: StellarSdk.Networks.TESTNET, }) .addOperation( StellarSdk.Operation.extendFootprintTtl({ extendTo: 100_000, // The number of ledgers past the LCL by which to extend }), ) .setTimeout(30); // Attach Soroban transaction data const sorobanData = new StellarSdk.SorobanDataBuilder() .setReadOnly([codeEntry]) .build(); transaction.setSorobanData(sorobanData); // Prepare the transaction transaction = await server.prepareTransaction(transaction.build()); transaction.sign(sourceKeypair); return await server.sendTransaction(transaction); } ``` :::caution `getWasmHash` assumes the contract runs an executable that _was deployed_. A [Stellar Asset Contract](../../../tokens/stellar-asset-contract.mdx) instance has a built-in executable instead of a Wasm hash, so `executable().wasmHash()` will throw for one of those. Check `instance.executable().switch()` first if your code has to handle both. ::: ## Extending all three together A Soroban operation has to be the only operation in its transaction, so you might expect to need one transaction per entry. Happily, you don't. A single `ExtendFootprintTTLOp` extends _every_ entry listed in the transaction's read-only footprint, applying the same `extendTo` target to all of them. When a batch of entries should live until the same ledger, list them all in one footprint and extend them together. Here's all three at once, reusing the `getWasmHash` helper from the previous section: ```typescript async function extendContractAndPersistentEntry( contractId: string, storageKey: string | Buffer, sourceKeypair: StellarSdk.Keypair, ) { const server = new Server("https://soroban-testnet.stellar.org"); const account = await server.getAccount(sourceKeypair.publicKey()); const fee = "100"; // Ledger key for the persistent entry... const persistentEntry = StellarSdk.xdr.LedgerKey.contractData( new StellarSdk.xdr.LedgerKeyContractData({ contract: StellarSdk.Address.fromString(contractId).toScAddress(), key: StellarSdk.xdr.ScVal.scvSymbol(storageKey), durability: StellarSdk.xdr.ContractDataDurability.persistent(), }), ); // ...the contract instance... const contract = new StellarSdk.Contract(contractId); // ...and the Wasm code that instance runs. const codeEntry = StellarSdk.xdr.LedgerKey.contractCode( new StellarSdk.xdr.LedgerKeyContractCode({ hash: await getWasmHash(server, contractId), }), ); // A single ExtendFootprintTtl operation extends the whole read-only footprint. let transaction: StellarSdk.Transaction | StellarSdk.TransactionBuilder = new StellarSdk.TransactionBuilder(account, { fee, networkPassphrase: StellarSdk.Networks.TESTNET, }) .addOperation( StellarSdk.Operation.extendFootprintTtl({ extendTo: 100_000, // one shared target, applied to every key below }), ) .setTimeout(30); // List every key to extend together in one read-only footprint. const sorobanData = new StellarSdk.SorobanDataBuilder() .setReadOnly([persistentEntry, contract.getFootprint(), codeEntry]) .build(); transaction.setSorobanData(sorobanData); transaction = await server.prepareTransaction(transaction.build()); transaction.sign(sourceKeypair); return await server.sendTransaction(transaction); } ``` That shared `extendTo` is the one real constraint. Entries that need _different_ target TTLs can't share an operation, so extend each one in its own transaction. That's exactly what the `extendPersistentEntryTTL`, `extendContractInstanceTTL`, and `extendContractCodeTTL` helpers above do: each builds its own operation, so each can carry its own `extendTo` target. :::note Batching keys into one footprint grows the transaction's resource usage, since `diskReadBytes` has to cover the serialized size of every entry in the read-only set. `server.prepareTransaction` works that out for you, but a large enough batch can bump into the network's transaction limits. If that happens, split the batch across a few transactions. ::: Putting one of these to work looks like this: ```typescript const contractId = "CBEE2DHGMYRKJKSJO5E55LBKFMPXE57ZWKTXTCRMC5ANIRJ7IW2Y2WVE"; const storageKey = "BALANCE"; const sourceKeypair = StellarSdk.Keypair.fromSecret( "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", ); extendPersistentEntryTTL(contractId, storageKey, sourceKeypair) .then((result) => console.log("Extension successful:", result)) .catch((error) => console.error("Extension failed:", error)); ``` --- ## Restore a contract using the JavaScript SDK As you can imagine, if your deployed contract instance or the code that backs it is archived, it can't be loaded to execute your invocations. Remember, there's a distinct, one-to-many relationship on the chain between a contract's code and deployed instances of that contract: ```mermaid flowchart LR A[my instance] & B[your instance]--> C[contract WASM] ``` We need **both** to be live for our contract calls to work. Let's work through how these can be recovered. The recovery process is slightly different for a convenient reason: we don't need simulation to figure out the footprints. Instead, we can leverage [`Contract.getFootprint()`](https://stellar.github.io/js-soroban-client/Contract.html#getFootprint), which prepares a footprint with the ledger keys used by a given contract instance (including its backing WASM code). Unfortunately, we still need simulation to figure out the _fees_ for our restoration. This, however, can be easily covered by the SDK's [`Server.prepareTransaction()`](https://stellar.github.io/js-soroban-client/Server.html#prepareTransaction) helper, which will do simulation and assembly for us. :::info This guide makes use of the (aptly named) `submitTx` function we created in [another guide](../transactions/submit-transaction-wait-js.mdx). ::: ```typescript BASE_FEE, Contract, Keypair, Networks, TransactionBuilder, SorobanDataBuilder, Operation } from "@stellar/stellar-sdk"; const server = new Server("https://soroban-testnet.stellar.org"); async function restoreContract( signer: Keypair, c: Contract, ): Promise { const instance = c.getFootprint(); const account = await server.getAccount(signer.publicKey()); const wasmEntry = await server.getLedgerEntries( getWasmLedgerKey(instance) ); const restoreTx = new TransactionBuilder(account, { fee: BASE_FEE }) .setNetworkPassphrase(Networks.TESTNET) .setSorobanData( // Set the restoration footprint (remember, it should be in the // read-write part!) new SorobanDataBuilder().setReadWrite([ instance, wasmEntry ]).build(), ) .addOperation(Operation.restoreFootprint({})) .build(); const preppedTx = await server.prepareTransaction(restoreTx); preppedTx.sign(signer); return submitTx(preppedTx); } function getWasmLedgerKey(entry: xdr.ContractDataEntry): { return xdr.LedgerKey.contractCode( new xdr.LedgerKeyContractCode({ hash: entry.val().instance().wasmHash() }) ); } ``` --- ## Restore archived contract data using the JavaScript SDK :::info The manual operation (`RestoreFootprintOp`) is, for the most part, no longer needed starting in Protocol 23, as archived entries are automatically restored when they appear in a footprint during `InvokeHostFunctionOp`. `RestoreFootprintOp` can be used in rare use cases [described in this section](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#contract-data-automatic-restoration). ::: This is a pretty likely occurrence: my piece of persistent data is archived because I haven't interacted with my contract in a while. How do I make it accessible again? If you find that a piece of persistent data is archived, it can be restored using a Stellar transaction containing a `RestoreFootprintOp` operation. We'll make two assumptions for the sake of this guide: - The contract instance itself is still live (i.e., others have been extending its TTL while you've been away). - You don't know how your archived data is represented on the ledger. The restoration process we'll use involves three discrete steps: 1. Simulate our transaction as we normally would. 2. If the simulation indicated it, we perform restoration with a `RestoreFootprintOp` operation using the hints we got from the simulation. 3. We retry running our initial transaction. Here's a function called `submitOrRestoreAndRetry()` that will take care of all those steps for us: :::info This guide makes use of the (aptly named) `submitTx` function we created in [another guide](../transactions/submit-transaction-wait-js.mdx). ::: ```typescript BASE_FEE, Networks, Keypair, TransactionBuilder, SorobanDataBuilder, rpc as StellarRpc, xdr, } from "@stellar/stellar-sdk"; // add'l imports to submitTx const { Api, assembleTransaction } = StellarRpc; // assume that `server` is the Server() instance from the submitTx async function submitOrRestoreAndRetry( signer: Keypair, tx: Transaction, ): Promise { // We can't use `prepareTransaction` here because we want to do // restoration if necessary, basically assembling the simulation ourselves. const sim = await server.simulateTransaction(tx); // Other failures are out of scope of this tutorial. if (!Api.isSimulationSuccess(sim)) { throw sim; } // If simulation didn't fail, we don't need to restore anything! Just send it. if (!Api.isSimulationRestore(sim)) { const prepTx = assembleTransaction(tx, sim); prepTx.sign(signer); return submitTx(prepTx); } // Build the restoration operation using the RPC server's hints. const account = await server.getAccount(signer.publicKey()); let fee = parseInt(BASE_FEE); fee += parseInt(sim.restorePreamble.minResourceFee); const restoreTx = new TransactionBuilder(account, { fee: fee.toString() }) .setNetworkPassphrase(Networks.TESTNET) .setSorobanData(sim.restorePreamble.transactionData.build()) .addOperation(Operation.restoreFootprint({})) .build(); restoreTx.sign(signer); const resp = await submitTx(restoreTx); if (resp.status !== Api.GetTransactionStatus.SUCCESS) { throw resp; } // now that we've restored the necessary data, we can retry our tx using // the initial data from the simulation (which, hopefully, is still // up-to-date) const retryTxBuilder = TransactionBuilder.cloneFrom(tx, { fee: (parseInt(tx.fee) + parseInt(sim.minResourceFee)).toString(), sorobanData: sim.transactionData.build(), }); // because we consumed a sequence number when restoring, we need to make sure // we set the correct value on this copy retryTxBuilder.source.incrementSequenceNumber(); const retryTx = retryTxBuilder.build(); retryTx.sign(signer); return submitTx(retryTx); } ``` --- ## Test TTL extension logic in smart contracts In order to test contracts that extend the contract data [TTL](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#ttl) via `extend_ttl` storage operations, you can use the TTL getter operation (`get_ttl`) in combination with manipulating the ledger sequence number. Note, that `get_ttl` function is only available for tests and only in Soroban SDK v21+. ## Example Follow along the [example](https://github.com/stellar/soroban-examples/blob/v23.0.0/ttl/src/lib.rs) that tests TTL extensions. The example has extensive comments, this document just highlights the most important parts. We use a very simple contract that only extends an entry for every Soroban storage type: ```rust #[contractimpl] impl TtlContract { /// Creates a contract entry in every kind of storage. pub fn setup(env: Env) { env.storage().persistent().set(&DataKey::MyKey, &0); env.storage().instance().set(&DataKey::MyKey, &1); env.storage().temporary().set(&DataKey::MyKey, &2); } /// Extend the persistent entry TTL to 5000 ledgers, when its /// TTL is smaller than 1000 ledgers. pub fn extend_persistent(env: Env) { env.storage() .persistent() .extend_ttl(&DataKey::MyKey, 1000, 5000); } /// Extend the instance entry TTL to become at least 10000 ledgers, /// when its TTL is smaller than 2000 ledgers. pub fn extend_instance(env: Env) { env.storage().instance().extend_ttl(2000, 10000); } /// Extend the temporary entry TTL to become at least 7000 ledgers, /// when its TTL is smaller than 3000 ledgers. pub fn extend_temporary(env: Env) { env.storage() .temporary() .extend_ttl(&DataKey::MyKey, 3000, 7000); } } ``` The focus of the example is the tests, so the following code snippets come from `test.rs`. It's a good idea to define the custom values of TTL-related network settings, since the defaults are defined by the SDK and aren't immediately obvious for the reader of the tests: ```rust /// Create an environment with specific values of network settings. fn create_env() -> Env { let env = Env::default(); env.ledger().with_mut(|li| { // Current ledger sequence - the TTL is the number of // ledgers from the `sequence_number` (exclusive) until // the last ledger sequence where entry is still considered // alive. li.sequence_number = 100_000; // Minimum TTL for persistent entries - new persistent (and instance) // entries will have this TTL when created. li.min_persistent_entry_ttl = 500; // Minimum TTL for temporary entries - new temporary // entries will have this TTL when created. li.min_temp_entry_ttl = 100; // Maximum TTL of any entry. Note, that entries can have their TTL // extended indefinitely, but each extension can be at most // `max_entry_ttl` ledger from the current `sequence_number`. li.max_entry_ttl = 15000; }); env } ``` You could also use the current [network settings](https://lab.stellar.org/network-limits) when setting up the tests, but keep in mind that these are subject to change, and the contract should be able to work with any values of these settings. Now we run a test scenario that verifies the TTL extension logic (see [`test_extend_ttl_behavior`](https://github.com/stellar/soroban-examples/blob/v23.0.0/ttl/src/test.rs#L38) test for the full scenario). First, we setup the data and ensure that the initial TTL values correspond to the network settings we've defined above: ```rust // Create initial entries and make sure their TTLs correspond to // `min_persistent_entry_ttl` and `min_temp_entry_ttl` values set in // `create_env()`. client.setup(); env.as_contract(&contract_id, || { // Note, that TTL doesn't include the current ledger, but when entry // is created the current ledger is counted towards the number of // ledgers specified by `min_persistent/temp_entry_ttl`, thus // the TTL is 1 ledger less than the respective setting. assert_eq!(env.storage().persistent().get_ttl(&DataKey::MyKey), 499); assert_eq!(env.storage().instance().get_ttl(), 499); assert_eq!(env.storage().temporary().get_ttl(&DataKey::MyKey), 99); }); ``` Notice, that we use `env.as_contract(...)` in order to access the contract's storage. Then we call the TTL extension operations and verify that they behave as expected, for example: ```rust // Extend persistent entry TTL to 5000 ledgers - now it is 5000. client.extend_persistent(); env.as_contract(&contract_id, || { assert_eq!(env.storage().persistent().get_ttl(&DataKey::MyKey), 5000); }); // ... repeat with `client.extend_instance` and `client.extend_temporary` ... ``` In order to test the extension thresholds (i.e. maximum current TTL that requires extension), we need to increase the ledger sequence number: ```rust // Now bump the ledger sequence by 5000 in order to sanity-check // the threshold settings of `extend_ttl` operations. env.ledger().with_mut(|li| { li.sequence_number = 100_000 + 5_000; }); // Now the TTL of every entry has been reduced by 5000 ledgers. env.as_contract(&contract_id, || { assert_eq!(env.storage().persistent().get_ttl(&DataKey::MyKey), 0); assert_eq!(env.storage().instance().get_ttl(), 5000); assert_eq!(env.storage().temporary().get_ttl(&DataKey::MyKey), 2000); }); ``` Then we can extend the entries again and ensure that only entries that are below threshold have been extended (specifically, persistent and temporary entries in this example): ```rust // Extend TTL of all the entries. client.extend_persistent(); client.extend_instance(); client.extend_temporary(); env.as_contract(&contract_id, || { assert_eq!(env.storage().persistent().get_ttl(&DataKey::MyKey), 5000); // Instance TTL hasn't been increased because the remaining TTL // (5000 ledgers) is larger than the threshold used by // `extend_instance` (2000 ledgers) assert_eq!(env.storage().instance().get_ttl(), 5000); assert_eq!(env.storage().temporary().get_ttl(&DataKey::MyKey), 7000); }); ``` Soroban SDK also emulates the behavior for the entries that have their TTL expired. Temporary entries behave 'as if' they were deleted (see [`test_temp_entry_removal`](https://github.com/stellar/soroban-examples/blob/v23.0.0/ttl/src/test.rs#L112) test for the full scenario): ```rust // Extend the temporary entry TTL to 7000 ledgers. client.extend_temporary(); // Bump the ledger sequence by 7001 ledgers (one ledger past TTL). env.ledger().with_mut(|li| { li.sequence_number += 7001; }); // Now the entry is no longer present in the environment. env.as_contract(&contract_id, || { assert_eq!(env.storage().temporary().has(&DataKey::MyKey), false); }); ``` Persistent entries are more subtle: when a transaction that is executed on-chain contains a persistent entry that has been archived (i.e., it has its TTL expired) in the footprint, then the entry will be automatically restored. Automatic restoration is mostly transparent, the main side effect is the increased fees. (see [`test_persistent_entry_auto_restored`](https://github.com/stellar/soroban-examples/blob/v23.0.0/ttl/src/test.rs#L136) test for the full scenario): ```rust // Extend the persistent entry TTL to 5000 ledgers. client.extend_persistent(); // Bump the ledger sequence by 5001 ledgers (one ledger past TTL). env.ledger().with_mut(|li| { li.sequence_number += 5001; }); // Now any call involving the expired persistent data will cause automatic // restoration. client.extend_persistent(); // Notice that disk read bytes and write bytes are increased even though the // function itself is read-only. let resources = env.cost_estimate().resources(); assert!(resources.disk_read_bytes > 0); assert!(resources.write_bytes > 0); assert_eq!(resources.write_entries, 1); ``` ## Testing TTL extension for other contract instances Sometimes a contract may want to extend TTL of another contracts and/or their Wasm entries (usually that would happen in factory contracts). This logic may be covered in a similar fashion to the example above using `env.deployer().get_contract_instance_ttl(&contract)` to get TTL of any contract's instance, and `env.deployer().get_contract_code_ttl(&contract)` to get TTL of any contract's Wasm entry. You can find an example of using these functions in the SDK [test suite](https://github.com/stellar/rs-soroban-sdk/blob/v23.4.1/soroban-sdk/src/tests/storage_testutils.rs#L76). --- ## Contract Authorization Learn about smart contract authorization on Stellar. --- ## Using __check_auth in interesting ways Using __check_auth in interesting ways ## Tutorial 1: time based restriction on token transfers Imagine a multi-sig account contract where certain actions, like transferring tokens, need to be controlled by the time elapsed between consecutive transfers. For example, a token transfer should only be allowed if a certain period of time has passed since the last transfer. This can be useful in scenarios where you want to limit the frequency of transactions to prevent misuse or to implement rate limiting. ### Base concepts #### Time-based restrictions The idea is to enforce a minimum time interval between consecutive token transfers. Each token can have its own time limit, and the contract will track the last transfer time for each token. #### Data storage The contract will store the time limit and last transfer time for each token in its storage to keep track of these values across transactions. ### Code walkthrough #### Contract structure and imports ```rust #![no_std] use soroban_sdk::{ auth::{Context, CustomAccountInterface}, contract, contracterror, contractimpl, contracttype, symbol_short, Address, crypto::Hash, BytesN, Env, Symbol, Vec, }; #[contract] struct AccountContract; #[contracttype] #[derive(Clone)] pub struct Signature { pub public_key: BytesN<32>, pub signature: BytesN<64>, } #[contracttype] #[derive(Clone)] enum DataKey { SignerCnt, Signer(BytesN<32>), TimeLimit(Address), LastTransferTime(Address), } #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum AccError { NotEnoughSigners = 1, NegativeAmount = 2, BadSignatureOrder = 3, UnknownSigner = 4, InvalidContext = 5, TimeLimitExceeded = 6, } const TRANSFER_FN: Symbol = symbol_short!("transfer"); ``` #### Contract initialization 1. Initializes the contract with a list of signers' public keys 2. Stores the count of signers in the contract's storage ```rust #[contractimpl] impl AccountContract { pub fn __constructor(env: Env, signers: Vec>) { for signer in signers.iter() { env.storage().instance().set(&DataKey::Signer(signer), &()); } env.storage() .instance() .set(&DataKey::SignerCnt, &signers.len()); } } ``` #### Setting time limits 1. Allows setting a time limit for a specific token 2. Ensures that only the contract itself can set these limits by requiring the contract's authorization ```rust #[contractimpl] impl AccountContract { pub fn set_time(env: Env, token: Address, time_limit: u64) { env.current_contract_address().require_auth(); env.storage() .instance() .set(&DataKey::TimeLimit(token), &time_limit); } } ``` #### Custom authentication and authorization 1. Authenticates the signatures 2. Checks if all required signers have signed 3. Iterates through the authorization context to verify the authorization policy ```rust #[contractimpl] impl CustomAccountInterface for AccountContract { type Error = AccError; type Signature = Vec; #[allow(non_snake_case)] fn __check_auth( env: Env, signature_payload: Hash<32>, signatures: Vec, auth_context: Vec, ) -> Result<(), AccError> { authenticate(&env, &signature_payload, &signatures)?; let tot_signers: u32 = env .storage() .instance() .get::<_, u32>(&DataKey::SignerCnt) .unwrap(); let all_signed = tot_signers == signatures.len(); let curr_contract = env.current_contract_address(); for context in auth_context.iter() { verify_authorization_policy( &env, &context, &curr_contract, all_signed, )?; } Ok(()) } } ``` #### Authentication logic 1. Verifies that the signatures are in the correct order and that each signer is authorized 2. Uses [ed25519_verify](https://docs.rs/soroban-sdk/latest/soroban_sdk/crypto/struct.Crypto.html#method.ed25519_verify) function to verify each signature ```rust fn authenticate( env: &Env, signature_payload: &Hash<32>, signatures: &Vec, ) -> Result<(), AccError> { for i in 0..signatures.len() { let signature = signatures.get_unchecked(i); if i > 0 { let prev_signature = signatures.get_unchecked(i - 1); if prev_signature.public_key >= signature.public_key { return Err(AccError::BadSignatureOrder); } } if !env .storage() .instance() .has(&DataKey::Signer(signature.public_key.clone())) { return Err(AccError::UnknownSigner); } env.crypto().ed25519_verify( &signature.public_key, &signature_payload.clone().into(), &signature.signature, ); } Ok(()) } ``` #### Authorization policy verification 1. Checks if the function being called is a transfer or approve function 2. Enforces the time-based restriction by comparing the current time with the last transfer time 3. Updates the last transfer time if the transfer is allowed ```rust fn verify_authorization_policy( env: &Env, context: &Context, curr_contract: &Address, all_signed: bool, ) -> Result<(), AccError> { let contract_context = match context { Context::Contract(c) => { if &c.contract == curr_contract { if !all_signed { return Err(AccError::NotEnoughSigners); } } c } Context::CreateContractHostFn(_) => return Err(AccError::InvalidContext), Context::CreateContractWithCtorHostFn(_) => return Err(AccError::InvalidContext), }; if contract_context.fn_name != TRANSFER_FN && contract_context.fn_name != Symbol::new(env, "approve") { return Ok(()); } let current_time = env.ledger().timestamp(); let time_limit: Option = env.storage() .instance() .get::<_, u64>(&DataKey::TimeLimit(contract_context.contract.clone())); if let Some(limit) = time_limit { let last_transfer_time: u64 = env.storage() .instance() .get::<_, u64>(&DataKey::LastTransferTime(contract_context.contract.clone())) .unwrap_or(0); if current_time - last_transfer_time < limit { return Err(AccError::TimeLimitExceeded); } env.storage() .instance() .set(&DataKey::LastTransferTime(contract_context.contract.clone()), ¤t_time); } Ok(()) } ``` #### Summary The contract begins by initializing with a set of authorized signers. It allows setting a time limit for token transfers, which controls how frequently a token can be transferred. The \_\_check_auth function is the core of the authorization process, ensuring that all necessary signatures are valid and checking the time-based restriction for token transfers. If the required time has not passed since the last transfer, the contract will deny the operation, enforcing the desired rate limiting. By tracking the last transfer time and enforcing a minimum time interval between transfers, the contract effectively limits the frequency of token transfers, resolving the issue of potential abuse through rapid consecutive transactions. #### Complete code Here are all the snippets stacked together in a single file for convenience: ```rust #![no_std] use soroban_sdk::{ auth::{Context, CustomAccountInterface}, contract, contracterror, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Hash, Symbol, Vec, }; #[contract] struct AccountContract; #[contracttype] #[derive(Clone)] pub struct Signature { pub public_key: BytesN<32>, pub signature: BytesN<64>, } #[contracttype] #[derive(Clone)] enum DataKey { SignerCnt, Signer(BytesN<32>), TimeLimit(Address), LastTransferTime(Address), } #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum AccError { NotEnoughSigners = 1, NegativeAmount = 2, BadSignatureOrder = 3, UnknownSigner = 4, InvalidContext = 5, TimeLimitExceeded = 6, } const TRANSFER_FN: Symbol = symbol_short!("transfer"); #[contractimpl] impl AccountContract { pub fn __constructor(env: Env, signers: Vec>) { for signer in signers.iter() { env.storage().instance().set(&DataKey::Signer(signer), &()); } env.storage() .instance() .set(&DataKey::SignerCnt, &signers.len()); } pub fn set_time(env: Env, token: Address, time_limit: u64) { env.current_contract_address().require_auth(); env.storage() .instance() .set(&DataKey::TimeLimit(token), &time_limit); } } #[contractimpl] impl CustomAccountInterface for AccountContract { type Error = AccError; type Signature = Vec; #[allow(non_snake_case)] fn __check_auth( env: Env, signature_payload: Hash<32>, signatures: Vec, auth_context: Vec, ) -> Result<(), AccError> { authenticate(&env, &signature_payload, &signatures)?; let tot_signers: u32 = env .storage() .instance() .get::<_, u32>(&DataKey::SignerCnt) .unwrap(); let all_signed = tot_signers == signatures.len(); let curr_contract = env.current_contract_address(); for context in auth_context.iter() { verify_authorization_policy( &env, &context, &curr_contract, all_signed, )?; } Ok(()) } } fn authenticate( env: &Env, signature_payload: &Hash<32>, signatures: &Vec, ) -> Result<(), AccError> { for i in 0..signatures.len() { let signature = signatures.get_unchecked(i); if i > 0 { let prev_signature = signatures.get_unchecked(i - 1); if prev_signature.public_key >= signature.public_key { return Err(AccError::BadSignatureOrder); } } if !env .storage() .instance() .has(&DataKey::Signer(signature.public_key.clone())) { return Err(AccError::UnknownSigner); } env.crypto().ed25519_verify( &signature.public_key, &signature_payload.clone().into(), &signature.signature, ); } Ok(()) } fn verify_authorization_policy( env: &Env, context: &Context, curr_contract: &Address, all_signed: bool, ) -> Result<(), AccError> { let contract_context = match context { Context::Contract(c) => { if &c.contract == curr_contract { if !all_signed { return Err(AccError::NotEnoughSigners); } } c } Context::CreateContractHostFn(_) => return Err(AccError::InvalidContext), Context::CreateContractWithCtorHostFn(_) => return Err(AccError::InvalidContext), }; if contract_context.fn_name != TRANSFER_FN && contract_context.fn_name != Symbol::new(env, "approve") { return Ok(()); } let current_time = env.ledger().timestamp(); let time_limit: Option = env.storage() .instance() .get::<_, u64>(&DataKey::TimeLimit(contract_context.contract.clone())); if let Some(limit) = time_limit { let last_transfer_time: u64 = env.storage() .instance() .get::<_, u64>(&DataKey::LastTransferTime(contract_context.contract.clone())) .unwrap_or(0); if current_time - last_transfer_time < limit { return Err(AccError::TimeLimitExceeded); } env.storage() .instance() .set(&DataKey::LastTransferTime(contract_context.contract.clone()), ¤t_time); } Ok(()) } mod test; ``` These are the test cases: ```rust #![cfg(test)] extern crate std; use ed25519_dalek::Keypair; use ed25519_dalek::Signer; use rand::thread_rng; use soroban_sdk::auth::ContractContext; use soroban_sdk::symbol_short; use soroban_sdk::testutils::Address as _; use soroban_sdk::testutils::AuthorizedFunction; use soroban_sdk::testutils::AuthorizedInvocation; use soroban_sdk::Val; use soroban_sdk::{ auth::Context, testutils::BytesN as _, vec, Address, BytesN, Env, IntoVal, Symbol, }; use soroban_sdk::testutils::Ledger; use soroban_sdk::testutils::LedgerInfo; use crate::AccError; use crate::{AccountContract, AccountContractClient, Signature}; fn generate_keypair() -> Keypair { Keypair::generate(&mut thread_rng()) } fn signer_public_key(e: &Env, signer: &Keypair) -> BytesN<32> { signer.public.to_bytes().into_val(e) } fn create_account_contract(e: &Env, signers: Vec>) -> AccountContractClient { AccountContractClient::new(e, &e.register(AccountContract {}, (signers,))) } fn sign(e: &Env, signer: &Keypair, payload: &BytesN<32>) -> Val { Signature { public_key: signer_public_key(e, signer), signature: signer .sign(payload.to_array().as_slice()) .to_bytes() .into_val(e), } .into_val(e) } fn token_auth_context(e: &Env, token_id: &Address, fn_name: Symbol, amount: i128) -> Context { Context::Contract(ContractContext { contract: token_id.clone(), fn_name, args: ((), (), amount).into_val(e), }) } #[test] fn test_token_auth() { let env = Env::default(); env.mock_all_auths(); let mut signers = [generate_keypair(), generate_keypair()]; if signers[0].public.as_bytes() > signers[1].public.as_bytes() { signers.swap(0, 1); } let account_contract = create_account_contract( &env, vec![ &env, signer_public_key(&env, &signers[0]), signer_public_key(&env, &signers[1]), ], ); let payload = BytesN::random(&env); let token = Address::generate(&env); env.try_invoke_contract_check_auth::( &account_contract.address, &payload, vec![&env, sign(&env, &signers[0], &payload)].into(), &vec![ &env, token_auth_context(&env, &token, Symbol::new(&env, "transfer"), 1000), ], ) .unwrap(); env.try_invoke_contract_check_auth::( &account_contract.address, &payload, vec![&env, sign(&env, &signers[0], &payload)].into(), &vec![ &env, token_auth_context(&env, &token, Symbol::new(&env, "transfer"), 1000), ], ) .unwrap(); // Add a time limit of 1000 seconds for the token. account_contract.set_time(&token, &1000); assert_eq!( env.auths(), std::vec![( account_contract.address.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( account_contract.address.clone(), symbol_short!("set_time"), (token.clone(), 1000_u64).into_val(&env), )), sub_invocations: std::vec![] } )] ); // Attempting a transfer within the time limit should fail. env.ledger().set(LedgerInfo { timestamp: 0, protocol_version: 1, sequence_number: 10, network_id: Default::default(), base_reserve: 10, min_temp_entry_ttl: 16, min_persistent_entry_ttl: 16, max_entry_ttl: 100_000, }); env.try_invoke_contract_check_auth::( &account_contract.address, &payload, vec![&env, sign(&env, &signers[0], &payload)].into(), &vec![ &env, token_auth_context(&env, &token, Symbol::new(&env, "transfer"), 1001) ], ) .err() .unwrap() .unwrap() == AccError::TimeLimitExceeded; // Simulate passing of time to allow the next transfer. env.ledger().set(LedgerInfo { timestamp: 1000, protocol_version: 1, sequence_number: 10, network_id: Default::default(), base_reserve: 10, min_temp_entry_ttl: 16, min_persistent_entry_ttl: 16, max_entry_ttl: 100_000, }); env.try_invoke_contract_check_auth::( &account_contract.address, &payload, vec![&env, sign(&env, &signers[0], &payload)].into(), &vec![ &env, token_auth_context(&env, &token, Symbol::new(&env, "transfer"), 1001), ], ) .unwrap(); } ``` ## Tutorial 2: implementing a smart wallet (WebAuthn) Imagine a world where traditional passwords are obsolete. In this world, WebAuthn (Web Authentication) has become the standard for secure online interactions. Alice, a blockchain enthusiast, wants to create a wallet that leverages WebAuthn technology for enhanced security. She decides to implement a WebAuthn wallet on Stellar, allowing users to manage their digital assets using their device's biometric features or security keys (e.g., YubiKey, Google Titan Security Key, etc.). ### Base concepts WebAuthn is a web standard for passwordless authentication. It allows users to authenticate using biometrics (like fingerprints or facial recognition). The WebAuthn wallet implemented in this tutorial will be able to: 1. Register and manage user credentials (public keys) 2. Authenticate users for transaction signing 3. Differentiate between admin and regular users 4. Allow contract updates for future improvements :::info This tutorial's code credit goes to [@kalepail's](https://github.com/kalepail) work on passkeys, which you can explore more [here](https://github.com/kalepail/passkey-kit). ::: ### Code walkthrough #### Contract structure and imports This section sets up the contract structure and imports necessary components from the Soroban SDK. ```rust #![no_std] use soroban_sdk::{ auth::{Context, CustomAccountInterface}, contract, contracterror, contractimpl, contracttype, crypto::Hash, panic_with_error, symbol_short, Bytes, BytesN, Env, FromVal, Symbol, Vec, }; #[contract] pub struct Contract; ``` #### Error definitions This enum defines possible errors that can occur during contract execution. ```rust #[contracterror] #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum Error { NotFound = 1, NotPermitted = 2, ClientDataJsonChallengeIncorrect = 3, Secp256r1PublicKeyParse = 4, Secp256r1SignatureParse = 5, Secp256r1VerifyFailed = 6, JsonParseError = 7, } ``` #### Core contract functions These functions handle adding and removing signers, updating the contract, and managing admin counts. ```rust // Implementing the Contract struct with various methods #[contractimpl] impl Contract { // Method to add a new signer, potentially as an admin pub fn add(env: Env, id: Bytes, pk: BytesN<65>, mut admin: bool) -> Result<(), Error> { // Check if the instance storage has the ADMIN_SIGNER_COUNT key if env.storage().instance().has(&ADMIN_SIGNER_COUNT) { // Require authentication from the current contract address env.current_contract_address().require_auth(); } else { // If it's the first signer, ensure they are an admin admin = true; } // Get the maximum time-to-live (TTL) for the storage entries let max_ttl = env.storage().max_ttl(); // If the signer is an admin if admin { // Check if the ID exists in temporary storage if env.storage().temporary().has(&id) { // Remove the ID from temporary storage env.storage().temporary().remove(&id); } // Update the admin signer count by incrementing it Self::update_admin_signer_count(&env, true); // Store the public key in persistent storage with the given ID env.storage().persistent().set(&id, &pk); // Extend the TTL for the persistent storage entry env.storage() .persistent() .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl); } else { // If the signer is not an admin // Check if the ID exists in persistent storage if env.storage().persistent().has(&id) { // Update the admin signer count by decrementing it Self::update_admin_signer_count(&env, false); // Remove the ID from persistent storage env.storage().persistent().remove(&id); } // Store the public key in temporary storage with the given ID env.storage().temporary().set(&id, &pk); // Extend the TTL for the temporary storage entry env.storage() .temporary() .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl); } // Extend the TTL for the instance storage env.storage() .instance() .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl); // Publish an event indicating a new signer has been added env.events() .publish((EVENT_TAG, symbol_short!("add"), id, pk), admin); // Return Ok indicating success Ok(()) } // Method to remove a signer pub fn remove(env: Env, id: Bytes) -> Result<(), Error> { // Require authentication from the current contract address env.current_contract_address().require_auth(); // Check if the ID exists in temporary storage if env.storage().temporary().has(&id) { // Remove the ID from temporary storage env.storage().temporary().remove(&id); } else if env.storage().persistent().has(&id) { // If the ID exists in persistent storage, decrement the admin signer count Self::update_admin_signer_count(&env, false); // Remove the ID from persistent storage env.storage().persistent().remove(&id); } // Get the maximum time-to-live (TTL) for the storage entries let max_ttl = env.storage().max_ttl(); // Extend the TTL for the instance storage env.storage() .instance() .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl); // Publish an event indicating a signer has been removed env.events() .publish((EVENT_TAG, symbol_short!("remove"), id), ()); // Return Ok indicating success Ok(()) } // Method to update the contract with new WASM code pub fn update(env: Env, hash: BytesN<32>) -> Result<(), Error> { // Require authentication from the current contract address env.current_contract_address().require_auth(); // Update the contract's WASM code with the new hash env.deployer().update_current_contract_wasm(hash); // Get the maximum time-to-live (TTL) for the storage entries let max_ttl = env.storage().max_ttl(); // Extend the TTL for the instance storage env.storage() .instance() .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl); // Return Ok indicating success Ok(()) } // Helper method to update the count of admin signers fn update_admin_signer_count(env: &Env, add: bool) { // Get the current count of admin signers from instance storage, defaulting to 0 let count = env .storage() .instance() .get::(&ADMIN_SIGNER_COUNT) .unwrap_or(0) + if add { 1 } else { -1 }; // If the count is less than or equal to 0, trigger an error if count <= 0 { panic_with_error!(env, Error::NotPermitted) } // Update the admin signer count in instance storage env.storage() .instance() .set::(&ADMIN_SIGNER_COUNT, &count); } } ``` #### Signature structure This structure represents a WebAuthn signature. ```rust #[contracttype] pub struct Signature { pub id: Bytes, pub authenticator_data: Bytes, pub client_data_json: Bytes, pub signature: BytesN<64>, } ``` #### CustomAccountInterface implementation This implements the core authentication logic for the WebAuthn wallet. ```rust #[contractimpl] impl CustomAccountInterface for Contract { // Defining the error and signature types for the trait type Error = Error; type Signature = Signature; #[allow(non_snake_case)] fn __check_auth( env: Env, // The environment context signature_payload: Hash<32>, // The payload that needs to be signed signature: Signature, // The signature provided by the client auth_contexts: Vec, // Contexts for authentication ) -> Result<(), Error> { // Destructure the signature into its components let Signature { id, mut authenticator_data, client_data_json, signature, } = signature; // Set the maximum time-to-live (TTL) for storage entries let max_ttl = env.storage().max_ttl(); // Try to retrieve the public key associated with the id from temporary storage let pk = match env.storage().temporary().get(&id) { Some(pk) => { // Check if a session signer is trying to perform protected actions for context in auth_contexts.iter() { match context { // If the context is a contract call Context::Contract(c) => { // Ensure that the current contract is not performing restricted actions if c.contract == env.current_contract_address() && (c.fn_name != symbol_short!("remove") || (c.fn_name == symbol_short!("remove") && Bytes::from_val(&env, &c.args.get(0).unwrap()) != id)) { return Err(Error::NotPermitted); } } _ => {} // Allow other contexts (e.g., deploying new contracts) }; } // Extend the TTL for the temporary storage entry env.storage() .temporary() .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl); pk // Return the public key } // If not found in temporary storage, try persistent storage None => { env.storage() .persistent() .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl); env.storage().persistent().get(&id).ok_or(Error::NotFound)? } }; // Extend the authenticator data with the SHA-256 hash of the client data JSON authenticator_data.extend_from_array(&env.crypto().sha256(&client_data_json).to_array()); // Verify the signature using the secp256r1 elliptic curve algorithm env.crypto() .secp256r1_verify(&pk, &env.crypto().sha256(&authenticator_data), &signature); // Parse the client data JSON, extracting the base64 URL encoded challenge let client_data_json = client_data_json.to_buffer::<1024>(); let client_data_json = client_data_json.as_slice(); let (client_data_json, _): (ClientDataJson, _) = serde_json_core::de::from_slice(client_data_json).map_err(|_| Error::JsonParseError)?; // Build the expected challenge from the signature payload let mut expected_challenge = [0u8; 43]; base64_url::encode(&mut expected_challenge, &signature_payload.to_array()); // Check that the challenge inside the client data JSON matches the expected challenge if client_data_json.challenge.as_bytes() != expected_challenge { return Err(Error::ClientDataJsonChallengeIncorrect); } // Extend the TTL for the instance storage env.storage() .instance() .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl); Ok(()) // Return success } } ``` #### Base64 url encoding This function provides Base64 URL encoding functionality used in the WebAuthn process. ```rust // Define the Base64 URL alphabet as a constant byte array. const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; // The `encode` function takes a mutable reference to a destination byte array `dst` and a source byte array `src`. pub fn encode(dst: &mut [u8], src: &[u8]) { // Initialize destination index `di` and source index `si` to 0. let mut di: usize = 0; let mut si: usize = 0; // Calculate the length of the source array that is a multiple of 3. let n = (src.len() / 3) * 3;- // Process the source array in chunks of 3 bytes. while si < n { // Combine 3 bytes into a single 24-bit value. let val = (src[si] as usize) << 16 | (src[si + 1] as usize) << 8 | (src[si + 2] as usize); // Encode the 24-bit value into 4 Base64 characters. dst[di] = ALPHABET[val >> 18 & 0x3F]; dst[di + 1] = ALPHABET[val >> 12 & 0x3F]; dst[di + 2] = ALPHABET[val >> 6 & 0x3F]; dst[di + 3] = ALPHABET[val & 0x3F]; // Increment the source index by 3 and the destination index by 4. si += 3; di += 4; } // Calculate the remaining number of bytes in the source array. let remain = src.len() - si; // If there are no remaining bytes, return early. if remain == 0 { return; } // Initialize a 24-bit value with the remaining byte(s). let mut val = (src[si] as usize) << 16; // If there are 2 remaining bytes, add the second byte to the 24-bit value. if remain == 2 { val |= (src[si + 1] as usize) << 8; } // Encode the remaining bytes into 2 or 3 Base64 characters. dst[di] = ALPHABET[val >> 18 & 0x3F]; dst[di + 1] = ALPHABET[val >> 12 & 0x3F]; // If there are 2 remaining bytes, encode the third Base64 character. if remain == 2 { dst[di + 2] = ALPHABET[val >> 6 & 0x3F]; } } ``` #### Written explanation of code The WebAuthn wallet contract manages user credentials and authentication. It allows adding and removing signers, distinguishing between admin and regular users. The add function registers new signers, storing admin keys persistently and regular keys temporarily. The remove function deletes signers, and update allows contract upgrades. The core of the wallet's security is the `__check_auth` function, which verifies WebAuthn signatures. It checks the signature against the stored public key, verifies the client data JSON, and ensures the challenge matches the expected value. The contract uses Soroban's storage capabilities to manage keys and admin counts, with different TTLs (Time To Live) for persistent and temporary storage. #### Test cases These are test cases to ensure our WebAuthn wallet is functioning correctly. We'll use the Soroban SDK's testing utilities to create and run these tests. ```rust #[cfg(test)] mod test { use std::println; extern crate std; use soroban_sdk::{ vec, Bytes, BytesN, Env, IntoVal, }; use crate::{Contract, ContractClient, Error, Signature}; #[test] fn test() { let env = Env::default(); let deployee_address = env.register(Contract, ()); let deployee_client = ContractClient::new(&env, &deployee_address); // Test data let id = Bytes::from_array( &env, &[243, 248, 216, 74, 226, 218, 85, 102, 196, 167, 14, 151, 124, 42, 73, 136, 138, 102, 187, 140], ); let pk = BytesN::from_array( &env, &[4, 163, 142, 245, 242, 113, 55, 104, 189, 52, 128, 238, 206, 174, 194, 177, 4, 100, 161, 243, 177, 255, 10, 53, 57, 194, 205, 45, 208, 10, 131, 167, 93, 44, 123, 126, 95, 219, 207, 230, 175, 90, 96, 41, 121, 197, 127, 180, 74, 236, 160, 0, 60, 185, 211, 174, 133, 215, 200, 208, 230, 51, 210, 94, 214], ); // Test adding a signer deployee_client.add(&id, &pk, &true); // Test authentication let signature_payload = BytesN::from_array( &env, &[150, 22, 248, 96, 91, 4, 111, 72, 170, 101, 57, 225, 210, 199, 91, 29, 159, 227, 209, 6, 231, 63, 222, 209, 232, 57, 112, 98, 140, 118, 206, 245], ); let signature = Signature { authenticator_data: Bytes::from_array( &env, &[75, 74, 206, 229, 181, 139, 119, 89, 254, 159, 95, 149, 227, 164, 109, 143, 188, 228, 143, 219, 181, 216, 77, 123, 142, 172, 60, 20, 162, 154, 181, 187, 29, 0, 0, 0, 0], ), client_data_json: Bytes::from_array( &env, &[123, 34, 116, 121, 112, 101, 34, 58, 34, 119, 101, 98, 97, 117, 116, 104, 110, 46, 103, 101, 116, 34, 44, 34, 99, 104, 97, 108, 108, 101, 110, 103, 101, 34, 58, 34, 108, 104, 98, 52, 89, 70, 115, 69, 98, 48, 105, 113, 90, 84, 110, 104, 48, 115, 100, 98, 72, 90, 95, 106, 48, 81, 98, 110, 80, 57, 55, 82, 54, 68, 108, 119, 89, 111, 120, 50, 122, 118, 85, 34, 44, 34, 111, 114, 105, 103, 105, 110, 34, 58, 34, 104, 116, 116, 112, 115, 58, 47, 47, 112, 97, 115, 115, 107, 101, 121, 45, 107, 105, 116, 45, 100, 101, 109, 111, 46, 112, 97, 103, 101, 115, 46, 100, 101, 118, 34, 125], ), id: id.clone(), signature: BytesN::from_array( &env, &[74, 48, 29, 120, 181, 135, 255, 178, 105, 76, 82, 118, 29, 135, 193, 72, 123, 144, 138, 214, 125, 27, 33, 159, 169, 200, 151, 55, 7, 250, 111, 172, 86, 89, 162, 167, 148, 105, 144, 68, 21, 249, 61, 253, 80, 61, 54, 29, 14, 162, 12, 173, 206, 194, 144, 227, 11, 225, 74, 254, 191, 221, 103, 86], ), }; let result: Result<(), Result> = env.try_invoke_contract_check_auth( &deployee_address, &signature_payload, signature.into_val(&env), &vec![&env], ); println!("{:?}", result); assert!(result.is_ok()); } } ``` #### Summary This WebAuthn wallet implementation provides a secure and user-friendly way to manage digital assets on Stellar. It leverages the security benefits of WebAuthn while maintaining the flexibility needed for blockchain interactions. #### Complete code Here are all the snippets stacked together in a single file for convenience: ```rust #![no_std] use soroban_sdk::{ auth::{Context, CustomAccountInterface}, contract, contracterror, contractimpl, contracttype, crypto::Hash, panic_with_error, symbol_short, Bytes, BytesN, Env, FromVal, Symbol, Vec, }; #[contract] pub struct Contract; // Error definitions for the contract #[contracterror] #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum Error { NotFound = 1, NotPermitted = 2, ClientDataJsonChallengeIncorrect = 3, Secp256r1PublicKeyParse = 4, Secp256r1SignatureParse = 5, Secp256r1VerifyFailed = 6, JsonParseError = 7, } // Structure representing a WebAuthn signature #[contracttype] pub struct Signature { pub id: Bytes, pub authenticator_data: Bytes, pub client_data_json: Bytes, pub signature: BytesN<64>, } // Implementing the Contract struct with various methods #[contractimpl] impl Contract { // Method to add a new signer, potentially as an admin pub fn add(env: Env, id: Bytes, pk: BytesN<65>, mut admin: bool) -> Result<(), Error> { // Check if the instance storage has the ADMIN_SIGNER_COUNT key if env.storage().instance().has(&ADMIN_SIGNER_COUNT) { // Require authentication from the current contract address env.current_contract_address().require_auth(); } else { // If it's the first signer, ensure they are an admin admin = true; } // Get the maximum time-to-live (TTL) for the storage entries let max_ttl = env.storage().max_ttl(); // If the signer is an admin if admin { // Check if the ID exists in temporary storage if env.storage().temporary().has(&id) { // Remove the ID from temporary storage env.storage().temporary().remove(&id); } // Update the admin signer count by incrementing it Self::update_admin_signer_count(&env, true); // Store the public key in persistent storage with the given ID env.storage().persistent().set(&id, &pk); // Extend the TTL for the persistent storage entry env.storage() .persistent() .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl); } else { // If the signer is not an admin // Check if the ID exists in persistent storage if env.storage().persistent().has(&id) { // Update the admin signer count by decrementing it Self::update_admin_signer_count(&env, false); // Remove the ID from persistent storage env.storage().persistent().remove(&id); } // Store the public key in temporary storage with the given ID env.storage().temporary().set(&id, &pk); // Extend the TTL for the temporary storage entry env.storage() .temporary() .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl); } // Extend the TTL for the instance storage env.storage() .instance() .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl); // Publish an event indicating a new signer has been added env.events() .publish((EVENT_TAG, symbol_short!("add"), id, pk), admin); // Return Ok indicating success Ok(()) } // Method to remove a signer pub fn remove(env: Env, id: Bytes) -> Result<(), Error> { // Require authentication from the current contract address env.current_contract_address().require_auth(); // Check if the ID exists in temporary storage if env.storage().temporary().has(&id) { // Remove the ID from temporary storage env.storage().temporary().remove(&id); } else if env.storage().persistent().has(&id) { // If the ID exists in persistent storage, decrement the admin signer count Self::update_admin_signer_count(&env, false); // Remove the ID from persistent storage env.storage().persistent().remove(&id); } // Get the maximum time-to-live (TTL) for the storage entries let max_ttl = env.storage().max_ttl(); // Extend the TTL for the instance storage env.storage() .instance() .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl); // Publish an event indicating a signer has been removed env.events() .publish((EVENT_TAG, symbol_short!("remove"), id), ()); // Return Ok indicating success Ok(()) } // Method to update the contract with new WASM code pub fn update(env: Env, hash: BytesN<32>) -> Result<(), Error> { // Require authentication from the current contract address env.current_contract_address().require_auth(); // Update the contract's WASM code with the new hash env.deployer().update_current_contract_wasm(hash); // Get the maximum time-to-live (TTL) for the storage entries let max_ttl = env.storage().max_ttl(); // Extend the TTL for the instance storage env.storage() .instance() .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl); // Return Ok indicating success Ok(()) } // Helper method to update the count of admin signers fn update_admin_signer_count(env: &Env, add: bool) { // Get the current count of admin signers from instance storage, defaulting to 0 let count = env .storage() .instance() .get::(&ADMIN_SIGNER_COUNT) .unwrap_or(0) + if add { 1 } else { -1 }; // If the count is less than or equal to 0, trigger an error if count <= 0 { panic_with_error!(env, Error::NotPermitted) } // Update the admin signer count in instance storage env.storage() .instance() .set::(&ADMIN_SIGNER_COUNT, &count); } } // Implementing the core authentication logic for the WebAuthn wallet #[contractimpl] impl CustomAccountInterface for Contract { // Defining the error and signature types for the trait type Error = Error; type Signature = Signature; #[allow(non_snake_case)] fn __check_auth( env: Env, // The environment context signature_payload: Hash<32>, // The payload that needs to be signed signature: Signature, // The signature provided by the client auth_contexts: Vec, // Contexts for authentication ) -> Result<(), Error> { // Destructure the signature into its components let Signature { id, mut authenticator_data, client_data_json, signature, } = signature; // Get the maximum time-to-live (TTL) for storage entries let max_ttl = env.storage().max_ttl(); // Try to retrieve the public key associated with the id from temporary storage let pk = match env.storage().temporary().get(&id) { Some(pk) => { // Check if a session signer is trying to perform protected actions for context in auth_contexts.iter() { match context { // If the context is a contract call Context::Contract(c) => { // Ensure that the current contract is not performing restricted actions if c.contract == env.current_contract_address() && (c.fn_name != symbol_short!("remove") || (c.fn_name == symbol_short!("remove") && Bytes::from_val(&env, &c.args.get(0).unwrap()) != id)) { return Err(Error::NotPermitted); } } _ => {} // Allow other contexts (e.g., deploying new contracts) }; } // Extend the TTL for the temporary storage entry env.storage() .temporary() .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl); pk // Return the public key } // If not found in temporary storage, try persistent storage None => { env.storage() .persistent() .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl); env.storage().persistent().get(&id).ok_or(Error::NotFound)? } }; // Extend the authenticator data with the SHA-256 hash of the client data JSON authenticator_data.extend_from_array(&env.crypto().sha256(&client_data_json).to_array()); // Verify the signature using the secp256r1 elliptic curve algorithm env.crypto() .secp256r1_verify(&pk, &env.crypto().sha256(&authenticator_data), &signature); // Parse the client data JSON, extracting the base64 URL encoded challenge let client_data_json = client_data_json.to_buffer::<1024>(); let client_data_json = client_data_json.as_slice(); let (client_data_json, _): (ClientDataJson, _) = serde_json_core::de::from_slice(client_data_json).map_err(|_| Error::JsonParseError)?; // Build the expected challenge from the signature payload let mut expected_challenge = [0u8; 43]; base64_url::encode(&mut expected_challenge, &signature_payload.to_array()); // Check that the challenge inside the client data JSON matches the expected challenge if client_data_json.challenge.as_bytes() != expected_challenge { return Err(Error::ClientDataJsonChallengeIncorrect); } // Extend the TTL for the instance storage env.storage() .instance() .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl); Ok(()) // Return success } } // Define the Base64 URL alphabet as a constant byte array. const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; // The `encode` function takes a mutable reference to a destination byte array `dst` and a source byte array `src`. pub fn encode(dst: &mut [u8], src: &[u8]) { // Initialize destination index `di` and source index `si` to 0. let mut di: usize = 0; let mut si: usize = 0; // Calculate the length of the source array that is a multiple of 3. let n = (src.len() / 3) * 3; // Process the source array in chunks of 3 bytes. while si < n { // Combine 3 bytes into a single 24-bit value. let val = (src[si] as usize) << 16 | (src[si + 1] as usize) << 8 | (src[si + 2] as usize); // Encode the 24-bit value into 4 Base64 characters. dst[di] = ALPHABET[val >> 18 & 0x3F]; dst[di + 1] = ALPHABET[val >> 12 & 0x3F]; dst[di + 2] = ALPHABET[val >> 6 & 0x3F]; dst[di + 3] = ALPHABET[val & 0x3F]; // Increment the source index by 3 and the destination index by 4. si += 3; di += 4; } // Calculate the remaining number of bytes in the source array. let remain = src.len() - si; // If there are no remaining bytes, return early. if remain == 0 { return; } // Initialize a 24-bit value with the remaining byte(s). let mut val = (src[si] as usize) << 16; // If there are 2 remaining bytes, add the second byte to the 24-bit value. if remain == 2 { val |= (src[si + 1] as usize) << 8; } // Encode the remaining bytes into 2 or 3 Base64 characters. dst[di] = ALPHABET[val >> 18 & 0x3F]; dst[di + 1] = ALPHABET[val >> 12 & 0x3F]; // If there are 2 remaining bytes, encode the third Base64 character. if remain == 2 { dst[di + 2] = ALPHABET[val >> 6 & 0x3F]; } } #[cfg(test)] mod test { use std::println; extern crate std; use soroban_sdk::{ vec, Bytes, BytesN, Env, IntoVal, }; use crate::{Contract, ContractClient, Error, Signature}; #[test] fn test() { let env = Env::default(); let deployee_address = env.register(Contract, ()); let deployee_client = ContractClient::new(&env, &deployee_address); // Test data let id = Bytes::from_array( &env, &[243, 248, 216, 74, 226, 218, 85, 102, 196, 167, 14, 151, 124, 42, 73, 136, 138, 102, 187, 140], ); let pk = BytesN::from_array( &env, &[4, 163, 142, 245, 242, 113, 55, 104, 189, 52, 128, 238, 206, 174, 194, 177, 4, 100, 161, 243, 177, 255, 10, 53, 57, 194, 205, 45, 208, 10, 131, 167, 93, 44, 123, 126, 95, 219, 207, 230, 175, 90, 96, 41, 121, 197, 127, 180, 74, 236, 160, 0, 60, 185, 211, 174, 133, 215, 200, 208, 230, 51, 210, 94, 214], ); // Test adding a signer deployee_client.add(&id, &pk, &true); // Test authentication let signature_payload = BytesN::from_array( &env, &[150, 22, 248, 96, 91, 4, 111, 72, 170, 101, 57, 225, 210, 199, 91, 29, 159, 227, 209, 6, 231, 63, 222, 209, 232, 57, 112, 98, 140, 118, 206, 245], ); let signature = Signature { authenticator_data: Bytes::from_array( &env, &[75, 74, 206, 229, 181, 139, 119, 89, 254, 159, 95, 149, 227, 164, 109, 143, 188, 228, 143, 219, 181, 216, 77, 123, 142, 172, 60, 20, 162, 154, 181, 187, 29, 0, 0, 0, 0], ), client_data_json: Bytes::from_array( &env, &[123, 34, 116, 121, 112, 101, 34, 58, 34, 119, 101, 98, 97, 117, 116, 104, 110, 46, 103, 101, 116, 34, 44, 34, 99, 104, 97, 108, 108, 101, 110, 103, 101, 34, 58, 34, 108, 104, 98, 52, 89, 70, 115, 69, 98, 48, 105, 113, 90, 84, 110, 104, 48, 115, 100, 98, 72, 90, 95, 106, 48, 81, 98, 110, 80, 57, 55, 82, 54, 68, 108, 119, 89, 111, 120, 50, 122, 118, 85, 34, 44, 34, 111, 114, 105, 103, 105, 110, 34, 58, 34, 104, 116, 116, 112, 115, 58, 47, 47, 112, 97, 115, 115, 107, 101, 121, 45, 107, 105, 116, 45, 100, 101, 109, 111, 46, 112, 97, 103, 101, 115, 46, 100, 101, 118, 34, 125], ), id: id.clone(), signature: BytesN::from_array( &env, &[74, 48, 29, 120, 181, 135, 255, 178, 105, 76, 82, 118, 29, 135, 193, 72, 123, 144, 138, 214, 125, 27, 33, 159, 169, 200, 151, 55, 7, 250, 111, 172, 86, 89, 162, 167, 148, 105, 144, 68, 21, 249, 61, 253, 80, 61, 54, 29, 14, 162, 12, 173, 206, 194, 144, 227, 11, 225, 74, 254, 191, 221, 103, 86], ), }; let result: Result<(), Result> = env.try_invoke_contract_check_auth( &deployee_address, &signature_payload, signature.into_val(&env), &vec![&env], ); println!("{:?}", result); assert!(result.is_ok()); } } ``` --- ## Smart contract authorization starter guide ## Intro to smart contract authorization Smart contracts can be invoked without any caller authorization by default. The functionality of the smart contract can either justify, or even require, authorization. Let’s say we want to read or write sensitive information: then access to the contract should be restricted through authorization. Authorization is required if the contract signs transactions. For a deeper understanding of how authorization works, see the [Security](../../../learn/fundamentals/contract-development/authorization.mdx) section. ### Address-based authorization Stellar smart contracts use addresses as identifiers for authorization. There are two different types of addresses: account addresses (G-addresses) and contract addresses (C-addresses), but they provide the same interface. This means that developers do not need to consider the type of address used for authorization; the authorization methods treat the two address types the same, from a developer's point of view. ## 1. Basic authorization Stellar smart contracts have authorization methods built-in, `require_auth()` and `require_auth_for_args()`. These two methods provide an easy way to authenticate and authorize users in contract functions. ### `require_auth()` The `require_auth()` method authenticates and authorizes a user invoking a smart contract function. To determine if a user has authorized the function invocation, simply call `require_auth()` on the user’s address. The contract function doesn’t need to consider signatures or other authorization processes. To check if a user is authorized to invoke a function, call `require_auth()` on the user address: `.require_auth()` Let’s examine how authorization can be added to a simple function, in this case, an `increment()` function. The increment function takes two arguments, a user (secret address) and a value, which is used to increment the current count value. The function will call `require_auth()` on the user when the function is invoked. ```rust pub fn increment(env: Env, user: Address, value: u32) -> u32 { user.require_auth(); let mut count: u32 = env.storage().instance().get(&user).unwrap_or(0); count += value; env.storage().persistent().set(&user, &count); count } ``` The function will first check if the user is authorized, then get the current `count` value (or 0 if there’s no value stored). Then `count` is incremented by adding the argument `value` to the current `count`. The incremented count is stored and returned. The `user` address is used as the storage key to get and set the `count` value in storage. Run the following command to invoke the contract function with the user’s secret key to authorize the user: ```rust stellar contract invoke \ --id CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN \ --source-account alice \ --network testnet \ -- \ increment \ --user SDWDUC7IIZPRIDUZIK44UHUD2KOG6A5XWUGVZBQG2RM3J2L5DSOROBAN \ --value 10 ``` The function call should return the following output: ```rust 10 ``` If the authorization doesn’t pass, the function will cause a contract panic. This example is based on the auth contract example in the [Soroban examples repository](https://github.com/stellar/soroban-examples/tree/main/auth). #### Test Contract functions using `require_auth()` for authorization can be tested with `cargo test` by mocking the authorization. Again, we use the auth example in the [Soroban examples repository](https://github.com/stellar/soroban-examples/tree/main/auth) to illustrate how to test the authorization. First, we instruct the environment to mock all authorizations and let all `require_auth()` calls to succeed using `env.mock_all_auths()`. Next, we generate a user with `Address::generate(&env)` and insert it as the user parameter in the `increment()` contract function, along with the value parameter. The test has two assertions: the first test will invoke the `increment()` function with the generated user and value as parameters, then checks if the returned value is incremented as expected. The second test will check if the expected authorization happened and only the expected authorization happened. ```rust #[test] fn test() { let env = Env::default(); let contract_id = env.register(IncrementContract, {}); let client = IncrementContractClient::new(&env, &contract_id); env.mock_all_auths(); let user = Address::generate(&env); let value = 10; assert_eq!(client.increment(&user, &value), 10); let expected_auth = AuthorizedInvocation { function: AuthorizedFunction::Contract(( contract_id.clone(), symbol_short!("increment"), (user.clone(), value.clone()).into_val(&env), )), sub_invocations: std::vec![] }; assert_eq!(env.auths(), std::vec![(user.clone(), expected_auth)]); } ``` Before going into testing authorization, we can run the test: ```rust cargo test ``` You should see the output: ```rust running 1 test test test::test ... ok ``` **Testing authorization** When testing authorized contract invocations, we check the authorization details by examining `env.auths()`. It will contain details of all the authorizations that happened, so by comparing the content of `env.auths()` to what we expect to see, we can check if the authorization was successful. And just as important, since `env.auths()` contains details of all authorizations that happened during the contract invocation, testing against what we expect to see will also ensure no unexpected authorizations happened. The expected content of `env.auths()` looks like this: ```rust [( Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4), AuthorizedInvocation { function: Contract(( Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM), Symbol(increment), Vec(Ok(Address(obj#75))) )), sub_invocations: [] } )] ``` The content of env.auths() is an array of an authorized user address and the associated contract invocation details. ### `require_auth_for_args()` The `require_auth_for_args()` method allows you to explicitly specify the contract call arguments you want to be authorized, where `require_auth()` automatically passes all the contract call arguments into the authorization payload. Let’s use the same example as we did in the `require_auth()` section; the only difference is that we explicitly want to authorize the `value` argument. ```rust pub fn increment(env: Env, user: Address, value: u32) -> u32 { user.require_auth_for_args((value.clone(),).into_val(&env)); let mut count: u32 = env.storage().instance().get(&user).unwrap_or(0); count += value.clone(); env.storage().persistent().set(&user, &count); count } ``` `require_auth_for_args()` is called on the `user`. In this case, we authorize the `value` argument, and as in the `require_auth()` example, the user address is used as the key in the key-value storage. The `value` argument is the value by which the current `count` is incremented when the function is called. The function returns the current count value. Run the following command to invoke the contract function with the user’s address for user authorization: ```rust stellar contract invoke \ --id CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN \ --source-account alice \ --network testnet \ -- \ increment \ --user SDWDUC7IIZPRIDUZIK44UHUD2KOG6A5XWUGVZBQG2RM3J2L5DSOROBAN \ --value 10 ``` The following output should appear: ```rust 10 ``` If the authorization doesn’t pass, the function will not reach the return line of the code. #### Test Contract functions using `require_auth_for_args()` for authorization can be tested with `cargo test` by mocking the authorization. The test is very similar to the test used in the `require_auth()` section, where the testing is done in two parts. First, a test of the contract function is performed, where the authorization is mocked, followed by a test of the authorization itself. ```rust #[test] fn test() { let env = Env::default(); let contract_id = env.register(IncrementContract, {}); let client = IncrementContractClient::new(&env, &contract_id); env.mock_all_auths(); let user = Address::generate(&env); let value = 10; assert_eq!(client.increment(&user, &value), 10); let expected_auth = AuthorizedInvocation { function: AuthorizedFunction::Contract(( contract_id.clone(), symbol_short!("increment"), (user.clone(), value.clone()).into_val(&env), )), sub_invocations: std::vec![] }; assert_eq!(env.auths(), std::vec![(user.clone(), expected_auth)]); } ``` Similar to how authorization is tested in the `require_auth()` section, we check if the authorization is working as expected by comparing the content of env.auths() to the expected authorization details. The expected content of `env.auths()` looks like this: ```rust [( Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4), AuthorizedInvocation { function: Contract(( Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM), Symbol(increment), Vec(Ok(String(obj#43), Ok(I32(10))) )), sub_invocations: [] } )] ``` The content of `env.auths()` is an array of an authorized user address and the associated contract invocation details. Now run the test: ```rust cargo test ``` You should see the output: ```rust running 1 test test test::test ... ok ``` ## 2. Cross-contract authorization Smart contracts can invoke functions in other contracts (called cross-contract calls). These direct calls are implicitly authorized by the invoker and do not need to be authorized. However, the contract function being invoked may require authorization from a user other than the contract itself. An example could be a transfer function, where an external user may have to authorize the transfer to the current contract. To illustrate how to add authorization to a simple cross-contract invocation, we use the [Cross Contract Calls example contract](../../smart-contracts/example-contracts/cross-contract-call.mdx), which consists of two separate contracts, one that has a simple addition contract function, and another contract to invoke the first one. ### Invoking contract The invoking contract in the mentioned example contract creates a client for invoking the `add()` function in the addition contract. Let’s say we want the `add()` contract to be authorized by a user. Then, we need to pass the user to the `add()` contract function from the invoking function. Even though the invoking function may not require the `user`’s authorization, it's recommended to authorize the `user` at the entry point. Without that, the authorized inner `add()` call can be front-run by anyone without being wrapped in `add_with()`. Adding `require_auth()` at the entry point ensures that all the inner contract calls that are authorized on behalf of the `user` will be executed atomically together with the entry point call. Therefore, we need to use `require_auth()` on the `user` in the invoking function. This is how the function looks: ```rust #[contractimpl] impl ContractB { pub fn add_with(env: Env, x: u32, y: u32, contract: Address, user: Address) -> u32 { user.require_auth(); let client = contract_a::Client::new(&env, &contract); client.add(&user, &x, &y) } } ``` The `add_with()` function takes four parameters. The parameters x and y are the two numbers to add, the contract is the contract ID of the contract with the `add()` function, and user is the user that will authorize the `add()` execution. ### Addition contract The addition contract function is very simple. In its original form in the example contract, it simply takes numbers to add (x and y). All we need to do here is add the `user` address as a parameter and then call `require_auth()` on the user to authorize the invocation. ```rust pub fn add(user: Address, x: u32, y: u32) -> u32 { user.require_auth(); x.checked_add(y).expect("no overflow") } ``` ### Invoker auth In the `add_with()` function above, a user is passed as an argument to the function, and the user is authorized in the invoked function `add()`. Let’s say we don’t need the invocation authorized by an external user, but will allow the invocation to be authorized by the invoker. This can be done by passing the invoker’s address as the user, like this: ```rust #[contractimpl] impl ContractB { pub fn add_with(env: Env, x: u32, y: u32, contract: Address, user: Address) -> u32 { user.require_auth(); let current_contract_address = env.current_contract_address(); let client = contract_a::Client::new(&env, &contract); client.add(¤t_contract_address, &x, &y) } } ``` By using `env.current_contract_address()`, we can get the invoker's address and pass it to the `add()` function. ## 3. Contract account authorization Contract accounts are contracts that implement a special reserved function for validating externally provided signatures within the respective authorization context. These accounts are essentially regular contracts, with an added capability to verify externally provided authorization. ### `__check_auth()` A contract that implements the `CustomAccountInterface` for authorizing calls becomes a contract account. The interface contains a single function called `__check_auth()`. The function is a reserved function, and it is invoked automatically by the Host when a contract authorizes a transaction. It cannot be called manually. The function `__check_auth()` may be invoked for a given contract if two conditions are met. The first condition is when `require_auth()` or `require_auth_for_args()` are called. The second condition occurs when an account contract has not provided invoker authorization, for example, by calling a function that requires authorization directly. #### How it works `__check_auth()` verifies the credentials and policy for an account address whenever another contract calls `require_auth`. A minimal implementation, shown below and documented end-to-end in the [Simple Account example](../../smart-contracts/example-contracts/simple-account.mdx), loads a stored Ed25519 key and verifies the signature payload against it: ```rust pub fn __check_auth( env: Env, signature_payload: BytesN<32>, signature: BytesN<64>, _auth_context: Vec, ) { let public_key: BytesN<32> = env .storage() .instance() .get::<_, BytesN<32>>(&DataKey::Owner) .unwrap(); env.crypto().ed25519_verify(&public_key, &signature_payload.into(), &signature); } ``` More advanced accounts build on the same structure: the [Complex Account example](../../smart-contracts/example-contracts/complex-account.mdx) adds multiple signers, signature weights, and spend limits enforced via `auth_context` traversal. #### Authorization logic You can customize `__check_auth()` to enforce whatever rules the account requires. Common checks include verifying all required signers, limiting which cross-contract sub-invocations are allowed, or enforcing spend limits. Refer to the Simple and Complex Account examples for end-to-end implementations. ### How to test As shown in previous examples, authorization can be mocked in tests by adding `env.mock_all_auths()`. However, `mock_all_auths` skips `__check_auth()`, so you still need direct coverage of the account logic. The recommended way is to call `env.try_invoke_contract_check_auth`, which emulates the host and feeds a payload, signature(s), and authorization context into your account. A typical pattern is: ```rust #[test] fn test_account() { let env = Env::default(); let signer = generate_keypair(); let payload = BytesN::random(&env); ... let public_key: BytesN<32> = signer.public.to_bytes().into_val(&env); let contract_id = env.register(SimpleAccount, SimpleAccountArgs::__constructor(&public_key)); let account_contract = SimpleAccountClient::new(&env, &contract_id); env.try_invoke_contract_check_auth::( &account_contract.address, &payload, sign(&env, &signer, &payload), &vec![&env], ) .unwrap(); } ``` That snippet highlights the key inputs: contract address, payload, signature, and authorization tree. For the full tests, see the [Simple Account example](../../smart-contracts/example-contracts/simple-account.mdx); the Complex Account doc includes multisig and policy coverage. #### `set_auths()` :::note `set_auths()` may not be relevant for most developers. ::: Another method is to use `set_auths()`, which will `call __check_auth()` when the contract function is called, similar to how the host will `call __check_auth()` on Testnet or Mainnet. This method is more complex, and provides a lower-level control over the authorization process in tests. To show how to use `set_auths()` for testing authorization, let’s use a very simple contract function `fn1()` and a very simple `__check_auth()` function. This simply calls `require_auth()` and returns an integer. The `__check_auth()` will pass without doing any verification. The example is based on the [Soroban SDK’s auth test cases](https://github.com/stellar/rs-soroban-sdk/blob/35ff7aaecedd38056a131843a7a657b1ddc9e684/tests/auth/src/lib.rs). ```rust #[contract] pub struct ContractA; #[contractimpl] impl ContractA { pub fn fn1(a: Address) -> u64 { a.require_auth(); 2 } #[allow(non_snake_case)] pub fn __check_auth( _signature_payload: Val, _signatures: Val, _auth_context: Vec, ) {} } ``` First, a client instance is created, and the contract ID and address are retrieved by registering the contract. Then the function `fn1()` is called by using the client, with the authorization details set, and finally the returned value of `fn1()` is compared to the expected value with `assert_eq!()`. ```rust #[test] fn test_with_real_contract_auth_approve() { let e = Env::default(); let contract_id = e.register(ContractA, ()); let client = ContractAClient::new(&e, &contract_id); let a = e.register(ContractA, ()); let a_xdr: ScAddress = (&a).try_into().unwrap(); let r = client .set_auths(&[SorobanAuthorizationEntry { credentials: SorobanCredentials::Address( SorobanAddressCredentials { address: a_xdr.clone(), nonce: 123, signature_expiration_ledger: 100, signature: ScVal::Void, } ), root_invocation: SorobanAuthorizedInvocation { function: SorobanAuthorizedFunction::ContractFn( InvokeContractArgs { contract_address: contract_id .clone() .try_into() .unwrap(), function_name: StringM::try_from("fn1") .unwrap().into(), args: std::vec![ScVal::Address( a_xdr.clone())] .try_into().unwrap(), } ), sub_invocations: VecM::default(), }, }]) .fn1(&a); assert_eq!(r, 2); } ``` Similar to how the host executes authorization on Testnet and Mainnet, `set_auths()` method also uses a list of `SorobanAuthorizationEntry` entries to verify authorization during contract function execution. A `SorobanAuthorizationEntry` contains authorization credentials and contract invocation details. The credentials part specifies the contract address of `the __check_auth()` function, and relevant authorization data. The root invocation part specifies the contract ID, the name of the function being tested, and the function’s arguments. For more information about authorization details provided in `set_auths()`, see the [Stellar Transactions](../../../learn/fundamentals/contract-development/contract-interactions/stellar-transaction.mdx) documentation. Testing using `set_auths()` is more complex than testing using `try_invoke_contract_check_auth`, but in return, it allows for more advanced testing scenarios. If more advanced testing, e.g., testing specific authorization edge cases, is required, `set_auths()` might be a good choice. Otherwise, `try_invoke_contract_check_auth` generally is the recommended method for testing. --- ## Stellar Basics Learn how to perform basic actions on the Stellar network. --- ## Automating Testnet and Futurenet reset data {`Automating Testnet and Futurenet Reset Data in Stellar`} ## Overview Stellar operates two primary testing environments: the [Testnet and the Futurenet](../../../networks/README.mdx). These networks allow developers to experiment with Stellar features without risking real assets. Periodically, these networks are reset to ensure they remain clean and manageable. ## What is the Testnet and Futurenet reset? Testnet and Futurenet are reset periodically to the genesis ledger to declutter the network, remove spam, reduce the time needed to catch up on the latest ledger, and help maintain the system. These resets take place approximately quarterly. Resets clear all ledger entries (accounts, trustlines, offers, smart contract data, etc.), transactions, and historical data from Stellar Core, Horizon, and the Stellar RPC, which is why developers should not rely on the persistence of accounts or the state of any balances when using Testnet or Futurenet. You can check current reset dates [here](../../../networks/README.mdx#testnet-and-futurenet-data-reset). ## Why resets are important 1. **Clean Slate:** Regular resets ensure that both Testnet and Futurenet provide a clean environment for testing. This helps in avoiding complications arising from old data or configurations. 2. **Performance:** Over time, test environments can accumulate a lot of data, which can slow down performance. Resets help in maintaining optimal performance. 3. **Protocol Updates:** Introducing new features or protocol changes often requires a reset to ensure compatibility and stability. 4. **Development Cycles:** Aligning with development cycles allows developers to plan their testing phases and ensures they have a reliable environment for their work. ## Data automation on Testnet and Futurenet Automating blockchain state on Stellar's Testnet and Futurenet can streamline development workflows, ensuring that you can consistently test and validate your applications in these environments. ### Code walkthrough ### Prerequisites: - [Node.js](https://nodejs.org/en) and `npm` installed. - Stellar SDK for [JavaScript](https://www.npmjs.com/package/@stellar/stellar-sdk) and `fs` installed - An understanding of the rudimentary, retry-enabled transaction polling function `submitTx` which we outlined in [another guide](../transactions/submit-transaction-wait-js.mdx) ### Code ```javascript Networks, Keypair, TransactionBuilder, Operation, Address, StrKey, Contract, LiquidityPoolAsset, LiquidityPoolFeeV18, BASE_FEE, } from "@stellar/stellar-sdk"; // const networkRPC = "USE EITHER FUTERNET OR TESTNET RPC" const networkRPC = "https://soroban-testnet.stellar.org"; // Example // FOR FUTERENET - https://rpc-futurenet.stellar.org // FOR TESTNET - https://soroban-testnet.stellar.org const server = new Server(networkRPC); // const networkURL = "USE EITHER FUTERNET OR TESTNET URL" const networkURL = "https://friendbot.stellar.org"; // Example // FOR FUTURENET - https://friendbot-futurenet.stellar.org // FOR TESTNET - https://friendbot.stellar.org const networkPassphrase = Networks.TESTNET; // or Networks.FUTURENET, PUBLIC // Create an Account async function createAccount(networkURL, SecretKey) { if (!SecretKey) { try { // Generate a keypair const pair = Keypair.random(); // Fund the new account using Friendbot const response = await fetch( `${networkURL}?addr=${encodeURIComponent(pair.publicKey())}`, ); const responseJSON = await response.json(); console.log("Account created:", responseJSON); return pair; } catch (error) { console.error("Error creating account:", error); } } else { try { const pair = Keypair.fromSecret(SecretKey); console.log("Account Restored:", pair); return pair; } catch (error) { console.error("Error restoring account:", error); } } } // Issues an Asset async function issueAsset(issuerKeys, receivingKeys, customAsset) { try { // First, the receiving account must trust the asset const receiver = await server.getAccount(receivingKeys.publicKey()); let transaction = new TransactionBuilder(receiver, { fee: BASE_FEE, networkPassphrase, }) .addOperation( Operation.changeTrust({ asset: customAsset, limit: "100000", }), ) // setTimeout is required for a transaction .setTimeout(100) .build(); transaction.sign(receivingKeys); const status = await submitTx(transaction); if (status !== Api.GetTransactionStatus.SUCCESS) { throw status; } console.log(`Receiver Trusting ${customAsset.code} Asset......`); // Second, the issuing account actually sends a payment using the asset const issuer = await server.getAccount(issuerKeys.publicKey()); transaction = new TransactionBuilder(issuer, { fee: BASE_FEE, networkPassphrase, }) .addOperation( Operation.payment({ destination: receivingKeys.publicKey(), asset: customAsset, amount: "1000", // change to desired amount you want to pay }), ) // setTimeout is required for a transaction .setTimeout(100) .build(); transaction.sign(issuerKeys); const status = await submitTx(transaction); if (status !== Api.GetTransactionStatus.SUCCESS) { throw status; } console.log( `Issuer Payment using ${ customAsset.code } to ${receivingKeys.publicKey()}`, ); } catch (e) { console.error("An error occurred while issuing assets:", e); } } //Create Liquidity Pool async function createLiquidityPool(accountKeypair, nativeAsset, customAsset) { try { const account = await server.getAccount(accountKeypair.publicKey()); // Create the liquidity pool const poolIdAsset = new LiquidityPoolAsset( nativeAsset, customAsset, LiquidityPoolFeeV18, ); const poolId = poolIdAsset.toString().split(":")[1]; // To Get the Pool ID const transaction = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: networkPassPhrase, }) .addOperation( Operation.changeTrust({ asset: poolIdAsset, limit: "100000", // Set an appropriate limit }), ) .addOperation( Operation.liquidityPoolDeposit({ liquidityPoolId: poolId, maxAmountA: "1000", // Amount of asset A to deposit maxAmountB: "500", // Amount of asset B to deposit minPrice: "0.5", // Minimum price ratio maxPrice: "2.0", // Maximum price ratio }), ) .setTimeout(30) .build(); transaction.sign(accountKeypair); const status = await submitTx(transaction); if (status !== Api.GetTransactionStatus.SUCCESS) { throw status; } console.log( `Creating Liquidity Pool for ${nativeAsset.code} and ${customAsset.code}`, ); } catch (error) { console.error("Error creating liquidity pool:", error); throw error; } } //Deploy and Invoke Contract async function deployAndInvokeContract(deployer, contractWasmFilePath) { try { // Step 1: Upload WASM const bytecode = fs.readFileSync(contractWasmFilePath); const account = await server.getAccount(deployer.publicKey()); const uploadTransaction = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase, }) .addOperation(Operation.uploadContractWasm({ wasm: bytecode })) .setTimeout(30) .build(); const uploadTx = await server.prepareTransaction(uploadTransaction); uploadTx.sign(deployer); console.log("Submitting WASM upload transaction..."); let status = await submitTx(uploadTx); if (status !== Api.GetTransactionStatus.SUCCESS) { throw status; } const wasmHash = status.returnValue.bytes(); const deployerAddress = new Address(deployer.publicKey()); // Deploy the Contract const createContractTransaction = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase, }) .addOperation( Operation.createCustomContract({ address: deployerAddress, wasmHash, }), ) .setTimeout(30) .build(); const createContractTx = await server.prepareTransaction( createContractTransaction, ); createContractTx.sign(deployer); status = await submitTx(createContractTx); if (status !== Api.GetTransactionStatus.SUCCESS) { throw status; } console.log(`Contract Deployed...`); const contractAddr = Address.fromScAddress( returnContractResponse.returnValue.address(), ); const contractId = contractAddr.toString(); const contract = new Contract(contractId); // Invoke Contract const invokeContractTransaction = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase, }) .addOperation( contract.call("hello", nativeToScVal("World", { type: "symbol" })), ) .setTimeout(30) .build(); const invokeContractTx = await server.prepareTransaction( invokeContractTransaction, ); invokeContractTx.sign(deployer); const returnInvokeContractResponse = await submitTx(invokeContractTx); console.log(`Invoke Contract.`); const returnValues = scValToNative( returnInvokeContractResponse.returnValue, ).filter(Boolean); return { contractId, returnValues }; } catch (error) { console.error("Error in contract deployment and invocation:", error); throw error; } } async function automateSetup() { try { //Check Network Status console.log("Checking network health..."); const health = await server.getHealth(); console.log("Network health:", health); // Flexible Account Configuration const secretkey = "SBGGNMUPVF2SDN4KZOJQFVFX7VDR4Q4NK3FMEFRQC64D3UBMFELKF5GC"; // This is an example of a user's secret key const accountOne = await createAccount(networkURL, secretkey); const accountTwo = await createAccount(networkURL); console.log("Issuing an Asset..."); // Issue assets to these accounts const customAsset = new Asset("Boya", accountOne.publicKey()); await issueAsset(accountOne, accountTwo, customAsset); // Create liquidity pool console.log("Creating liquidity pool..."); const nativeAsset = Asset.native(); await createLiquidityPool(accountTwo, nativeAsset, customAsset); // Deploy a contract console.log("Deploying contract..."); // Ensure you have the contract Wasm file compiled and saved in the specified path. // Adjust this path as necessary const contractWasmFilePath = "./target/wasm32v1-none/release/hello_world.wasm"; const ContractData = await deployAndInvokeContract( accountOne, contractWasmFilePath, ); console.log("Contract ID:", ContractData.contractId); console.log("Return Values:", ContractData.returnValues.join(", ")); } catch (error) { console.error("An error occurred:", error); } } automateSetup(); ``` The code defines several asynchronous functions: 1. `createAccount(networkURL)`: Generates a new Stellar keypair (public and secret keys). Uses FriendBot to fund the new account on the test network. Returns the created keypair. 2. `issueAsset(issuerKeys, receivingKeys, customAsset)`: Sets up a trust line for the receiving account to accept the custom asset, Issues the custom asset from the issuer account to the receiving account. 3. `createLiquidityPool(accountKeypair, nativeAsset, customAsset)`: Creates a liquidity pool asset, Sets up a trust line for the pool and Deposits initial liquidity into the pool. 4. `deployAndInvokeContract(deployer, contractWasmFilePath)`: Uploads the contract's WebAssembly (Wasm) code, creates and deploys the contract on the network, invokes the contract function and returns the contract ID and function return values 5. `automateSetup()`: Initializes the Stellar server connection, Creates two accounts, Issues a custom asset, Creates a liquidity pool, Deploys a smart contract and returns the contract ID and function values. **Helper functions** `sleep(ms)`: A utility function to introduce delays in asynchronous operations. `submitTx(tx)`: a retry-enabled transaction submission function `submitTx` outlined in [another guide](../transactions/submit-transaction-wait-js.mdx) ### Conclusion Automating the setup of data on the Stellar Testnet and Futurenet can significantly enhance your development workflow, ensuring that you can quickly return to testing after a network reset. By following the above steps and using the provided code samples, you can streamline your processes and maintain consistency across resets. --- ## Add support for smart contracts Stellar recently [upgraded its protocol](https://stellar.org/blog/developers/protocol-20-preparing-smart-contracts-to-stellar) to support smart contracts, adding a new way to interact with the network. This one-pager highlights key considerations for integrating with Stellar’s smart contracts specifically for wallets and exchanges that already support Stellar’s “classic” operations. It quickly outlines what changes to expect, provides links to detailed documentation, and is a starting point for adapting existing Stellar Classic processes to the new smart contract environment. ## 1. Infrastructure ### Transitioning from Horizon to RPC To properly support Stellar's smart contracts, you must use [Stellar RPC](../../../data/apis/rpc). More specifically, you need RPC to simulate transactions that execute smart contracts, as described in the "Simulating Transactions" section, and it provides a convenient [API](../../../data/apis/rpc/api-reference) for consuming contract events. Both of these features are not available in Horizon. ### Running Your Own RPC vs. Leveraging Third-Party Providers You can set up an RPC environment by hosting your own node or using a third-party provider. For guidance on hosting your own instance, including a Docker-based setup, refer to the [Admin Guide](../../../data/apis/rpc/admin-guide). Alternatively, a list of trusted providers is available in the [ecosystem RPC providers documentation](../../../data/apis/rpc/providers). ## 2. Data Ingestion ### Ingesting Smart Contract Events Horizon offers an effects endpoint that describes state changes executed by classic operations. Similarly, contracts emit [events](../../../learn/fundamentals/stellar-data-structures/events.mdx) that describe changes to their state, which can be fetched via RPC's API. Off-chain solutions can [monitor and ingest these events](../../../build/guides/events/ingest.mdx) (for token transfers or protocol updates) and remain in sync with on-chain data. Each event is defined by the contract and is subject to standards applied to the implementation. Depending on the requirements for retention, a solution might have to handle ingestion directly or use a [third-party service](../../../data/indexers/README.mdx) for a longer-term history. ### Simulating Transaction While events should be used to monitor changes to a contract’s state, clients may also need to determine the current state of a contract. [Simulation](../../../build/guides/transactions/simulateTransaction-Deep-Dive.mdx) allows clients to execute contract invocations with incurring fees or finalizing changes, making it an ideal approach for fetching contracts’ current state. For example, clients can call the balance function on a [SEP-41](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md)-compliant token contract to fetch a user’s current balance. ## 3. Transaction Workflow Changes ### Building Transactions Transaction construction involves specifying a call to a contract function rather than any of the built-in operations Stellar offers. When building these contract calls, it is necessary to specify the relevant contract ID and function arguments according to the contract interface. See more at [Documentation for Contract Interaction - Stellar Transaction](../../../learn/fundamentals/contract-development/contract-interactions/stellar-transaction.mdx). You can still use the libraries or tools you’re already familiar with to assemble these transactions, [but keep in mind the extra steps required for contract invocation](../../../build/guides/transactions/invoke-contract-tx-sdk.mdx). ### Simulating Transaction Transactions that execute smart contracts must be [simulated](../../../build/guides/transactions/simulateTransaction-Deep-Dive.mdx) before sending. That is because simulation doesn’t just provide the results of executing transactions; it also provides essential information such as the transaction’s read/write [footprint](../../../learn/fundamentals/contract-development/contract-interactions/transaction-simulation.mdx#footprint) and [authorizations](../../../learn/fundamentals/contract-development/contract-interactions/transaction-simulation.mdx#authorization) needed, and clients must add this information to the transaction before sending it to the network for execution. ### Signing & Auth Entries Smart contracts can define their own [custom authorization logic](../../../learn/fundamentals/contract-development/authorization.mdx), meaning you might need [additional signatures for specific authorization data](../../../learn/fundamentals/contract-development/contract-interactions/stellar-transaction.mdx#authorization-data) to prove permission for certain contract calls. Each contract can have its own requirements, so a good practice is to [leverage transaction simulations to identify specific authorization requirements for a transaction](../../../learn/fundamentals/contract-development/contract-interactions/transaction-simulation.mdx#authorization). ### Asynchronous Transaction Submission Unlike Horizon, [RPC only queues transactions for inclusion rather than waiting for final confirmation](../../../data/apis/rpc/api-reference/methods/sendTransaction). Therefore, it is necessary to [poll the transaction’s status](../../../data/apis/rpc/api-reference/methods/getTransaction) to determine if a transaction eventually succeeds or fails. This asynchronous model, as well as other common challenges, can be seen at the [Documentation for Dapp Development - Common Pitfalls](../../../build/guides/dapps/frontend-guide.mdx#7-common-pitfalls). --- ## Verify Trustlines When performing payments on Stellar for [Stellar Assets](../../../tokens/README.mdx) other than XLM, it is important to ensure that the receiving account has a trustline established for the asset being sent. This one-pager provides a quick overview of how to verify trustlines before sending transactions, ensuring that payments are processed smoothly and allowing for application to appropriately handle cases where trustlines are not established or invalid. ## Why Verify Trustlines? In Stellar, trustlines are used to establish a relationship between an account and a Stellar Asset. They indicate that the account is willing to hold and transact with that asset. If a trustline is not established for an asset, the account cannot receive payments in that asset, leading to transaction failures. Furthermore, asset issuers may enforce specific requirements through trustlines, such as maximum balances an account can hold or granular authorization to receive/send the asset or to maintain liabilities. See the [Asset Design Considerations](../../../tokens/control-asset-access.mdx) for more details on how control flags and trustlines can be used to customize these behaviors. Verifying trustlines before sending transactions helps ensure that the receiving account meets the requirements and can successfully receive the asset. This allows for the application to handle cases where trustlines are not established or invalid, providing clear feedback and a smooth user experience while preventing failed transactions. ## Checking a Trustline through the Stellar RPC To check if a trustline exists for a specific asset, you can use the Stellar RPC API to directly retrieve the ledger entry for the trustline and validate its state. The following code snippet demonstrates how to check if a trustline exists for a specific asset using the `getLedgerEntries` method from Stellar RPC API: ```js // Initialize Soroban RPC server for testnet const rpc = new Server("https://soroban-testnet.stellar.org"); // Define the receiver account ID // This is the account that will receive the payment and for which we will check the trustline. const receiver = "GCLNZP3WX3GG4D2HC3L2VVXNYBSVHO2OPGGTDQ4YGBQOUXHHTM3FSBNH"; // First, check to make sure that the destination account exists. try { await rpc.getAccount(receiver); } catch (error) { console.error("Error checking destination account:", error); throw error; } // Now we defined which asset we want to check the trustline for. // In this case, we are checking for USDC issued in testnet. const USDC = new Asset( "USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", ); // This is the amount we want to send. const sendingAmount = "1"; // We then conver the receiver's public key to the XDR format. // This is necessary to create the ledger key for the trustline. const publicKeyXdr = xdr.PublicKey.publicKeyTypeEd25519( StrKey.decodeEd25519PublicKey(receiver), ); // Now we create the trustline ledger key using the public key and the asset. // The trustline ledger key is used to retrieve the trustline entry from the ledger. const trustlineKeyXdr = new xdr.LedgerKeyTrustLine({ accountId: publicKeyXdr, asset: USDC.toTrustLineXDRObject(), }); // We then create the ledger key based on the trustline key XDR. // This key is used to query the ledger for the trustline entry. // The ledger key is a unique identifier for the trustline in the Stellar network. // It combines the account ID and the asset to form a deterministic unique key for the trustline entry const key = xdr.LedgerKey.trustline(trustlineKeyXdr); // Now we query the ledger through the RPC for the trustline entry using the ledger key. // The `_getLedgerEntries` method retrieves the ledger entries for the specified key. // This will return the trustline entry if it exists, or an empty array if it does not. const response = await rpc._getLedgerEntries(key); // If the trustline entry is not found, we log an error and throw an exception. // This indicates that the account does not have a trustline set up for the specified asset. if (!response.entries || response.entries.length === 0) { console.error( `Trustline for asset ${USDC.code} issued by ${USDC.issuer} not found for account ${receiver}.`, ); throw new Error("Trustline not found"); } // If the trustline entry is found, we parse the XDR data from the response. // The response contains an array of entries, and we take the first one. // This is because we are querying for a specific trustline, so there should only be one entry. const ledgerData = response.entries[0]; // We then convert the XDR data to a LedgerEntryData object. // This object contains the trustline data, which includes the asset, account ID, limit, // balance, and flags. const trustlineData = xdr.LedgerEntryData.fromXDR( ledgerData.xdr, "base64", ).trustLine(); // At this point, since the trustline is found, we check if it is authorized. // An authorized trustline means that the account is allowed to receive payments. // Here the authorization is indicated by the flags field in the trustline entry. if (trustlineData.flags() !== 1) { console.error( `Trustline for asset ${USDC.code} issued by ${USDC.issuer} is not authorized for account ${receiver}.`, ); throw new Error("Trustline not authorized"); } // Before checking the values, we parse the limit and balance from the // trustline data from stroops (1 XLM = 10,000,000 stroops). const limit = Number(trustlineData.limit().toBigInt()) / 10 ** 7; const balance = Number(trustlineData.balance().toBigInt()) / 10 ** 7; // Finally, we check if the trustline has enough limit to receive the payment. // We compare the trustline's limit minus its current balance with the amount we want to send. // Attempting to send an amount that exceeds the available limit will result in a failed transaction, // therefore, if the limit is insufficient, we log an error and throw an exception. if (limit - balance < parseFloat(sendingAmount)) { console.error( `Insufficient limit for asset ${USDC.code} issued by ${USDC.issuer} in account ${receiver}.`, ); throw new Error("Insufficient limit for asset"); } // If all checks pass, we log that the trustline is valid and ready for payment. console.log( `Trustline for asset ${USDC.code} issued by ${USDC.issuer} is valid for account ${receiver}.`, ); // Proceed with sending the payment... ``` ## Checking a Trustline through the Horizon API Given a receiver address, the following code snippet demonstrates how to check if a trustline exists for a specific asset using the Horizon API: ```js // Initialize Horizon server for testnet const server = new StellarSdk.Horizon.Server( "https://horizon-testnet.stellar.org" ); // Define the receiver account ID // This is the account that will receive the payment and for which we will check the trustline. const receiver = "GCLNZP3WX3GG4D2HC3L2VVXNYBSVHO2OPGGTDQ4YGBQOUXHHTM3FSBNH"; // First, check to make sure that the destination account exists. try { await server.loadAccount(receiver); } catch (error) { console.error("Error checking destination account:", error); throw error; } // Now we defined which asset we want to check the trustline for. // In this case, we are checking for USDC issued in testnet. const assetCode = "USDC"; const assetIssuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; // This is the amount we want to send. const sendingAmount = "1"; // We then load the account data for the receiver to check if the trustline exists. // This will also include other balances and trustlines for the account. const accountData = await server.accounts().accountId(receiver).call(); // Now we check if the trustline for the specified asset exists in the account data. // We are looking for a trustline that matches the asset code and issuer. const trustline = accountData.balances.find( (balance) => balance.asset_type === "credit_alphanum4" && balance.asset_code === assetCode && balance.asset_issuer === assetIssuer ) as StellarSdk.Horizon.HorizonApi.BalanceLineAsset; // If the trustline is not found, we log an error and throw an exception. // This indicates that the account does not have a trustline set up for the specified asset. if (!trustline) { console.error( `Trustline for asset ${assetCode} issued by ${assetIssuer} not found for account ${receiver}.` ); throw new Error("Trustline not found"); } // If the trustline is found, we check if it is authorized. // An authorized trustline means that the account is allowed to receive payments. if (trustline.is_authorized === false) { console.error( `Trustline for asset ${assetCode} issued by ${assetIssuer} is not authorized for account ${receiver}.` ); throw new Error("Trustline not authorized"); } // Finally, we check if the trustline has enough limit to receive the payment. // We compare the trustline's limit minus its current balance with the amount we want to send. // Attempting to send an amount that exceeds the available limit will result in a failed transaction, // therefore, if the limit is insufficient, we log an error and throw an exception. if ( Number(trustline.limit) - Number(trustline.balance) < parseFloat(sendingAmount) ) { console.error( `Insufficient limit for asset ${assetCode} issued by ${assetIssuer} in account ${receiver}.` ); throw new Error("Insufficient limit for asset"); } // If all checks pass, we log that the trustline is valid and ready for payment. console.log( `Trustline for asset ${assetCode} issued by ${assetIssuer} is valid for account ${receiver}.` ); // Proceed with sending the payment... ``` ## Checking a Trustline through the Stellar Asset Contract (SAC) All Stellar assets, including the native asset (XLM), can be managed with smart contract transactions through Stellar Asset Contracts (SAC). SACs provide a smart contract interface for handling assets, allowing for more complex interactions and programmability. This means that it includes certain functions to help developers manage assets, such as verifying trustlines and sending payments, in a more flexible way than classic operations. For this example, we'll be using SAC as a smart contract interface for the testnet USDC asset. A contract invocation transaction will be made to call the function `authorized`, which checks if a trustline exists for a given account and returns a boolean indicating whether the trustline is authorized. This function can be accessed directly in a smart contract invocation as the example below demonstrates, or it can also be invoked by another contract, allowing for more complex interactions and programmability to be built in smart contracts. :::info To use the RPC example below you should first generate the contract bindings so the client can be used accordingly. This can be achieved through the [Stellar CLI](../../../tools/cli/README.mdx). E.g.: Generating the typescript bindings for the `sac` contract of a given asset: ```bash stellar contract bindings typescript --network=testnet --contract-id=CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA --output-dir=./bindings ``` ::: Given a receiver addresss, the following code snippet demonstrates how to simulate a transaction to check if a trustline exists for a specific asset: ```js // Initialize Soroban RPC server for testnet const rpc = new Server("https://soroban-testnet.stellar.org"); // Define the receiver account ID // This is the account that will receive the payment and for which we will check the trustline. const receiver = "GCLNZP3WX3GG4D2HC3L2VVXNYBSVHO2OPGGTDQ4YGBQOUXHHTM3FSBNH"; // First, check to make sure that the destination account exists. try { await rpc.getAccount(receiver); } catch (error) { console.error("Error checking destination account:", error); throw error; } // Now we defined which asset we want to check the trustline for. // In this case, we are checking for USDC issued in testnet. const USDC = new Asset( "USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", ); // Now we initialize the Stellar Asset Contract (SAC) client. // The client needs the RPC endpoint, network passphrase, contract ID for the asset, // and your account's public key. Since we are only going to simulate the transaction, // we do not need to provide a signing function. const usdcClient = new Client({ rpcUrl: "https://soroban-testnet.stellar.org", networkPassphrase: Networks.TESTNET, contractId: USDC.contractId(Networks.TESTNET), publicKey: receiver, }); // Now, using the client, we assemble a soroban transaction to invoke the transfer // function of the USDC asset contract. The cient will automatically // bundle the operation and simulate the transaction before providing us // with an assembled transaction object. This object contains the result of the simulation, // which we can check to see if the trustline is authorized or not. let assembledTx; try { assembledTx = await usdcClient.authorized({ id: receiver, }); // The result parameter contains the return value of the contract function. // If the trustline is authorized, it will return true; otherwise, it will return false. const result = assembledTx.result; // If the trustline is not authorized, we log an error and throw an exception. // This indicates that the account does not have a trustline set up for the specified asset and // any attempt to send USDC to this account will fail. if (result === false) { console.error( `Trustline for asset ${USDC.code} issued by ${USDC.issuer} not authorized for account ${receiver}.`, ); throw new Error("Trustline not authorized"); } // If the trustline is authorized, we log a success message. // This means that the account is allowed to receive payments in USDC. console.log( `Trustline for asset ${USDC.code} issued by ${USDC.issuer} is authorized for account ${receiver}.`, ); // assembledTx = await xlmClient.transfer({ // to: destinationId, // amount: BigInt(10_0000000), // Amount in stroops (1 XLM = 10,000,000 stroops) // from: sourceKeys.publicKey(), // }); } catch (error) { console.error("Error assembling and simulating the transaction:", error); throw error; } // If the trustline is authorized, we can proceed with sending the payment... ``` --- ## Contract Accounts Contract accounts are smart contracts that act like accounts. They hold balances and use `__check_auth` to decide who can act and under what conditions. Use a contract account when: - You need custom authentication in the contract (passkeys/WebAuthn, hardware keys, or other signer checks). - You need on chain rules such as spend caps, allow lists, or timelocks. Stay with a classic account when: - You want the simplest path: one private key, fund the account, no extra contract code or infrastructure. - You must interoperate with systems/tools that expect memos (for example, some exchanges require payments with memos from a G-address; contract account payments go through the Stellar Asset Contract as muxed transfers, so transfers from contracts are not supported by exchanges today). Quick links: - [Simple Account example](../../smart-contracts/example-contracts/simple-account.mdx) - [Complex Account example](../../smart-contracts/example-contracts/complex-account.mdx) - [Contract authorization starter guide](../auth/contract-authorization.mdx) and [Authorization fundamentals](../../../learn/fundamentals/contract-development/authorization.mdx) --- ## Advanced contract account patterns Use these patterns to extend a basic contract account with guardrails. Put the logic in `__check_auth` or a helper it calls, and test both the allow and deny paths. Treat signer rules as “who can act” and policy rules as “under what conditions.” ## Spend limits - Store a limit and a running total in instance storage (for example, outflows over the last 24 hours). - Derive a window key from the ledger timestamp (for example, `day = timestamp / 86_400`) and reset the total when the window changes. - On each request, check the remaining allowance; if the amount would exceed it, reject and emit an event with the attempted amount and remaining allowance. See the [Complex Account example](../../smart-contracts/example-contracts/complex-account.mdx) for a reference implementation with weighted signers and limits. ## Allow lists - Keep allowed destination addresses or contract IDs in storage. - Inspect both the root invocation and any subinvocations in `auth_context` before approving so nested calls cannot bypass the list. ## Policy signers - Define one or more policy signer roles in storage. A policy signer exists to approve or deny specific actions (for example, an auditor key, a guardian key, or a service that enforces spending rules). - For high-risk contract calls (e.g. large transfers, changing signers), require the end user plus a policy signer; reject if either is missing. - Separate roles (admin vs. standard) so daily use can be delegated while upgrades or recovery stay with admins. ## Time rules - Block execution until a specific ledger timestamp, or add a cooldown after actions like key rotation or large transfers. - Store the earliest allowed timestamp in instance storage; compare it against the current ledger timestamp in `__check_auth`. ## Session keys - Generate a short-lived session key (for example, a p256 key created client-side and kept in secure browser storage) and limit it to one function plus a maximum amount. - Store a record for the session key with its expiry, allowed scope, and remaining allowance; reduce the allowance on each authorized call. - Reject if the session key is unknown, expired, out of allowance, or used outside its allowed scope. ## External policy contracts - Offload specific checks (deny lists, time windows, device posture) to a dedicated policy contract; pass context into `__check_auth` and require the policy contract to approve before returning success. - Keep the policy interface minimal (for example, `fn approve(auth_context) -> bool`) so you can swap policies without changing your account. ## Where to go next - See these patterns applied in the [contract account examples](./examples.mdx). - Explore reference implementations and libraries: - [OpenZeppelin Stellar contracts](https://github.com/OpenZeppelin/stellar-contracts) - [Crossmint smart account](https://github.com/Crossmint/stellar-smart-account) --- ## Contract account examples Use these projects as references or starting points. They are community-built; review and test before reusing in production. ### Zafegard A smart wallet that demonstrates the use of stateful policy signers. - View the [code](https://github.com/kalepail/zafegard) - Watch the [demo video](https://www.youtube.com/watch?v=I4i6sL-pHrs) ### Do Math A smart wallet that demonstrates the use of multi-sig and policy signers to, surprisingly, do math without needing to input a passkey for every interaction. - View the [code](https://github.com/kalepail/do-math) - Watch the [demo video](https://www.youtube.com/watch?v=lwvE6pEBmXw) ### Soroban by example An app that uses passkeys to sign Stellar smart contract transactions. - View the [demo](https://passkey.sorobanbyexample.org) - View the [code](https://github.com/kalepail/soroban-passkey) - Watch the [demo video](https://www.youtube.com/watch?v=y38_O4oIvbY) - Watch the [discussion](/meetings/2024/05/09) - Read the [blog](https://kalepail.com/blockchain/the-passkey-powered-future-of-web3) ### Super Peach A passkey-powered multi-signer abstract account contract example. - View the [demo](https://superpeach.xyz) - View the [code](https://github.com/kalepail/superpeach) - Watch the [demo video](https://youtu.be/0Agiwso2OMc?si=CHD9U8s-YLyqbXUJ) ### Ye Olde Guestbook A passkey-powered internet guestbook from yesteryear built with smart contracts and frontend code. - View the [demo](https://ye-olde-guestbook.vercel.app) - View the [code](https://github.com/ElliotFriend/ye-olde-guestbook) - Read the [tutorial](../../apps/guestbook/README.mdx) --- ## Smart wallets Smart wallets are contract accounts that act as user wallets. They hold assets and enforce authorization in `__check_auth` instead of a single secret key. Passkeys (WebAuthn) are common, but you can also use Ed25519 keys, policy signers, session keys or anything the contract can verify. ## When to use a smart wallet - You need programmable authorization (limits, allow lists, multi-factor approvals such as user plus device key). - You want a passkey or hardware key experience without exposing seed phrases. - You need flexible signer mixes: passkeys for UX, Ed25519 for compatibility, policy or multisig signers for risk controls. ## WebAuthn [WebAuthn](https://www.w3.org/TR/webauthn) is a browser standard for passwordless authentication using public key cryptography. A device creates a keypair and proves possession with a challenge/response flow. Keys stay on the device or synced across devices through cloud providers. Benefits: - Works across modern browsers and platforms. - Familiar flows (Touch ID, Face ID, hardware keys) without seed phrases. - Produces signatures you verify in `__check_auth`. ## secp256r1 on Stellar `secp256r1` (prime256v1) is the curve most WebAuthn authenticators use. Stellar added native verification for this curve in Protocol 21, so contracts can validate WebAuthn signatures on chain. ## Passkeys - WebAuthn is the browser standard for passwordless auth. - secp256r1 is the curve most authenticators use; Stellar verifies it on-chain. - Passkeys are the platform or hardware-backed credentials that implement WebAuthn. - See [examples](./examples.mdx) for real projects. ## Passkeys in practice - Registration: use WebAuthn to create a device keypair; store the public key (and optional credential ID) in contract state. - Signing: request a WebAuthn assertion when the user approves an action; it returns a signature over the payload. - Verification: pass the signature and credential ID to your contract; in `__check_auth`, verify the secp256r1 signature and apply any policy checks (limits, allow lists, timelocks). ## Tooling - **Passkey Kit**: TypeScript SDK for creating contract accounts and signing with passkeys. - Demo: [passkey-kit-demo.pages.dev](https://passkey-kit-demo.pages.dev) - Code: [github.com/kalepail/passkey-kit](https://github.com/kalepail/passkey-kit) - **Launchtube**: relay for submitting transactions and handling fees/sequence numbers. - Code: [github.com/stellar/launchtube](https://github.com/stellar/launchtube) ## Get involved - Join the conversation in the `#passkeys` channel on the [Stellar Developer Discord](https://discord.gg/stellardev). - Track the evolving contract account interface in [the SEP discussion](https://github.com/orgs/stellar/discussions/1499). --- ## Contract Conventions These guides describe the "typical" way something might be accomplished in a Rust contract. These guides aren't meant to be quite as _prescriptive_ as some others; instead, they serve to highlight some of the norms we've seen when crop up in contract development. --- ## Making cross-contract calls As with developing software in any language, developing a Stellar smart contract with a rich feature set can be a challenging and time-consuming task. Thankfully, someone else might already have solved part of your issues or built components which can be reused. The open source community is vibrant and Stellar's community does not disappoint. There are two kinds of dependencies that can be introduced in a Stellar smart contract: 1. Other Rust crates can be used (provided that they are compatible with Soroban's [Rust dialect](../../../learn/fundamentals/contract-development/rust-dialect.mdx)); 2. Other smart contract can be used. This is referred to as a [cross-contract call](../../../learn/fundamentals/contract-development/contract-interactions/cross-contract.mdx). In the following, we will see how contracts can be leveraged from within another contract. ## Contract as a dependency While finding a contract is out of scope for this guide, there are a few places to be on the lookout. Most projects and dApps publicly disclose the address of their Stellar smart contract on their website. With this information, a [block explorer](../../../tools/developer-tools/block-explorers.mdx) is a powerful tool to understand how a contract is being used. Some explorers also allow you to download the compiled contract as a Wasm file. There are also projects that provide a link to access the code itself. :::info[Contract address] To depend on a project, we need to know the contract address of the contract to depend on. ::: ## Public API All public functions of a contract can be called. Using a network explorer can be helpful as some allow you to see the Rust interface of a contract. Bindings can also be generated using the CLI: ```bash stellar contract bindings rust --network testnet --contract-id ... --output-dir ... ``` ## Making a cross-contract call Once we know which function to call and which arguments to use, there are two main ways to make a cross-contract call: we can either manually invoke the contract or use a contract client. Let's start by manually invoking the contract using only a contract's address. In this example, we have an external contract with a public function named `add_with` which takes two `u32` as input values to sum them. ```rust #[contract] pub struct ContractB; #[contractimpl] impl ContractB { pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 { env.invoke_contract(&contract, symbol_short!("add"), vec![&env, x.to_val(), y.to_val()]) } } ``` Only using the contract address comes with its own challenges. We don't have a contract client so we don't have any typing inference, and have to manually convert function inputs to `Val`. If we want more tools to help us, we need to get access to a contract client. A common way to do this is to load the Wasm of a contract using `contractimport!`. This allows us to pass normal types without needing manual conversions from our side. Behind the scenes, this way of doing it is simply a convenient wrapper around `env.invoke_contract`. ```rust mod contract_a { soroban_sdk::contractimport!( file = "soroban_contract_a.wasm" ); } #[contract] pub struct ContractB; #[contractimpl] impl ContractB { pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 { let client = contract_a::Client::new(&env, &contract); client.add(&x, &y) } } ``` Although we have access to the Wasm, we still need a contract address because there could be multiple contracts deployed on-chain that use the same underlying code. This can be of importance if, for instance, the function that you need requires access to stored values from other users. :::tip[Address and Wasm] Thanks to the Rust bindings of the external contract, we can also use any public enum of the contract as if we would have defined them within our own contract. ```rust client::ContractAEnum::SomeField ``` ::: ## Handling responses In the examples above, we have used `env.invoke_contract` and `client.some_function`. In both cases, if there is an issue with the underlying contract call, the contract will panic. This might be a valid approach, but in some cases we want to catch errors and handle them depending on the outcome. This allows you to forward a custom error message, or even trigger an alternative code path. Enter `try_`. By using `env.try_invoke_contract` or `client.try_some_function`, underlying errors won't make the contract panic. Instead, errors will be wrapped and can be handled. For example if we wanted to default to 0 in case of an error: ```rust client.try_add(&x, &y).unwrap_or(Ok(0)).unwrap() ``` Of course, we could have far more complex error handling by leveraging the `match` statement: ```rust match client.try_add(&x, &y) { // the contract returned a value Ok(Ok(number)) => todo!("do something with the number returned"), Ok(Err(ConversionError)) => todo!("got a value back, but it wasn't a number like we expected"), // the contract errored Err(Ok(Error::AnError)) => todo!("do something when an error occurs that the contract included in its contract spec"), Err(Err(status)) => todo!("do something when an unrecognized error, or system error, occurs"), } ``` ## Depending on another contract Congratulations, now you can effectively leverage the whole Soroban ecosystem and its myriad of smart contracts. There is one last point to discuss before closing up: **dependability**. As with calling any smart contract, depending on an external smart contract should be done with care. It is advisable to do your own research and analysis on the contract you want to use. As contracts can be updated without their address changing, it is important to pay close attention to any changes in the underlying code. :::tip[Contract Wasm] The Wasm can be fetched by using the [Stellar CLI](../../../tools/cli/README.mdx). This can serve as a quick way to (for example) automate a hash check in a continuous integration system. However, such a check would not provide strong on-chain guarantees, but one could build a contract for that! ```bash stellar contract fetch --id C... --network ... > contract.wasm ``` ::: Besides this security consideration, upgrading a contract is an integral part of a contract's lifecycle. New features are added, bugs are fixed, and public API changes are made. Here as well, it is important to observe any development on these contracts to ensure the continuous operation of your own contract. ## Examples See the following full example with tests: - [Cross-contract calls](../../smart-contracts/example-contracts/cross-contract-call.mdx) shows how to create two contracts, deploy them, and then how to call one from the other. --- ## Deploy a contract from installed Wasm bytecode using a deployer contract {`Deploy a contract from installed Wasm bytecode using a deployer contract`} ## Overview This guide walks through the process of deploying a smart contract from installed Wasm bytecode using a deployer contract. We will cover setting up your environment, uploading Wasm bytecode, and deploying and initializing a contract atomically. ### Prerequisites: - Basic understanding of [Rust programming language](https://www.rust-lang.org). To brush up on Rust, check out [Rustlings](https://github.com/rust-lang/rustlings) or [The Rust book](https://doc.rust-lang.org/book). - Familiarity with [Stellar smart contracts](../../smart-contracts/getting-started/hello-world.mdx) - Installed Cargo, [Stellar CLI](../../smart-contracts/getting-started/setup.mdx#install-the-stellar-cli) and Soroban SDK ### Setup environment The [deployer example](https://github.com/stellar/soroban-examples/tree/main/deployer) demonstrates how to deploy contracts using a contract. 1. Clone the Soroban Examples Repository: ```bash git clone -b main https://github.com/stellar/soroban-examples ``` 2. Navigate to the Deployer Example: ```bash cd soroban-examples/deployer/deployer ``` :::note For this example to work, you should navigate to `deployer/contract` and `deployer/deployer` and run the command `stellar contract build` in each directory to generate the target files. ::: 3. Run tests: In the `deployer/deployer`, run the following command: ```bash cargo test ``` You should see the output indicating the tests passed: ```bash running 1 test test test::test ... ok ``` ### Code overview ```rust title="deployer/deployer/src/lib.rs" #[contract] pub struct Deployer; const ADMIN: Symbol = symbol_short!("admin"); #[contractimpl] impl Deployer { /// Construct the deployer with a provided administrator. pub fn __constructor(env: Env, admin: Address) { env.storage().instance().set(&ADMIN, &admin); } /// Deploys the contract on behalf of the `Deployer` contract. /// /// This has to be authorized by the `Deployer`s administrator. pub fn deploy( env: Env, wasm_hash: BytesN<32>, salt: BytesN<32>, constructor_args: Vec, ) -> Address { let admin: Address = env.storage().instance().get(&ADMIN).unwrap(); admin.require_auth(); // Deploy the contract using the uploaded Wasm with given hash on behalf // of the current contract. // Note, that not deploying on behalf of the admin provides more // consistent address space for the deployer contracts - the admin could // change or it could be a completely separate contract with complex // authorization rules, but all the contracts will still be deployed // by the same `Deployer` contract address. let deployed_address = env .deployer() .with_address(env.current_contract_address(), salt) .deploy_v2(wasm_hash, constructor_args); deployed_address } } ``` ## How it works The deployer contract provides a mechanism to deploy other contracts in a secure and deterministic manner. It stores an administrator address at construction time and requires that administrator's authorization for every deployment. It also supports atomic initialization of the newly deployed contract via constructor arguments. This guarantees that the contract is properly initialized immediately after deployment, preventing any potential issues with uninitialized contracts. ### Function breakdown - `env: Env`: The Env object represents the current contract execution environment. It provides methods to interact with the blockchain and other contracts, as well as perform various operations. - `admin: Address`: The Address of the administrator, provided to the `__constructor` function and stored for later use. Only this address can authorize deployments. - `wasm_hash: BytesN<32>`: The hash of the Wasm bytecode of the contract to be deployed and must already be installed and on the ledger. This hash is used to uniquely identify the contract's code. - `salt: BytesN<32>`: A unique value used to derive the address of the deployed contract. The same combination of the `Deployer` contract's address and salt will always produce the same deployed contract address, ensuring deterministic contract addresses. - `constructor_args: Vec`: A vector of arguments, specified as `{type: value}` objects, to be passed to the constructor of the deployed contract. ### Function steps ```rust let admin: Address = env.storage().instance().get(&ADMIN).unwrap(); admin.require_auth(); ``` The function loads the administrator address that was stored during construction and requires its authorization, ensuring that only the administrator can trigger a deployment. ```rust let deployed_address = env .deployer() .with_address(env.current_contract_address(), salt) .deploy_v2(wasm_hash, constructor_args); ``` - Uses the `env.deployer()` method to create a deployer object. - `with_address(env.current_contract_address(), salt)` specifies that the deployed contract's address is derived from the `Deployer` contract's own address and the given salt. - `deploy_v2(wasm_hash, constructor_args)` deploys the contract using the provided Wasm bytecode hash, invokes its constructor with `constructor_args`, and returns the address of the newly deployed contract. ```rust deployed_address ``` Returns the address of the newly deployed contract. ### Build the contracts To build the contract into a `.wasm` file, use the `stellar contract build` command. This command builds both the deployer contract and the test contract. ```sh stellar contract build ``` Both `.wasm` files should be found in both contract `target` directories after building both contracts: ``` target/wasm32v1-none/release/soroban_deployer_contract.wasm ``` ``` target/wasm32v1-none/release/soroban_deployer_test_contract.wasm ``` ## Run the contract Before deploying the test contract with the deployer, install the test contract Wasm using the `upload` command. The `upload` command will print out the hash derived from the Wasm file which should be used by the deployer. ```sh stellar contract upload \ --wasm contract/target/wasm32v1-none/release/soroban_deployer_test_contract.wasm \ --source-account alice \ --network testnet ``` When deploying a smart contract to the network, you must specify an identity that will be used to sign the transactions. Change the `alice` [identity] to your own. [identity]: ../../smart-contracts/getting-started/setup.mdx#configure-an-identity The command prints out the hash as hex. It will look something like `6bc6d975ef99c057231481c40f69f4c2a8a89ac56e8826ef0378f8e5c6abc4be`. We also need to deploy the `Deployer` contract. Since the `Deployer` contract's `__constructor` function requires an administrator address, we provide the `alice` [identity] as the `--admin` argument. Create and provide your own identity, where necessary. ```sh stellar contract deploy \ --wasm deployer/target/wasm32v1-none/release/soroban_deployer_contract.wasm \ --source-account alice \ --network testnet \ -- --admin alice ``` This will return the deployer address. For example: `CDKYZMA3OXR54YAHOQ5D4EWDFDT3ZNKKRYKQT7MGPWH22ZVD2DX2BJID`. Then the deployer contract may be invoked with the Wasm hash value above. The `constructor_args` are passed through to the constructor of the deployed contract, so they are used here to set the initial `value` to `8`. ```sh stellar contract invoke \ --id CDKYZMA3OXR54YAHOQ5D4EWDFDT3ZNKKRYKQT7MGPWH22ZVD2DX2BJID \ --source-account alice \ --network testnet \ -- \ deploy \ --salt 123 \ --wasm_hash 6bc6d975ef99c057231481c40f69f4c2a8a89ac56e8826ef0378f8e5c6abc4be \ --constructor_args '[{"u32":8}]' ``` The deployer contract invocation will return the Contract address (For example: `CCTVFX6BFTQHTGAHA5TY4YZQJRUKRE2RRNUTGVBNKE3PJF5C7CI53APY`) of the newly deployed test contract. Invoke the deployed test contract using the address returned from the previous command. ```sh stellar contract invoke \ --id CCTVFX6BFTQHTGAHA5TY4YZQJRUKRE2RRNUTGVBNKE3PJF5C7CI53APY \ --source-account alice \ --network testnet \ -- \ value ``` You should receive something like the following output: ```json 8 ``` --- ## Deploy a SAC for a Stellar asset using code {`Deploy a Stellar Asset Contract (SAC) for a Stellar asset using code`} ## Overview In this guide, you'll learn how to deploy a [Stellar Asset Contract (SAC)](../../../tokens/stellar-asset-contract.mdx) for a Stellar asset using the [Stellar SDK](../../../tools/sdks/client-sdks.mdx#javascript-sdk). The Stellar SDK is a set of tools and libraries designed to help developers build applications that interact with the Stellar blockchain network. ### Prerequisites: - [Node.js ](https://nodejs.org/en) and npm installed. - Stellar SDK for [JavaScript](https://www.npmjs.com/package/@stellar/stellar-sdk) installed - [Knowledge about Issuing an Asset on Stellar](../../../tokens/how-to-issue-an-asset.mdx) - An understanding of the rudimentary, retry-enabled transaction polling function `submitTx` which we outlined in [another guide](../transactions/submit-transaction-wait-js.mdx) ## Code overview ```javascript title="deployassetcontract.js" const networkRPC = "https://soroban-testnet.stellar.org"; const server = new StellarSdk.rpc.Server(networkRPC); const networkPassphrase = StellarSdk.Networks.TESTNET; const deployStellarAssetContract = async () => { const sourceSecrets = "SASI6PA4K52GQJF6BC263GLYOADVKFJ4SZ7TFX4QQF2U76T3EJ54DT7Y"; // Replace with your Secret Key const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSecrets); const sourceAccount = await server.getAccount(sourceKeypair.publicKey()); try { const assetCode = "JOEBOY"; const issuerPublicKey = sourceKeypair.publicKey(); const customAsset = new StellarSdk.Asset(assetCode, issuerPublicKey); const transaction = new StellarSdk.TransactionBuilder(sourceAccount, { fee: StellarSdk.BASE_FEE, networkPassphrase, }) .addOperation( StellarSdk.Operation.createStellarAssetContract({ asset: customAsset, }), ) .setTimeout(30) .build(); const uploadTx = await server.prepareTransaction(transaction); uploadTx.sign(sourceKeypair); const feedback = await submitTx(uploadTx); const contract = StellarSdk.Address.fromScAddress( feedback.returnValue.address(), ); console.log( `ContractID of Our ${customAsset.code} Asset`, contract.toString(), ); } catch (e) { console.error("An error occurred while Deploying assets:", e); } }; await deployStellarAssetContract(); ``` ## Code explanation **Server Setup** ```javascript const networkRPC = "https://soroban-testnet.stellar.org"; const server = new StellarSdk.rpc.Server(networkRPC); const networkPassphrase = StellarSdk.Networks.TESTNET; ``` - `networkRPC`: The URL for the Soroban testnet. - `server`: A new instance of `rpc.Server` is created, which will be used to interact with the Soroban testnet. - `networkPassphrase`: sets the network passphrase to the TESTNET **`DeployStellarAssetContract` function** ```javascript const deployStellarAssetContract = async () => { const sourceSecrets = "SASI6PA4K52GQJF6BC263GLYOADVKFJ4SZ7TFX4QQF2U76T3EJ54DT7Y"; // Replace with your Secret Key const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSecrets); const sourceAccount = await server.getAccount(sourceKeypair.publicKey()); try { const assetCode = "JOEBOY"; const issuerPublicKey = sourceKeypair.publicKey(); const customAsset = new StellarSdk.Asset(assetCode, issuerPublicKey); const transaction = new StellarSdk.TransactionBuilder(sourceAccount, { fee: StellarSdk.BASE_FEE, networkPassphrase, }) .addOperation( StellarSdk.Operation.createStellarAssetContract({ asset: customAsset, }), ) .setTimeout(30) .build(); const uploadTx = await server.prepareTransaction(transaction); uploadTx.sign(sourceKeypair); const feedback = await submitTx(uploadTx); const contract = StellarSdk.Address.fromScAddress( feedback.returnValue.address(), ); console.log( `ContractID of Our ${customAsset.code} Asset`, contract.toString(), ); } catch (e) { console.error("An error occurred while Deploying assets:", e); } }; ``` This function is designed to deploy a Stellar Asset Contract (SAC) on the Soroban testnet. - **Secret Key**: It starts by defining the secret key for the source account (`sourceSecrets`), which you must replace with your own. - **Keypair and Account**: Generates the keypair from the secret key and fetches the account details from the Soroban server. - **Custom Asset**: Defines a custom asset with the code `JOEBOY` and the issuer's public key. - **Transaction Building**: A transaction is built using the `TransactionBuilder`, which includes the `createStellarAssetContract` operation for the custom asset. The transaction is then prepared and signed. - **Send Transaction**: The signed transaction is sent to the network using `server.sendTransaction`. - **Feedback Handling**: It waits for the transaction feedback using the `submitTx` function to ensure it has succeeded. Then, it extracts the return value and converts it to a contract ID using `StellarSdk.Address`. Finally, it logs the contract ID for the deployed asset. --- ## Organize contract errors with an error enum type A convenient way to manage and meaningfully communicate contract errors is to collect them into an `enum` struct. These errors are a special type of enum integer type that are stored on the ledger as Error values containing a `u32` code. First, create the `Error` struct in your smart contract. ```rust #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum Error { FirstError = 1, AnotherError = 2, YetAnotherError = 3, GenericError = 4 } ``` Smart contracts can fail with error enums in two different ways. They can either return a `Result` with their intended return value and the `#[contracterror]` struct as the error, or just invoke `panic_with_error!` with the appropriate Error enum value whenever an error condition is reached. By default, most ecosystem standards assume that contract functions do not return a `Result`, so using `panic_with_error!` is recommended. However, both styles behave in the same way. If an error is returned or `panic_with_error!` is invoked, the transaction will fail. Contracts making cross contract calls have the ability to catch and handle these failures with `try_` functions. ```rust #[contractimpl] impl Contract { /// Call `panic_with_error!` to fail with custom errors /// This is the default, recommended approach adopted by most SEP standards pub fn cause_error(env: Env, error_code: u32) -> u32 { let error_type = match error_code { 0 => return 0, 1 => Error::FirstError, 2 => Error::AnotherError, 3 => Error::YetAnotherError, _ => Error::GenericError, }; panic_with_error!(env, error_type); } /// Return `Err` to fail with custom errors pub fn cause_error_result(env: Env, error_code: u32) -> Result { let error_type = match error_code { 0 => return Ok(0), 1 => Error::FirstError, 2 => Error::AnotherError, 3 => Error::YetAnotherError, _ => Error::GenericError, }; return Err(error_type); } } ``` When converted to XDR, the value becomes an `ScVal`, containing a `ScError`, containing the integer value of the error as contract error. ```json { "error": { "contractError": 1 } } ``` --- ## Extend a deployed contract's TTL with code # Extending a deployed contract's TTL using code When a smart contract is deployed on the Stellar network, two ledger entries are created. A contract instance ledger entry is created that defines the contract address, which WebAssembly (WASM) code it uses, and any additional storage the contract utilizes. The WebAssembly (WASM) code also has its own ledger entry that can be shared by multiple contract instances. Each ledger entry has a [Time To Live (TTL)](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#ttl) that determines how long it remains accessible. The TTL is the number of ledgers between the current ledger and the final ledger for which the data can still be accessed. If the TTL expires, the data becomes archived and inaccessible. To prevent this, you need to periodically extend the TTL of the contract's Wasm code and instance entry. This guide will show you how to extend the TTL of a deployed contract's Wasm code and instance within a contract, and by submitting a TTL extension transaction using the Stellar JS SDK or CLI. ## Understanding TTL in Soroban Before we demonstrate the TTL extension methods, you should note that in Soroban: - The contract instance and code are two separate, persistent storage entries - TTL exists to prevent the blockchain from being cluttered with unused storage entries - TTL extension can be done for both the contract instance and the contract code ## Prerequisites - Stellar SDK: `npm install @stellar/stellar-sdk` for Javascript - Install the [Stellar CLI](../../../tools/cli/install-cli.mdx) - A [Stellar RPC](../../../data/apis/rpc/README.mdx) endpoint (e.g., `https://soroban-testnet.stellar.org`) - Basic knowledge of the SDK in use ## Extending Contract Code and Instance TTL with a Contract 1. Self-Extension: Extending the TTL from within the contract itself, in Rust. - Use case: When a contract needs to manage its own lifetime - Process: Directly accessing the contract's instance storage to extend its TTL 2. External Extension: Extending the TTL from another contract (the deployer), in Rust. - Use case: When managing multiple contract instances or implementing administrative control - Process: Using the deployer's authority to extend TTL for any contract it has deployed ```rust #![no_std] use soroban_sdk::{contract, contractimpl, Address, Env}; #[contract] pub struct ExtendTTLContract; #[contractimpl] impl ExtendTTLContract { // Self-extension pub fn extend_contract_ttl(env: Env, threshold: u32, extend_to: u32) { env.storage().instance().extend_ttl(threshold, extend_to); } // External extension pub fn extend_other_contract_ttl(env: Env, contract_address: Address, threshold: u32, extend_to: u32) { let deployer = env.deployer(); deployer.extend_ttl( contract_address, threshold, extend_to ); } } ``` - `env.storage().instance().extend_ttl(...)` is called to extend the TTL of the current contract instance and the code entries. - `threshold` is a check that ensures that the current TTL of the contract instance is less than the set threshold value. - `extend_to` is the minimum number of ledgers from the current ledger that the TTL should be extended to (if the current TTL is already greater, this call is a no-op). - `contract_address` is the address of the contract instance whose TTL we want to extend. - `env.deployer()` accesses the deployer, which has methods for managing the contract's lifecycle. - `deployer.extend_ttl(...)` extends the TTL of the specified contract instance and code entries. ## Extending Contract Code and Instance TTL with a Transaction - Use Case: When you need to manage contract TTLs through an external application or automated system. - Process: - Get the contract's footprint - Set the entries you want to extend as read-only in the Soroban Data - Create an operation `StellarSdk.Operation.extendFootprintTtl` with the new TTL value (`extendTo`), this will ensure the entries' TTL will be at least extendTo ledgers from now - Simulate to determine the resource fee needed to extend the TTL - Sign and submit the transaction :::note A resource fee and inclusion fee are both charged in this transaction. ::: The CLI has separate commands to extend contract code and contract instance ledger entries. The `--ledgers-to-extend` argument is the new entries TTL value. Contract Code: ```bash stellar contract extend \ --network testnet \ --source-account alice \ --ledgers-to-extend 500000 \ --wasm-hash a7e8e679a9692676e19926cca52aa975a2ca6c933368bc2b09739eed140f2adc ``` Contract Instance: ```bash stellar contract extend \ --network testnet \ --source-account alice \ --ledgers-to-extend 500000 \ --id CC77VMQFKNXIKZQ6LMX56FTH2S2NLAF2FDZIHITSM3UKW24Z75UZ4YW2 ``` The code below uses Nodejs environment but same concept can also be applied in the browser using Freighter wallet or using any other [Stellar SDK](../../../tools/sdks/client-sdks.mdx). ```javascript async function extendContractWasmTTL(contractId, wasmHash, sourceKeypair) { const server = new Server("https://soroban-testnet.stellar.org"); // Create a new transaction builder const account = await server.getAccount(sourceKeypair.publicKey()); const fee = "100"; // Inclusion fee // Get the contract instance ledger key const contract = new StellarSdk.Contract(contractId); const instance = contract.getFootprint(); // Build the contract code ledger key const contractCode = StellarSdk.xdr.LedgerKey.contractCode( new StellarSdk.xdr.LedgerKeyContractCode({ hash: Buffer.from(wasmHash, "hex"), }), ); // Set the Soroban data and create an operation to extend the contract's TTL // You can include any ledger keys you would like to extend in the array const sorobanData = new StellarSdk.SorobanDataBuilder() .setReadOnly([instance, contractCode]) .build(); const transaction = new StellarSdk.TransactionBuilder(account, { fee, networkPassphrase: StellarSdk.Networks.TESTNET, // Use appropriate network }) .setSorobanData(sorobanData) .addOperation( StellarSdk.Operation.extendFootprintTtl({ extendTo: 500_000, }), ) .setTimeout(30) .build(); // Simulate, sign and submit the transaction const extendTx = await server.prepareTransaction(transaction); extendTx.sign(sourceKeypair); const result = await server.sendTransaction(extendTx); console.log( "Transaction submitted. Result:", JSON.stringify(result, null, 2), ); return result; } // Usage const contractId = "CC77VMQFKNXIKZQ6LMX56FTH2S2NLAF2FDZIHITSM3UKW24Z75UZ4YW2"; const wasmHash = "a7e8e679a9692676e19926cca52aa975a2ca6c933368bc2b09739eed140f2adc"; const sourceKeypair = StellarSdk.Keypair.fromSecret( "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", ); extendContractWasmTTL(contractId, wasmHash, sourceKeypair) .then(console.log) .catch(console.error); ``` ### Breaking Down the Code Let's walk through the key parts of this function: 1. Setting up the Soroban data: The `SorobanDataBuilder()` is where we prep the Soroban-specific info for our transaction. - We use `setReadOnly([instance, contractCode])` to tell the network which contract stuff we need to access. We're using `setReadOnly()` instead of `setReadWrite()` because we're just extending the TTL, not changing any data. Why `setReadOnly()`? A few reasons: - It's faster and uses fewer resources - It's safer - we can't accidentally change data we're not supposed to - The `ExtendFootprintTTLOp` operation needs it 2. Adding the operation: After setting up the Soroban data, we add the `extendFootprintTtl` operation to our transaction. We're telling it to extend the TTL to 500,000 ledgers with `extendTo: 500_000`. 3. What's the point? This whole process is about keeping our contract's data alive in the ledger. It's like renewing a lease - we're telling the network "Hey, keep this stuff around longer, we're still using it!" This is super important for contracts that need to stick around for a while. Without extending the TTL, the contract's data could expire and disappear from the ledger. Learn how to test the TTL extension in this [guide](../archival/test-ttl-extension.mdx). Want to dive deeper? Check out the docs on the [Extend Footprint TTL operation](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#extendfootprintttlop). --- ## Upgrading Wasm bytecode for a deployed contract Upgrading Wasm Bytecode for a Deployed Contract ## Introduction Upgrading a smart contract allows you to improve or modify your contract without changing its address. This guide will walk you through the process of upgrading a WebAssembly (Wasm) bytecode contract using the Soroban SDK. ### Prerequisites: - Basic understanding of the [Rust programming language]. To brush up on Rust, check out [Rustlings](https://github.com/rust-lang/rustlings) or [The Rust book](https://doc.rust-lang.org/book). - Familiarity with [Stellar smart contracts](../../smart-contracts/getting-started/hello-world.mdx) - Installed [Stellar CLI](../../smart-contracts/getting-started/setup.mdx#install-the-stellar-cli) and Soroban SDK ### Download the upgradeable contract example The [upgradeable contract example] demonstrates how to upgrade a Wasm contract. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [upgradeable contract example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/upgradeable_contract [Rust programming language]: https://www.rust-lang.org/ ### Code The example contains both an "old" and "new" contract, where we upgrade from "old" to "new". The code below is for the "old" contract. ```rust title="upgradeable_contract/old_contract/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env}; #[contracttype] #[derive(Clone)] enum DataKey { Admin, } #[contract] pub struct UpgradeableContract; #[contractimpl] impl UpgradeableContract { pub fn __constructor(env: Env, admin: Address) { env.storage().instance().set(&DataKey::Admin, &admin); } pub fn version() -> u32 { 1 } pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); admin.require_auth(); env.deployer().update_current_contract_wasm(new_wasm_hash); } } mod test; ``` Source: https://github.com/stellar/soroban-examples/blob/v23.0.0/upgradeable_contract/old_contract/src/lib.rs ## How it works When upgrading a contract, the key function used is `env.deployer().update_current_contract_wasm`, which takes the Wasm hash of the new contract as a parameter. Here's a step-by-step breakdown of how this process works: 1. **No change in contract address**: The contract's address remains the same after the upgrade. This ensures that all references to the contract stay intact. 2. **The Wasm executable must already be uploaded**: The upgrade depends on the compiled executable (identified by the `new_wasm_hash`) being uploaded and available on the blockchain. This must be done _prior_ to invoking the contract's `upgrade(...)` function. 3. **Admin authorization**: Before upgrading, the contract checks if the action is authorized by the `Admin` address. This is crucial to prevent unauthorized upgrades. Only someone with admin rights can perform the upgrade. 4. **The upgrade function**: Below is the function that handles the upgrade process: ```rust pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); admin.require_auth(); env.deployer().update_current_contract_wasm(new_wasm_hash); } ``` - `env: Env`: The environment object representing the current state of the blockchain. - `new_wasm_hash: BytesN<32>`: The hash of the new Wasm code for the contract. The Wasm bytecode must already be installed/present on the ledger. - The function first retrieves the admin's address from the contract's storage. - It then requires the admin's authorization (`admin.require_auth()`) to proceed. - Finally, it updates the contract with the new Wasm bytecode (`env.deployer().update_current_contract_wasm(new_wasm_hash)`). 5. The `update_current_contract_wasm` host function will also emit a `SYSTEM` contract [event] that contains the old and new wasm reference, allowing downstream users to be notified when a contract they use is updated. The event structure will have `topics = ["executable_update", old_executable: ContractExecutable, new_executable: ContractExecutable]` and `data = []`. [event]: ../../../learn/fundamentals/stellar-data-structures/events.mdx#event-types ## Tests Open the `upgradeable_contract/old_contract/src/test.rs` file to follow along. ```rust title="upgradeable_contract/old_contract/src/test.rs" #![cfg(test)] extern crate std; use soroban_sdk::{ symbol_short, testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, Address, BytesN, Env, IntoVal, }; use crate::{UpgradeableContract, UpgradeableContractClient}; mod new_contract { soroban_sdk::contractimport!( file = "../new_contract/target/wasm32v1-none/release/soroban_upgradeable_contract_new_contract.wasm" ); } fn install_new_wasm(env: &Env) -> BytesN<32> { env.deployer().upload_contract_wasm(new_contract::WASM) } #[test] fn test() { let env = Env::default(); env.mock_all_auths(); let admin = Address::generate(&env); let contract_id = env.register(UpgradeableContract, (&admin,)); let client = UpgradeableContractClient::new(&env, &contract_id); assert_eq!(1, client.version()); let new_wasm_hash = install_new_wasm(&env); client.upgrade(&new_wasm_hash); assert_eq!(2, client.version()); // new_v2_fn was added in the new contract, so the existing // client is out of date. Generate a new one. let client = new_contract::Client::new(&env, &contract_id); assert_eq!(1010101, client.new_v2_fn()); // New contract version requires the `NewAdmin` key to be initialized, but since the constructor // hasn't been called, it is not initialized, thus calling try_upgrade won't work. let new_update_result = client.try_upgrade(&new_wasm_hash); assert!(new_update_result.is_err()); // `handle_upgrade` sets the `NewAdmin` key properly. client.handle_upgrade(); // Now upgrade should succeed (though we are not actually changing the Wasm). client.upgrade(&new_wasm_hash); // The new admin is the same as the old admin, so the authorization is still performed for // the `admin` address. assert_eq!( env.auths(), std::vec![( admin, AuthorizedInvocation { function: AuthorizedFunction::Contract(( contract_id.clone(), symbol_short!("upgrade"), (new_wasm_hash,).into_val(&env), )), sub_invocations: std::vec![] } )] ) } ``` Source: https://github.com/stellar/soroban-examples/blob/v23.0.0/upgradeable_contract/old_contract/src/test.rs We first import the compiled Wasm file for the new contract: ```rust mod new_contract { soroban_sdk::contractimport!( file = "../new_contract/target/wasm32v1-none/release/soroban_upgradeable_contract_new_contract.wasm" ); } ``` We register the old contract, initialize it with an admin, and verify the version it returns. The note in the code below is important: ```rust let admin = Address::generate(&env); let contract_id = env.register(UpgradeableContract, (&admin,)); let client = UpgradeableContractClient::new(&env, &contract_id); assert_eq!(1, client.version()); ``` We install the new contract's Wasm: ```rust let new_wasm_hash = install_new_wasm(&env); ``` Then we run the upgrade, and verify that the upgrade worked: ```rust client.upgrade(&new_wasm_hash); assert_eq!(2, client.version()); ``` ## Build the contract To build the contract `.wasm` files, run `stellar contract build` in both `upgradeable_contract/old_contract` and `upgradeable_contract/new_contract` in that order. Both `.wasm` files should be found in both contract `target` directories after building both contracts: ``` target/wasm32v1-none/release/soroban_upgradeable_contract_old_contract.wasm ``` ``` target/wasm32v1-none/release/soroban_upgradeable_contract_new_contract.wasm ``` ## Run the contract If you have [`stellar-cli`] installed, you can invoke contract functions. Deploy the old contract and install the Wasm for the new contract. First, navigate to the `upgradeable_contract/old_contract` directory, and deploy an instance of the old contract. We're providing the `alice` identity to the `__constructor` function, so it will be the contract's `Admin` address. Create and provide your own [identities], where necessary. This command will output the contract address it was deployed to. ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_upgradeable_contract_old_contract.wasm \ --source-account alice \ --network testnet \ -- --admin alice # CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3 ``` Then, navigate to `upgradeable_contract/new_contract` and upload the compiled executable file for the new contract. This command will output the Sha256 hash of the executable, which will be used later for the `new_wasm_hash` parameter. ```sh stellar contract upload \ --source-account alice \ --wasm target/wasm32v1-none/release/soroban_upgradeable_contract_new_contract.wasm \ --network testnet # aa24c81289997ad815489b29db337b53f284cca5aba86e9a8ae5cef7d31842c2 ``` Our deployed `old_contract` address is `CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3`. You may need to replace this value with your own. Invoke the `version` function of the contract, to see the current deployed version. ```sh stellar contract invoke \ --id CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3 \ --source-account alice \ --network testnet \ -- version # 1 ``` Now upgrade the contract. Notice the `--source-account` should be the identity name matching the address passed to the `__constructor` function, when the contract was deployed. ```sh stellar contract invoke \ --id CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3 \ --source-account alice \ --network testnet \ -- \ upgrade \ --new_wasm_hash aa24c81289997ad815489b29db337b53f284cca5aba86e9a8ae5cef7d31842c2 ``` Invoke the `version` function again. Now that the contract was upgraded, you'll see a new version. ```sh stellar contract invoke \ --id CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3 \ --source-account alice \ --network testnet \ -- version # 2 ``` Hooray, our contract has been upgraded! [`stellar-cli`]: ../../smart-contracts/getting-started/setup.mdx#install-the-stellar-cli [identities]: ../../smart-contracts/getting-started/setup.mdx#configure-an-identity --- ## Write metadata for your contract Write structured metadata. The [`contractmeta!`] macro provided in the Rust SDK allows users to write two strings - a `key` and a `val` - within a serialized `SCMetaEntry::SCMetaV0` XDR object to the custom section of Wasm contracts. The section name for this metadata is `contractmetav0`. Developers can utilize this macro to write metadata, and tools can then read and display this information to users. The [liquidity pool example] provides a clear demonstration of how to use the `contractmeta!` macro: ```rust // Metadata that is added on to the Wasm custom section contractmeta!( key = "Description", val = "Constant product AMM with a .3% swap fee" ); pub trait LiquidityPoolTrait {... ``` [`contractmeta!`]: https://docs.rs/soroban-sdk/22.0.4/soroban_sdk/macro.contractmeta.html [liquidity pool example]: https://github.com/stellar/soroban-examples/blob/v22.0.1/liquidity_pool/src/lib.rs#L152-L155 --- ## Workspaces # Workspace Using Cargo's workspace [feature](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) makes it very convenient to organize your smart contracts in subdirectories of your project's root. It's very simple to get started using the cli: ``` stellar contract init soroban-project --name add_contract ``` Running this command will create a root project directory (`soroban-project`) and then initialize a project workspace with a single contract named `add_contract`. Adding one more contract template to the project can be done using the same command: ``` stellar contract init soroban-project --name main_contract ``` The project tree in the `soroban-project` directory will look like this: ``` . ├── Cargo.toml ├── contracts │   ├── add-contract │   │   ├── Cargo.toml │   │   ├── Makefile │   │   └── src │   │   ├── lib.rs │   │   └── test.rs │   └── main-contract │   ├── Cargo.toml │   ├── Makefile │   └── src │   ├── lib.rs │   └── test.rs └── README.md ``` Running `stellar contract init` command created a new contract, located in `./contracts`, each containing: - Cargo.toml file with the `soroban-sdk` dependency - `src` directory with an example hello-world contract and a test. Build the contracts with the following command (don't forget to change working directory to `soroban-project` first before running it), and check the build directory `target/wasm32v1-none/release/` for the compiled `.wasm` files. ``` stellar contract build ``` ### Integrating Contracts in the Same Workspace With the given project structure, cross-contract calls can be easily made. Starting with modifying the sample hello world contract into an add contract: ```rust title="contracts/add_contract/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl}; #[contract] pub struct ContractAdd; #[contractimpl] impl ContractAdd { pub fn add(x: u32, y: u32) -> u32 { x.checked_add(y).expect("no overflow") } } ``` :::tip In this tutorial we use workspaces to import contract client. However, it's also possible to use contract's compiled code instead (for example, if you don't have a source code for it). See [making cross-contract calls](./cross-contract.mdx) guide for more info ::: Next, in order to call `ContractAdd` from another contract, it's necessary to add a workspace dependency: ```toml title="./contracts/main_contract/Cargo.toml" # <...> [dependencies] soroban-sdk = { workspace = true } add_contract = { path = "../add_contract" } # <...> ``` The `ContractAdd` can now be referenced and used from another contracts using `ContractAddClient`: ```rust title="contracts/main_contract/src/lib.rs" #![no_std] use add_contract::ContractAddClient; use soroban_sdk::{contract, contractimpl, Address, Env}; #[contract] pub struct ContractMain; #[contractimpl] impl ContractMain { pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 { let client = ContractAddClient::new(&env, &contract); client.add(&x, &y) } } mod test; ``` Here, main contract will invoke `ContractAdd`'s `add` function to calculate the sum of 2 numbers. It's a good idea to update tests for our main contract as well: ```rust title="contracts/main_contract/src/test.rs" #![cfg(test)] use crate::{ContractMain, ContractMainClient}; use soroban_sdk::Env; use add_contract::ContractAdd; #[test] fn test_adding_cross_contract() { let env = Env::default(); // Register add contract using the imported contract. let contract_add_id = env.register(ContractAdd, ()); // Register main contract defined in this crate. let contract_main_id = env.register(ContractMain, ()); // Create a client for calling main contract. let client = ContractMainClient::new(&env, &contract_main_id); // Invoke main contract via its client. Main contract will invoke add contract. let sum = client.add_with(&contract_add_id, &5, &7); assert_eq!(sum, 12); } ``` Contracts can now be re-complied running the following command from the project root: ``` stellar contract build ``` And check that the test is working correctly, running tests in `contracts/main_contract`: ``` cargo test running 1 test test test::test_adding_cross_contract ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s ``` Finally, let's deploy this contracts and call our main contract using cli. If you haven't already, set up an account (alice) first ```bash stellar keys generate alice --fund --network testnet stellar keys use alice ``` Second is to deploy the contracts: ```bash stellar contract deploy --network testnet --wasm target/wasm32v1-none/release/add_contract.wasm --alias add_contract stellar contract deploy --network testnet --wasm target/wasm32v1-none/release/main_contract.wasm --alias main_contract ``` And finally call the main contract: ``` $ stellar contract invoke --id main_contract --network testnet -- add_with --contract add_contract --x 9 --y 10 ℹ️ Send skipped because simulation identified as read-only. Send by rerunning with `--send=yes`. 19 ``` ### Adding contract interfaces As the next step, we can abstract away add contract and allow it to have multiple implementations. Main contract will in turn use the contract interface that is not bound to its implementation. ``` stellar contract init . --name adder_interface stellar contract init . --name add_extra_contract ``` First, let's create an interface and change our existing implementation to use this interface: ```rust title="contracts/adder_interface/src/lib.rs" #![no_std] use soroban_sdk::contractclient; #[contractclient(name = "AdderClient")] pub trait Adder { fn add(x: u32, y: u32) -> u32; } ``` To use the interface definition our workspace members will now have an `adder_interface` as a dependency: ```toml title="./Cargo.toml" # <...> [workspace.dependencies] soroban-sdk = "26" adder-interface = { path = "contracts/adder_interface" } # <...> ``` ```toml title="./contracts/add_contract/Cargo.toml" # <...> [dependencies] soroban-sdk = { workspace = true } adder-interface = {workspace = true} # <...> ``` ```toml title="./contracts/main_contract/Cargo.toml" # <...> [dependencies] soroban-sdk = { workspace = true } adder-interface = {workspace = true} add_contract = { path = "../add_contract" } # <...> ``` ```toml title="./contracts/add_extra_contract/Cargo.toml" # <...> [dependencies] soroban-sdk = { workspace = true } adder-interface = {workspace = true} # <...> ``` And change lib type of `adder_interface` crate: ```toml title="./contracts/adder_interface/Cargo.toml" # <...> [lib] crate-type = ["rlib"] # <...> ``` ```rust title="./contracts/adder_interface/src/lib.rs" #![no_std] use soroban_sdk::contractclient; #[contractclient(name = "AdderClient")] pub trait Adder { fn add(x: u32, y: u32) -> u32; } ``` ```rust title="contracts/add_contract/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl}; use adder_interface::Adder; #[contract] pub struct ContractAdd; #[contractimpl] impl Adder for ContractAdd { fn add(x: u32, y: u32) -> u32 { x.checked_add(y).expect("no overflow") } } ``` ```rust title="contracts/main_contract/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl, Address, Env}; use adder_interface::AdderClient; #[contract] pub struct ContractMain; #[contractimpl] impl ContractMain { pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 { let client = AdderClient::new(&env, &contract); client.add(&x, &y) } } mod test; ``` As the final step we can create an alternative `Adder` implementation that adds an extra 1: ```rust title="contracts/add_extra_contract/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl}; use adder_interface::Adder; #[contract] pub struct ContractAdd; #[contractimpl] impl Adder for ContractAdd { fn add(x: u32, y: u32) -> u32 { x.checked_add(y).expect("no overflow").checked_add(1).expect("no overflow") } } ``` We can now deploy this contracts and test the new behavior: ```bash stellar contract build stellar contract deploy --network testnet --wasm target/wasm32v1-none/release/add_contract.wasm --alias add_contract stellar contract deploy --network testnet --wasm target/wasm32v1-none/release/add_extra_contract.wasm --alias wrong_math_contract stellar contract deploy --network testnet --wasm target/wasm32v1-none/release/main_contract.wasm --alias main_contract ``` Now let's try to do sum 2 unsigned integers causing an overflow: ``` $ stellar contract invoke --id main_contract --network testnet -- add_with --contract add_contract --x 2 --y 2 ℹ️ Send skipped because simulation identified as read-only. Send by rerunning with `--send=yes`. 4 $ stellar contract invoke --id main_contract --network testnet -- add_with --contract wrong_math_contract --x 2 --y 2 ℹ️ Send skipped because simulation identified as read-only. Send by rerunning with `--send=yes`. 5 ``` --- ## Type Conversions A collection of guides for converting from one data type to another in a variety of SDK languages. --- ## Convert an address to other types The `Address` is an opaque type that represents either a 'default' externally owned account on the Stellar network, or a contract (that may also provide logic for custom externally owned accounts, see [authorization docs](../../../learn/fundamentals/contract-development/authorization.mdx#account-abstraction) for details). For the smart contracts it normally doesn't matter which kind of `Address` is used. However, in some contexts it's useful to convert `Address` to/from different data types, such as string or XDR. The conversions have distinctly different purpose depending on whether they happen in the smart contract itself, or in the client code. ## String conversions The default string format for `Address` is a so called 'string key' (or 'strkey'), defined fully in [SEP-23](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md). For the default externally owned accounts strkey always starts with `G` and for all the contracts it starts with `C`. ### String Conversions in Client SDKs Outside of the smart contracts, it is convenient to represent `Address` as string most of the time. It can be stored in string serialization formats such as JSON and XML. Storing addresses as strings in databases can simplify database schema design and queries. Strings are easier to manipulate and are more compatible with user interfaces and APIs. Thus ```js const StellarSdk = require("@stellar/stellar-sdk"); // Example Stellar address const stellarAddress = "GCM5WPR4DDR24FSAX5LIEM4J7AI3KOWJYANSXEPKYXCSZOTAYXE75AFN"; // Create an Address object from string const address = new StellarSdk.Address(stellarAddress); // Convert the address back to string const addressToString = address.toString(); ``` ```python from stellar_sdk import Address # Example Stellar address stellar_address = "GCM5WPR4DDR24FSAX5LIEM4J7AI3KOWJYANSXEPKYXCSZOTAYXE75AFN" # Create an Address object from string address = Address(stellar_address) # Convert the address back to string address_to_string = address.address ``` ### String Conversions in Smart Contracts It's generally preferred for contracts to operate directly on the `Address` type. String conversions may be useful for specialized use cases, such as passing the Stellar `Address`es to/from other chains. ```rust use soroban_sdk::{Address, String, Env}; pub fn address_to_string(address: Address) -> String { address.to_string() } pub fn address_from_string(strkey: String) -> Address { Address::from_string(&strkey) } ``` `Address` can also be built from a string literal, which may be useful for testing. ```rust let test_address = Address::from_str( &env, "GCM5WPR4DDR24FSAX5LIEM4J7AI3KOWJYANSXEPKYXCSZOTAYXE75AFN", ); ``` ## XDR conversions XDR is schema-based binary serialization format used by the Stellar network. It is used for all the Stellar blockchain interactions, such as building the transactions, storing data in the ledger, communicating the transaction results etc. Stellar SDKs provide the typed wrappers for all the Stellar XDR data types. Address is represented as `ScAddress` type, which can then be wrapped into `ScVal` which is a type that represents any contract type supported by Stellar contracts. ### XDR Conversions in Client SDKs On the client side XDR conversions are useful to build the transactions and process the transaction results. ```js // Example Stellar address const stellarAddress = "GCM5WPR4DDR24FSAX5LIEM4J7AI3KOWJYANSXEPKYXCSZOTAYXE75AFN"; // Create an Address object from string const address = new StellarSdk.Address(stellarAddress); // Convert the Address to xdr.ScVal const scVal = address.toScVal(); // Convert scVal structure to the binary format const scValBuffer = scVal.toXDR("raw"); // Convert the Address to xdr.ScAddress const scAddress = address.toScAddress(); ``` ```python from stellar_sdk import Address # Example Stellar address stellar_address = 'GBJCHUKZMTFSLOMNC7P4TS4VJJBTCYL3XKSOLXAUJSD56C4LHND5TWUC' # Create an Address object address = Address(stellar_address) # Convert the Address object to an ScAddress sc_address_xdr = address.to_xdr_sc_address() ``` ### XDR Conversions in Smart Contracts Smart contracts don't need to explicitly interact with the XDR types, as all the smart contract data types are automatically converted to XDR by the smart contract runtime. Every contract type, including `Address`, can be serialized to XDR bytes. This conversion is useful, for example, for performing hashing in smart contracts. It is also possible to convert the serialized XDR bytes back to contract types, which can be useful in some narrow use cases, such as custom authentication schemes. Note, that XDR conversions are an advanced feature and are not necessary for most Stellar smart contracts. ```rust use soroban_sdk::{ xdr::{FromXdr, ToXdr}, Address, Bytes, Env, }; pub fn address_to_xdr_bytes(env: Env, address: Address) -> Bytes { address.to_xdr(&env) } pub fn address_from_xdr_bytes(env: Env, bytes: Bytes) -> Address { Address::from_xdr(&env, &bytes).unwrap() } ``` --- ## Convert from bytes to other types Bytes is a contiguous growable array type containing u8s. They may represent various types of data including strings, addresses, or other information. Converting any data type to bytes ensures that the data be consistently handled by the Soroban runtime and interacting systems. ## Bytes to Address When retrieving data stored on the blockchain, addresses might be stored in byte representation for compactness and efficiency. Off-chain systems, such as APIs, databases, or user interfaces, usually expect addresses in a human-readable format. In such cases, you need to convert the bytes to an address format to ensure compatibility. ```rust use soroban_sdk::{Address, Bytes}; pub fn bytes_to_address(bytes: Bytes) -> Address { Address::from_string_bytes(&bytes) } ``` ```js const StellarSdk = require("@stellar/stellar-sdk"); // Example bytes value const rawBytes = Buffer.from( "99db3e3c18e3ae1640bf56823389f811b53ac9c01b2b91eac5c52cba60c5c9fe", "hex", ); // Convert bytes to an account Address const addressFromBytes = StellarSdk.Address.account(rawBytes); addressFromBytes.toString(); // Convert from bytes to a string address const addressFromBytes = StellarSdk.Address.contract(rawBytes); addressFromBytes.toString(); ``` ```python from stellar_sdk.address import Address # Example bytes value raw_bytes = bytes.fromhex("99db3e3c18e3ae1640bf56823389f811b53ac9c01b2b91eac5c52cba60c5c9fe") bytes_to_address = Address.from_raw_account(raw_bytes) ``` ## Bytes to String When dealing with binary data, you may need to convert certain portions of the data to a human-readable format like strings for logging, debugging, processing or display. ```rust use soroban_sdk::{String, Bytes}; pub fn bytes_to_string(bytes: Bytes) -> String { String::from(bytes) } ``` ```js // Example bytes value const rawBytes = Buffer.from( "99db3e3c18e3ae1640bf56823389f811b53ac9c01b2b91eac5c52cba60c5c9fe", "hex", ); // Convert bytes to string const bytesToString = rawBytes.toString("hex"); ``` ```python # Example bytes value raw_bytes = bytes.fromhex("99db3e3c18e3ae1640bf56823389f811b53ac9c01b2b91eac5c52cba60c5c9fe") # Convert bytes to string bytes_to_string = raw_bytes.hex() ``` ## Bytes to ScVal In a Soroban smart contract that interacts with an external oracle service to provide price data in raw byte format, you would need to convert the bytes to ScVal to process and manipulate the data within your contract. ```rust use soroban_sdk::{Bytes, Val}; pub fn bytes_to_val(bytes: Bytes) -> Val { Val::from(bytes) } ``` ```js // Example bytes value const rawBytes = Buffer.from( "99db3e3c18e3ae1640bf56823389f811b53ac9c01b2b91eac5c52cba60c5c9fe", "hex", ); // Convert bytes to xdr.ScVal const bytesToScVal = StellarSdk.nativeToScVal(rawBytes); ``` ```python # Example bytes value raw_bytes = bytes.fromhex("99db3e3c18e3ae1640bf56823389f811b53ac9c01b2b91eac5c52cba60c5c9fe") # Convert bytes to ScVal sc_val = stellar_sdk.scval.to_bytes(raw_bytes) ``` ## BytesN to Fixed-Size Array `BytesN` is a fixed-length byte buffer used for values like hashes and public keys. It can be converted to a native Rust `[u8; N]` array using the `to_array()` method or the standard `Into` trait. ```rust use soroban_sdk::BytesN; // Convert by value using to_array() pub fn bytesn_to_array(b: BytesN<32>) -> [u8; 32] { b.to_array() } // Convert by value using Into pub fn bytesn_into_array(b: BytesN<32>) -> [u8; 32] { b.into() } // Convert by reference (original BytesN is not consumed) pub fn bytesn_ref_to_array(b: &BytesN<32>) -> [u8; 32] { b.into() } ``` --- ## Convert a ScVal to other type Soroban Contract Value (`ScVal`) is a custom type defined within the Soroban runtime environment that represents other data types such as strings, bytes, and more complex structures used within smart contracts in a format that that the soroban runtime can process, store and retrieve efficiently. ## ScVal to bytes ```js // An ScVal bytes value const myScVal = StellarSdk.xdr.ScVal.fromXDR("AAAADQAAAARQ/8AB", "base64"); // Convert the ScVal to a Buffer const bytesFromScVal = StellarSdk.scValToNative(myScVal); ``` ```python # Convert the ScVal to bytes bytes_from_sc_val = stellar_sdk.scval.from_bytes("AAAADQAAAARQ/8AB") ``` ## ScVal to address ```js // An ScVal Address value const myScVal = StellarSdk.xdr.ScVal.fromXDR( "AAAAEgAAAAAAAAAAmds+PBjjrhZAv1aCM4n4EbU6ycAbK5HqxcUsumDFyf4=", "base64", ); // Convert the ScVal to an Address const addressFromScVal = StellarSdk.Address.fromScVal(myScVal); ``` ```python # Convert the ScVal to address address_from_sc_val = stellar_sdk.scval.from_address("AAAAEgAAAAAAAAAAmds+PBjjrhZAv1aCM4n4EbU6ycAbK5HqxcUsumDFyf4=") ``` ## ScVal to String ```js // An ScVal string value const myScVal = StellarSdk.xdr.ScVal.fromXDR( "AAAADgAAAA9IZWxsbywgU3RlbGxhciEA", "base64", ); // Convert the ScVal to a string const stringFromScVal = StellarSdk.scValToNative(myScVal); ``` ```python # Convert the ScVal to a string string_from_sc_val = stellar_sdk.scval.from_string("AAAADgAAAA9IZWxsbywgU3RlbGxhciEA").decode() ``` --- ## Convert a string to other types Strings are a sequence of characters used to represent readable text. They are used to store and manipulate text-based information such as function names, arguments, key-value data and interfacing with external systems. Strings may often need to be converted to other data types for efficient processing and storage. ## String to bytesN Some systems use binary formats where data needs to be represented as a fixed-length byte array for storage or processing. For example, fixed-length hashes or identifiers. Converting strings to a fixed byte size ensures that the data fits the required size constraints. ```rust use soroban_sdk::{Bytes, String}; pub fn string_to_bytes(string: String) -> Bytes { Bytes::from(string) } ``` ```js // Example string const stringValue = "Hello, Stellar!"; // Convert the string to bytes format const byteValue = Buffer.from(stringValue, "utf-8"); ``` ```python # Example string string_value = "Hello, Stellar!" # Convert the string to bytes format string_value.encode() ``` ## String to address An address received in a user input may be of string type and you would need to convert it to an address type to perform validations, transactions, or other operations within your smart contract. ```rust use soroban_sdk::{Address, Env, String}; pub fn string_to_address(string: String) -> Address { Address::from_string(&string) } ``` ```js const StellarSdk = require("@stellar/stellar-sdk"); // Example Stellar address const stellarAddress = "GCM5WPR4DDR24FSAX5LIEM4J7AI3KOWJYANSXEPKYXCSZOTAYXE75AFN"; // Create an Address object from string const address = new StellarSdk.Address(stellarAddress); // Convert the address back to string const addressToString = address.toString(); ``` ```python from stellar_sdk import Address # Example Stellar address stellar_address = "GCM5WPR4DDR24FSAX5LIEM4J7AI3KOWJYANSXEPKYXCSZOTAYXE75AFN" # Create an Address object from string address = Address(stellar_address) # Convert the address back to string address_to_string = address.address ``` ## String to ScVal When calling functions or methods that expect ScVal types, you need to convert your string data to ScVal to make the call successful. For example, if your smart contract needs to store or manipulate a user input string within its state or use it as part of its logic, you would convert the string to an ScVal type to integrate it with the contract's operations. ```rust use soroban_sdk::{String, Val}; pub fn string_to_val(string: String) -> Val { Val::from(string) } ``` ```js // Example string value const stringValue = "Hello, Stellar!"; // Convert the string to ScVal const stringToScVal = StellarSdk.xdr.ScVal.scvString(stringValue); ``` ```python # Example string value string_value = "Hello, Stellar!" # Convert the string to ScVal string_to_sc_val = stellar_sdk.scval.to_string(string_value) ``` --- ## Dapp Development We've written some helpful guides on some of the most useful tools available to you, the dapp developer. --- ## Comprehensive frontend guide for Stellar dapps ## Pre-requisites: - Basic knowledge of React, Tailwind CSS, and related web technologies - Basic understanding of the Stellar blockchain - Node.js and npm installed - Web browser with [Freighter Wallet](https://www.freighter.app) extension installed ## 1. Introduction ### The role of frontend in Stellar dapps Frontend development plays a crucial role in decentralized applications (dapps) built on the Stellar network. It serves as the primary interface between users and the underlying blockchain technology. A well-designed frontend not only makes your dapp accessible and user-friendly but also helps users interact seamlessly with complex blockchain operations. In Stellar dapps, the frontend is responsible for: 1. Presenting blockchain data in a human-readable format 2. Facilitating user interactions with smart contracts and Stellar operations 3. Managing user accounts and keys securely 4. Providing real-time updates on transaction status and account balances 5. Guiding users through complex processes like multi-signature transactions or claimable balance operations ### Importance of user interface and user experience The importance of a good user interface (UI) and user experience (UX) in Stellar dapps cannot be overstated. Blockchain technology can be intimidating for many users, and a well-designed UI/UX can make the difference between a successful dapp and one that users find frustrating or confusing. Key aspects of UI/UX in Stellar dapps include: 1. Simplicity: Presenting complex blockchain concepts in an easy-to-understand manner 2. Transparency: Providing clear information about transaction fees, network status, and operation outcomes 3. Feedback: Offering immediate and clear feedback on user actions and transaction progress 4. Error Handling: Gracefully managing and explaining errors in a user-friendly way 5. Performance: Ensuring quick load times and responsive interactions, even when dealing with blockchain operations By focusing on these aspects, you can create Stellar dapps that are not only functional but also enjoyable to use, encouraging wider adoption of your application and the Stellar network as a whole. ## 2. Setting up the development environment Before we start building our Stellar dapp, we need to set up our development environment. We'll be using React with Next.js for our frontend framework, Tailwind CSS for styling, and the Stellar SDK for interacting with the Stellar network. ### Installing Node.js and npm First, make sure you have Node.js and npm (Node Package Manager) installed on your system. You can download and install them from the official Node.js website: https://nodejs.org/ To verify your installation, open a terminal and run: ```bash node --version npm --version ``` Both commands should return version numbers if the installation was successful. ### Setting up a Next.js project Next.js is a React framework that provides features such as server-side rendering and routing out of the box. To create a new Next.js project, run the following commands in your terminal: ```bash npx create-next-app@latest stellar-dapp cd stellar-dapp ``` When prompted, choose the following options: - Would you like to use TypeScript? Yes - Would you like to use ESLint? Yes - Would you like to use Tailwind CSS? Yes - Would you like to use `src/` directory? No - Would you like to use App Router? Yes - Would you like to customize the default import alias? No ### Setup HTTPS on Localhost Freighter wallet requires a secure connection (HTTPS) to interact with your dapp. To enable HTTPS on localhost, you can use a tool like `mkcert`. Fortunately, Next.js provides built-in support for HTTPS. To enable HTTPS in your Next.js project, open the `package.json` file and edit the `scripts` section as follows: ```json "scripts": { "dev": "next dev --experimental-https", } ``` ### Installing Stellar SDK and other dependencies To interact with the Stellar network, we'll need to install the Stellar SDK and some additional dependencies: ```bash npm install stellar-sdk @stellar/freighter-api bignumber.js ``` - `stellar-sdk`: The official Stellar SDK for interacting with the Stellar network - `@stellar/freighter-api`: A library for integrating with the Freighter wallet (a popular Stellar wallet browser extension) - `bignumber.js`: A library for arbitrary-precision decimal and non-decimal arithmetic Now that we have our development environment set up, we're ready to start building our Stellar dapp! ## 3. Building basic interface elements In this section, we'll create reusable components for our Stellar dapp and implement forms and inputs using React and Tailwind CSS. ### Creating reusable components Let's start by creating a button component that we'll use throughout our application. Create a new file `components/Button.tsx`: ```typescript interface ButtonProps { onClick?: () => void; children: React.ReactNode; disabled?: boolean; className?: string; } const Button: React.FC = ({ onClick, children, disabled = false, className = "", }) => { return ( ); }; export default Button; ``` This button component uses Tailwind CSS classes for styling and accepts props for customization. Next, let's create another helper button to help connect the Freighter wallet. Create a new file `components/ConnectWalletButton.tsx`: ```typescript "use client"; export interface ConnectButtonProps { label: string; isHigher?: boolean; } export function ConnectButton({ label }: ConnectButtonProps) { return ( ); } ``` This button component uses the `setAllowed` function from the `@stellar/freighter-api` library to connect the Freighter wallet when clicked. ### Implementing forms and inputs Next, let's create a reusable input component. Create a new file `components/Input.tsx`: ```typescript interface InputProps { type: string; placeholder: string; value: string; onChange: (e: React.ChangeEvent) => void; className?: string; } const Input: React.FC = ({ type, placeholder, value, onChange, className = "", }) => { return ( ); }; export default Input; ``` Now, let's create a form component that uses these reusable components. Create a new file `components/SendPaymentForm.tsx`: ```typescript "use client"; interface SendPaymentFormProps { onSubmit: (destination: string, amount: string) => void; } const SendPaymentForm: React.FC = ({ onSubmit }) => { const [destination, setDestination] = useState(""); const [amount, setAmount] = useState(""); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit(destination, amount); }; return (
setDestination(e.target.value)} /> setAmount(e.target.value)} />
); }; export default SendPaymentForm; ``` ### Designing responsive layouts with Tailwind CSS To create a responsive layout for our dapp, we'll use Tailwind CSS utility classes. Let's create a layout component that we can use across our pages. Edit the file `app/layout.tsx`: ```typescript export const metadata: Metadata = { title: "Stellar Payment DApp", description: "Payment DApp built on Stellar", }; interface LayoutProps { children: React.ReactNode; } const Layout: React.FC = ({ children }) => { return (
Stellar Dapp
{children}
); }; export default Layout; ``` Now, let's update our `app/page.tsx` file to use this layout and our new components: ```typescript "use client"; export default function Home() { const handleSendPayment = (destination: string, amount: string) => { // We'll implement this function in the next section console.log(`Sending ${amount} XLM to ${destination}`); }; return ( Send Payment ); } ``` This setup provides a solid foundation for building the user interface of our Stellar dapp. In the next section, we'll integrate these components with the Stellar SDK to perform actual blockchain operations. ## 4. Basic Concepts of Soroban Contracts Before we start integrating smart contract functionality into our Stellar dapp, let's understand the basic concepts of Soroban contracts. Soroban is a smart contract platform built on the Stellar network. It allows developers to write and deploy smart contracts that run on the Stellar blockchain. Soroban contracts are written in Rust and compiled to [XDR (External Data Representation)](../../../learn/fundamentals/data-format/xdr.mdx) for execution on the Stellar network. ### Data Types Stellar supports a few data types that can be used in Soroban contracts and we need to do conversions from time to time between those types and the types we have in Javascript. The full list of primitive data types are explained [here](../../../learn/fundamentals/contract-development/types/built-in-types.mdx). ### XDR Values that are passed to contracts and returned from contracts are serialized using XDR. XDR is a standard data serialization format used in Stellar to represent complex data structures. It is used to encode and decode data for transmission over the Stellar network. XDR is mostly represented in Javascript as string and can be converted to other types using the Stellar SDK. ### Fees Gas fees like they are called in the ethereum network are charged differently here. When submitting different types of transactions, the type of fees paid are different. When submitting a transaction to the network that interacts with the network, a Base fee is paid together with the resource fee for the operation. The base fee is a fixed fee that is paid for every transaction on the network. The resource fee is a fee that is paid for the resources used by the operation and is calculated based on the resources used by the operation and the network's current resource price. Calculating these fees can be cumbersome but the Stellar SDK provides a way to calculate these fees using the `server.prepareTransaction` method which simulates the transaction, gets the appropriate fees and appends the correct fee settings to the transaction. ### ABI or Spec The ABI or spec is a json file that contains the contract's interface. It defines the functions that can be called on the contract, their parameters, and return types. The ABI is used by clients to interact with the contract and execute its functions. The ABI is generated from the contract's source code and is used to compile the contract into XDR for execution on the Stellar network. ABI can be genrated for a contract using the [`stellar contract bindings`](../../../tools/cli/stellar-cli.mdx#stellar-contract-bindings-json) command and can be used to interact with the contract. This ABI can also be generated as a typescript library to ease development in your DApp and we'll be looking it later in this guide ## 5. Integrating with Stellar blockchain Now that we have our basic UI components in place, let's integrate them with the Stellar blockchain using the Stellar SDK. It is imperative to know that the Stellar blockchain has three major networks: - Public network (also called Mainnet): This is the main Stellar network where real transactions take place. - Test network: This is a test environment for developers to test their applications without using real lumens. - Futurenet network: This is a network for testing new features before they are deployed to the public network. For this guide, we'll be using the Test network to avoid using real lumens during development. Read more about the [Stellar networks here](../../../networks/README.mdx). ### Setting up the Stellar SDK Below is a snippet that shows how to set up the Stellar SDK in your project. We will be using parts of it a lot in this guide so lets try to understand it. ```typescript export const server = new StellarRpc.Server("https://soroban-testnet.stellar.org"); // soroban testnet server const transaction = new StellarSdk.TransactionBuilder(account, { fee: StellarSdk.BASE_FEE networkPassphrase: StellarSdk.Networks.TESTNET, // Use appropriate network }) ``` In the above snippet, we import the Stellar SDK and create a server instance for the Stellar Testnet. We also create a transaction builder instance with the appropriate network passphrase. The `BASE_FEE` is the minimum fee required for a transaction on the Stellar network. while the `Networks.TESTNET` is the network passphrase for the Stellar Testnet. When using the public network, you can replace `TESTNET` with `PUBLIC` or `FUTURENET` for the Futurenet network. ### Interacting with the Stellar network If you are coding along this guide at this point, you should have a minimal web application with a form to send payments. Now, let's integrate this form with the Stellar SDK to send actual payments on the Stellar network. Before we proceed, please ensure that you have setup the Freighter wallet extension on your browser. You can download it [here](https://www.freighter.app). Also, ensure that you have lumens on the Testnet account you are using. You can get free lumens from the [Stellar Friendbot](https://laboratory.stellar.org/#account-creator?network=test). Now, let's update our `app/page` component to interact with the Stellar network: ```typescript "use client"; isConnected, setAllowed, getAddress, signTransaction, } from "@stellar/freighter-api"; export default function Home() { const [publicKey, setPublicKey] = useState(null); useEffect(() => { const checkFreighter = async () => { try { const connected = await isConnected(); if (connected) { const pubKey = await getAddress(); setPublicKey(pubKey.address); } } catch (error) { console.error("Error checking Freighter connection:", error); } }; checkFreighter(); }, []); const handleConnectWallet = async () => { try { await setAllowed(); const pubKey = await getAddress(); setPublicKey(pubKey.address); } catch (error) { console.error("Error connecting to Freighter:", error); } }; const handleSendPayment = async (destination: string, amount: string) => { if (!publicKey) { console.error("Wallet not connected"); return; } try { const server = new StellarRpc.Server( "https://soroban-testnet.stellar.org", ); const sourceAccount = await server.getAccount(publicKey); const transaction = new StellarSdk.TransactionBuilder(sourceAccount, { fee: StellarSdk.BASE_FEE, networkPassphrase: StellarSdk.Networks.TESTNET, }) .addOperation( StellarSdk.Operation.payment({ destination: destination, asset: StellarSdk.Asset.native(), amount: amount, }), ) .setTimeout(30) .build(); const signedTransaction = await signTransaction(transaction.toXDR(), { networkPassphrase: StellarSdk.Networks.TESTNET, }); const transactionResult = await server.sendTransaction( StellarSdk.TransactionBuilder.fromXDR( signedTransaction.signedTxXdr, StellarSdk.Networks.TESTNET, ), ); console.log("Transaction successful:", transactionResult); alert("Payment sent successfully!"); } catch (error) { console.error("Error sending payment:", error); alert("Error sending payment. Please check the console for details."); } }; return ( Send Payment {publicKey ? ( <> Connected: {publicKey} ) : ( )} ); } ``` In the updated `app/page` component, we added a `useEffect` hook to check if the Freighter wallet is connected and retrieve the public key. We also added a `handleConnectWallet` function to connect the wallet if it's not already connected. The `handleSendPayment` function now interacts with the Stellar network to send a payment. It retrieves the source account details, creates a payment transaction, signs the transaction using the Freighter wallet, and sends the transaction to the Stellar network. If the transaction is successful, it displays an alert to the user. Notice how we imported the `isConnected`, `setAllowed`, `getAddress`, and `signTransaction` functions from the `@stellar/freighter-api` library. These functions are used to interact with the Freighter wallet extension and sign transactions securely. :::tip Hurray! You have successfully integrated your Stellar dapp with the Stellar network. You can now send payments using the Freighter wallet extension on the Stellar Testnet. ::: ### Interacting with smart contracts In the above example, we sent a simple payment transaction using the `StellarSdk.Operation.payment` operation. However, Stellar also supports many other operations, one of which is `invokeHostFunction` operation which is used to interact with smart contracts on the Stellar network. We will be working with a deployed version of the [counter smart contract](https://github.com/stellar/soroban-examples/tree/main/events) on the Stellar Testnet. The smart contract has a single function `increment` which increments a counter value stored on the Stellar network. Create a new file `app/counter/page.tsx`: ```typescript "use client"; BASE_FEE, Contract, Networks, rpc as StellarRpc, Transaction, TransactionBuilder, xdr, } from "@stellar/stellar-sdk"; getAddress, isConnected, signTransaction, } from "@stellar/freighter-api"; // Replace with your actual contract ID and network details const CONTRACT_ID = "CBWNXQRGC7WFYGDXUANDAZSRF2E5NPPA3NP6UZSPHYBVU3K46PSNWQOO"; const NETWORK_PASSPHRASE = Networks.TESTNET; const SOROBAN_URL = "https://soroban-testnet.stellar.org:443"; export default function CounterPage() { const [publicKey, setPublicKey] = useState(null); const [count, setCount] = useState(null); const [loading, setLoading] = useState(false); const server = new StellarRpc.Server(SOROBAN_URL); useEffect(() => { const checkWallet = async () => { const connected = await isConnected(); if (connected) { const pubKey = await getAddress(); setPublicKey(pubKey.address); } }; checkWallet(); }, []); const handleIncrement = async () => { if (!publicKey) { console.error("Wallet not connected"); return; } setLoading(true); try { const account = await server.getAccount(publicKey); const contract = new Contract(CONTRACT_ID); // const instance = contract.getFootprint(); const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: NETWORK_PASSPHRASE, }) .addOperation(contract.call("increment")) .setTimeout(30) .build(); const preparedTx = await server.prepareTransaction(tx); const signedXdr = await signTransaction( preparedTx.toEnvelope().toXDR("base64"), { networkPassphrase: NETWORK_PASSPHRASE, }, ); const signedTx = TransactionBuilder.fromXDR( signedXdr.signedTxXdr, NETWORK_PASSPHRASE, ) as Transaction; const txResult = await server.sendTransaction(signedTx); if (txResult.status !== "PENDING") { throw new Error("Something went Wrong"); } const hash = txResult.hash; let getResponse = await server.getTransaction(hash); // Poll `getTransaction` until the status is not "NOT_FOUND" while (getResponse.status === "NOT_FOUND") { console.log("Waiting for transaction confirmation..."); getResponse = await server.getTransaction(hash); await new Promise((resolve) => setTimeout(resolve, 1000)); } if (getResponse.status === "SUCCESS") { // Make sure the transaction's resultMetaXDR is not empty if (!getResponse.resultMetaXdr) { throw "Empty resultMetaXDR in getTransaction response"; } } else { throw `Transaction failed: ${getResponse.resultXdr}`; } // Extract the new count from the transaction result const returnValue = getResponse.resultMetaXdr .v4() .sorobanMeta() ?.returnValue(); if (returnValue) { const newCount = returnValue.u32(); setCount(newCount); } } catch (error) { console.error("Error incrementing counter:", error); alert( "Error incrementing counter. Please check the console for details.", ); } finally { setLoading(false); } }; return ( Stellar Smart Contract Counter {publicKey ? ( Connected: {publicKey} Current Count: {count === null ? "Unknown" : count} ) : ( <> Please connect your Freighter wallet to use this app. )} ); } ``` In the above code snippet, we created a new page component `CounterPage` that interacts with the counter smart contract on the Stellar network. The `handleIncrement` function sends a transaction to the smart contract to increment the counter value. It then retrieves the updated counter value from the transaction result and displays it to the user. Notice how we used a different operation `contract.call("increment")` to interact with the smart contract. This operation is a facade for the `invokeHostFunction` operation on the Stellar network. We also used the `prepareTransaction` and `sendTransaction` methods to send the transaction to the network and retrieve the transaction result. `prepareTransaction` call is used to prepare a transaction for submission to the network. It simulates the transaction, then uses the appropriate resource fees and other params to create a transaction that is ready to be submitted to the network. `` is a helper button that connects the Freighter wallet when clicked like we described earlier. After calling the transaction to increment successfully, we need to poll and then call another method `server.getTransaction` to get the transaction result. The transaction result contains the new counter value which we extract and display to the user. The value gotten from the transaction result is stored in a form called `scval` due to the limited [number of types](../../../learn/fundamentals/contract-development/types/built-in-types.mdx) that exist in soroban today. This value is transmitted about in the form of `xdr` which can be converted to an `scVal` using the sdk. The value is then parsed as `u32` which is a 32-bit unsigned integer. We will learn more about converting these types [in this collection of guides](../conversions/README.mdx). :::tip Congratulations! You have successfully interacted with a smart contract on the Stellar network using your dapp. ::: ### Reading events from the Stellar network Events on Stellar are values emitted from contracts grouped by topics. These events are emitted when a contract is called and can be read by any client that is interested in the contract. Events are stored in the Stellar ledger and can be read by any client that is interested in the contract. In addition to sending transactions and interacting with smart contracts, you can also read events from the Stellar network using the Stellar SDK. This allows you to monitor account changes, transaction status, and other network events in real-time. This is made possible by using the `server.getEvents` method which allows you to query events based on topics. We will consider a simple example where we read events from the counter smart contract we interacted with earlier. We will be editing the `CounterPage` component to read events from the counter smart contract immediately the page loads to get the initial counter value and update instead of using "Unknown". Before we continue, please take a look at the [contract code](https://github.com/stellar/soroban-examples/blob/main/events/src/lib.rs). In the contract code, an event named `increment` is emitted whenever the `increment` function is called. It is published over 2 topics, `increment` and `COUNTER` and we need to listen to these topics to get the events. The topics are stored in a data type called `symbol` and we will need to convert both `increment` and `COUNTER` to `symbol` before we can use them in the [`server.getEvents`](../../../data/apis/rpc/api-reference/methods/getEvents.mdx) method. At maximum, stellar RPCs keep track of events for 7 days and you can query events that happened within the last 7 days, so if you need to store events for longer, you may need to make use of an [indexer](../../../data/indexers/README.mdx). To use events,we edit our counter page and add the following code: ```typescript useEffect(() => { const checkWallet = async () => { const connected = await isConnected(); if (connected) { const pubKey = await getAddress(); setPublicKey(pubKey.address); } }; checkWallet(); getInitialCount(); }, []); const getInitialCount = async () => { try { const topic1 = xdr.ScVal.scvSymbol("COUNTER").toXDR("base64"); const topic2 = xdr.ScVal.scvSymbol("increment").toXDR("base64"); const latestLedger = await server.getLatestLedger(); const events = await server.getEvents({ startLedger: latestLedger.sequence - 2000, filters: [ { type: "contract", contractIds: [CONTRACT_ID], topics: [[topic1, topic2]], }, ], limit: 20, }); setCount(events.events.map((e) => e.value.u32()).pop() || null); } catch (error) { console.error(error); } }; ``` In the above code, we use an RPC method called `getEvents` to query events from the Stellar network. We pass in the contract ID, topics, and other filters like the ledger to start searching from to 2000 ledgers away to get the events we are interested in. We then extract the counter value from the events and update the state accordingly. However, we had to convert the topics `COUNTER` and `increment` to `symbol` using the `xdr.ScVal.scvSymbol` and then convert them to XDR before we can use them in the `getEvents` method. This is because the topics are stored in the form of `symbol` and transmitted over XDR format to the network like we discussed earlier. Similarly, the value of the new count is sent over the network as XDR but the SDK helped us convert it to an `scVal` and then we parsed it as a `u32` to get the actual value. Source code for this program is available [here](https://github.com/myestery/stellar-dapp) Now, when the page loads, it will query the events from the Stellar network and update the counter value accordingly. ## 6. Using Typescript Bindings The Stellar CLI comes with support for exporting a contract spec to a typescript library. This library is optimized for importing as an npm based module to your application. The library generated contains helper methods for calling each contract and also does automatic type conversion of the types for any contract method. ### How to create the bindings library To achieve this, you need to - Have the [Stellar CLI](../../../tools/cli/install-cli.mdx) installed. - Have either source code or deployed contract ID of the contract. - Know the network it was deployed to. #### Scenario 1: I have the Contract ID but no code In this scenario, we need to use the command [`stellar contract fetch`](../../../tools/cli/stellar-cli.mdx#stellar-contract-fetch) to fetch the wasm code of the contract #### Scenario 2: I have the code The next step here is to build it to a wasm file using the [`stellar contract build`](../../../tools/cli/stellar-cli.mdx#stellar-contract-build) command. ### Generating the Library After getting the wasm, we can now run [`stellar contract bindings typescript`](../../../tools/cli/stellar-cli.mdx#stellar-contract-bindings-typescript) to generate the library which is ready to be published to NPM. The library generated is suited for working with complex contracts. ## 7. Common Pitfalls Below are a few common pitfalls that soroban DApp developers might run into ### Data Type Conversions The data types in Javascript are different from the data types in soroban. When working with soroban contracts, you need to be aware of the data types and how to convert them to the types you are familiar with in Javascript. The Stellar SDK provides helper methods for converting between XDR and Javascript types in the `xdr` namespace. ### Fees Calculating fees for transactions can be tricky. The Stellar SDK provides a way to calculate fees using the `server.prepareTransaction` method, which simulates the transaction and returns the appropriate fees. Fees are calculated in [stroops](../../../learn/glossary.mdx#stroop) ### SendTransaction and GetTransaction Results from a smart contract execution or any of the [valid transactions](../../../learn/fundamentals/transactions/list-of-operations.mdx#extend-footprint-ttl) on soroban are not immediate. They are kept in a `PENDING` state until they are confirmed. You need to poll the `getTransaction` method to get the final result of the transaction. ### State Archival State Archival is a characteristic of soroban contracts where some data stored on the ledger about the contract might be archived. These [guides](../archival/README.mdx) helps to understand how to work with state archival in DApps. ### Data Retention Data can only be queried within the configured retention window of the RPC instance you are using (the default is 7 days). So you may need an indexer to store transaction or event data for longer periods. --- ## Initialize a dapp using scripts When setting up an example Soroban Dapp, correct initialization is crucial. This process entails several steps, including deploying Docker, cloning and deploying smart contracts, and invoking functions to configure them. In this comprehensive guide, you will walk you through the necessary steps to successfully build and deploy these smart contracts, ensuring a seamless setup for your Soroban Dapp. ## Building the Soroban Token Smart Contract In dapps like the [Example Payment Dapp](https://github.com/stellar/soroban-react-payment), the [Soroban Token smart contracts](https://github.com/stellar/soroban-examples/tree/main/token) are used to represent the tokenized asset that users can send and receive. Here is an example of how to build and deploy the Soroban Token smart contracts: Start by cloning the Soroban examples repository: ```shell git clone https://github.com/stellar/soroban-examples.git ``` Then, navigate to the `token` directory: ```shell cd soroban-examples/token ``` At this point you can build the smart contract: ```shell make ``` This action will compile the smart contracts and place them in the `token/target/wasm32v1-none/release` directory. After building, you're ready to deploy the smart contracts to Testnet. ## Deploying and Initializing the Soroban Token Smart Contract The smart contract can be initialized at deploy-time by utilizing the `__constructor` function. This function will be called at deploy-time and can be declared with parameters you can pass to the function with the deploy command. The `__constructor` function is added to the contract entry point, in this `token` smart contract it is found in the [contract.rs](https://github.com/stellar/soroban-examples/blob/main/token/src/contract.rs) file. ```rust title="contract.rs" #[contractimpl] impl Token { pub fn __constructor(e: Env, admin: Address, decimal: u32, name: String, symbol: String) { if decimal > 18 { panic!("Decimal must not be greater than 18"); } write_administrator(&e, &admin); write_metadata( &e, TokenMetadata { decimal, name, symbol, }, ) } ... } ``` The `__constructor` function in this example project takes the following parameters: - Administrator Account: This is the public key of the administrator account. The administrator has control and authority over the token contract, enabling management of various contract functionalities. Learn more about the administrator's role from the Soroban Token Interface. - Decimal Precision: The decimal precision specifies the maximum number of decimal places the token can support transactions up to. This precision level enables flexibility when transferring token amounts. - Token Name: The token's name. - Token Symbol: This is the token's symbol. To deploy the smart contract in this example, open a terminal in the `soroban-examples/token` directory and execute the following: ```shell stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_token_contract.wasm \ --source-account alice \ --rpc-url https://soroban-testnet.stellar.org \ --network-passphrase 'Test SDF Network ; September 2015' -- \ --admin alice \ --decimal 10 \ --name 'Demo Token' \ --symbol 'DT' ``` This command deploys the smart contracts to Testnet using the `stellar contract deploy` function. The smart contract will be deployed with the mandatory parameters, and the smart contract will be initialized with these values. ## Minting Tokens Lastly, you need to mint some tokens to the sender's account: ```shell stellar contract invoke \ --id \ --source-account alice \ --rpc-url https://soroban-testnet.stellar.org \ --network-passphrase 'Test SDF Network ; September 2015' -- mint \ --to bob \ --amount 1000000000 ``` This command will mint 100 tokens to the designated user's account, in this case `bob`. By following these steps, you ensure that the Soroban token smart contracts are correctly deployed and initialized, setting the stage for the Dapp to effectively interact with the token. For a deeper dive into Stellar CLI commands, check out the [Stellar CLI repo](https://github.com/stellar/stellar-cli/blob/main/FULL_HELP_DOCS.md). ## Automating Initialization with Scripts To streamline the initialization process, you can use a script. This script should automate various tasks such as setting up the network, generating token-admin identities, funding the token-admin account, building and deploying the contracts, and initializing them with necessary parameters. Here's an example initializer script: ```bash title="initialize.sh" #!/bin/bash set -e NETWORK="$1" # If stellar-cli is called inside the soroban-preview docker container, # it can call the stellar standalone container just using its name "stellar" if [[ "$IS_USING_DOCKER" == "true" ]]; then SOROBAN_RPC_HOST="http://stellar:8000" else SOROBAN_RPC_HOST="http://localhost:8000" fi case "$1" in standalone) echo "Using standalone network" SOROBAN_NETWORK_PASSPHRASE="Standalone Network ; February 2017" FRIENDBOT_URL="$SOROBAN_RPC_HOST/friendbot" SOROBAN_RPC_URL="$SOROBAN_RPC_HOST/soroban/rpc" ;; futurenet) echo "Using Futurenet network" SOROBAN_NETWORK_PASSPHRASE="Test SDF Future Network ; October 2022" FRIENDBOT_URL="https://friendbot-futurenet.stellar.org/" SOROBAN_RPC_URL="https://rpc-futurenet.stellar.org" ;; testnet) echo "Using Testnet network" SOROBAN_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" FRIENDBOT_URL="https://friendbot.stellar.org/" SOROBAN_RPC_URL="https://soroban-testnet.stellar.org" ;; *) echo "Usage: $0 standalone|futurenet|testnet" exit 1 ;; esac echo Add the $NETWORK network to cli client stellar network add \ --rpc-url "$SOROBAN_RPC_URL" \ --network-passphrase "$SOROBAN_NETWORK_PASSPHRASE" "$NETWORK" if !(stellar keys ls | grep token-admin 2>&1 >/dev/null); then echo Create the token-admin identity stellar keys generate token-admin fi TOKEN_ADMIN_SECRET="$(stellar keys show token-admin)" TOKEN_ADMIN_ADDRESS="$(stellar keys address token-admin)" echo "$TOKEN_ADMIN_SECRET" > .soroban-example-dapp/token_admin_secret echo "$TOKEN_ADMIN_ADDRESS" > .soroban-example-dapp/token_admin_address # This will fail if the account already exists, but it'll still be fine. echo Fund token-admin account from friendbot curl --silent -X POST "$FRIENDBOT_URL?addr=$TOKEN_ADMIN_ADDRESS" >/dev/null echo Build the contract make build echo Deploy the contract ARGS="--network $NETWORK --source-account token-admin -- --admin $2 --decimal $3 --name $4 --symbol $5" CROWDFUND_ID="$( stellar contract deploy $ARGS \ --wasm target/wasm32v1-none/release/soroban_token_contract.wasm )" echo "Contract deployed successfully with ID: $CROWDFUND_ID" echo "$CROWDFUND_ID" > .soroban-example-dapp/crowdfund_id echo "Done" ``` Here's a summary of what the `initialize.sh` script does: - Identifies the network (standalone or futurenet) based on user input - Determines the Stellar RPC host URL depending on its execution environment (either inside the soroban-preview Docker container or locally) - Sets the Stellar RPC URL based on the previously determined host URL - Sets the Soroban network passphrase and Friendbot URL depending on the chosen network - Adds the network configuration using `stellar network add` - Generates a token-admin identity using `stellar keys generate` - Fetches the TOKEN_ADMIN_SECRET and TOKEN_ADMIN_ADDRESS from the newly generated identity - Saves the TOKEN_ADMIN_SECRET and TOKEN_ADMIN_ADDRESS in the .soroban directory - Funds the token-admin account using Friendbot - Builds the contract with `make build` and deploys it using `stellar contract deploy`, storing the returned CROWDFUND_ID - Prints "Done" to signify the end of the initialization process By leveraging automated initialization, you can streamline the setup process for your Soroban Dapp, ensuring it is correctly deployed and initialized. --- ## Develop contract with frontend templates Develop contract with frontend templates This guide picks up where [Build a Dapp Frontend](../../apps/dapp-frontend.mdx) left off. From there, we'll: 1. Search GitHub for other Soroban templates 1. Build our own simple template Building our own template will be a great way to learn how they work. They're not that complicated! ## Search GitHub for other Soroban templates The official template maintained by Stellar Development Foundation (SDF), as used in [Build a Dapp Frontend](../../apps/dapp-frontend.mdx), lives on GitHub at [stellar/soroban-template-astro](https://github.com/stellar/soroban-astro-template). It uses the [Astro](https://astro.build) web framework. While Astro works with React, Vue, Svelte, and any other UI library, the template opts not to use them, preferring Astro's own templating language, which uses vanilla JavaScript with no UI library. (You may wonder why it makes this unpopular choice. A fair question! The team wanted to balance actual utility with broad approachability. Not everyone learning Stellar and Soroban is familiar with React, or any other UI library. It also demonstrates that core Soroban libraries all work with any JavaScript project.) To use other templates, we will clone them from their repositories, and then copy these files into the root of the existing `soroban-hello-world` directory: ```bash # For example, you could clone this repository git clone https://github.com/stellar/soroban-examples ``` Now copy files into the root of `soroban-hello-world`. So how can you find other valid frontend templates? At some point we may set up a DAO or other voting system to curate a list of high-quality templates, but for now... search GitHub! And GitLab. And any other source code site you know about. In GitHub, in the main search bar, search for `"soroban-template-"`. With the quotes. Here's a direct link to the search results: [github.com/search?q=%22soroban-template-%22](https://github.com/search?q=%22soroban-template-%22) You can copy this approach for any other source code website, such as GitLab. How do you know if any of these are any good? Try them. Look at their source code. How many stars do they have? How active are their maintainers? None of these are perfect metrics, which is why a curated registry might be nice in the future. If none of them suit, then it might be time to... ## Make your own template This will be easier than you might imagine. There are a few gotchas, but overall these templates are fairly standard Node projects. ### But first, how are these projects organized? When you run `stellar contract init`, it _always_ includes a Rust/Cargo project, with a `Cargo.toml` that defines a workspace, and workspace members located in a `contracts` folder. At a minimum, there's one contract, `contracts/hello_world`, but you can get more by using `--name`. So keep that in mind. We're making a fairly simple Node project, and telling it about our Soroban stuff, like the `contracts` folder. ### 1. Initialize an NPM project So let's make a simple Node project! Rather than initializing an Astro project, like the official frontend template, let's try something new. The [State of JS survey](https://2022.stateofjs.com/en-US/libraries/front-end-frameworks/#front_end_frameworks_experience_linechart) tells me that something called "Solid" is fairly new and well-loved. [What does it say to do?](https://www.solidjs.com) ```bash npx degit solidjs/templates/ts soroban-template-solid cd soroban-template-solid npm install npm run dev ``` Ok, we have a running Solid template! Now let's turn it into a Soroban template! ### 2. git init Commit early, commit often! You can stop running the dev server (the one you started with `npm run dev` above; stop it with ctrlc, even on a Mac) if you want, or open a new terminal and: ```bash git init git add . git commit -m "init from solid ts template" ``` ### 3. Copy in the `initialize.js` script The SDF-maintained `soroban-template-astro` has [an `initialize.js` script](https://github.com/stellar/soroban-astro-template/blob/main/initialize.js) in its project root. Copy-paste it into your project. If you're familiar with Node scripting, you should be able to figure out what it's doing. If not, it's a great way to learn! Tip: start at the bottom, where it does the `generateAccount(); buildAll(); deployAll();` stuff. That stuff at the end should give you a clue about what this script does. It does stuff that a _Soroban_ app needs. Soroban apps make calls to Soroban. So this script: 1. generates a Soroban/Stellar account 2. builds all contracts 3. deploys all contracts (to a locally-running Soroban network; we'll recap this soon) 4. _binds_ contracts — that is, it creates NPM projects for each deployed contract by running `stellar contract bindings typescript` 5. imports the contracts for straightforward use in the rest of your project Remember that it needs to do this all _in a contract-agnostic way_. The frontend template doesn't know what contracts it might find! But it needs to build, deploy, bind, and import whatever's there. So this script does that. But it needs a few other things. #### A. `devDependencies` The script uses a couple NPM packages. Install them: ```bash npm install --save-dev dotenv glob ``` #### B. `.env` The [`dotenv` package](https://www.npmjs.com/package/dotenv) installed above parses environment variables from a `.env` file. The official template includes [a `.env.example`](https://github.com/stellar/soroban-astro-template/blob/main/.env.example). Go ahead and copy it into your own project. To set you up for testing it all out, you can also copy it to a local `.env`. ```bash cp .env.example .env ``` #### C. `.gitignore` You'll want to ignore the `.env` file you created above, as well as some other build artifacts created by the `initialize.js` script. Paste the bottom of [the official template's `.gitignore`](https://github.com/stellar/soroban-astro-template/blob/main/.gitignore) into your template's `.gitignore`: ```gitignore # environment variables .env .env.production # generated contract clients packages/* # if you have other workspace packages, add them here !packages/.gitkeep # generated contract client imports src/contracts/* !src/contracts/util.ts ``` #### C. `package.json` Make sure users of your template don't forget to run the `initialize.js` script. Modify the `scripts` section as follows: ```diff - "start": "vite", - "dev": "vite", - "build": "vite build", - "serve": "vite preview" + "init": "node initialize.js", + "start": "npm run init && vite", + "dev": "npm run init && vite", + "build": "npm run init && vite build", + "serve": "npm run init && vite preview" ``` Alternatively, rather than adding the repetitive `npm run init` statements, you could opt to add [a `postinstall` script](https://docs.npmjs.com/cli/v7/using-npm/scripts#life-cycle-operation-order) where you `npm run init`, or just `node initialize.js` in there. You also need to tell NPM to treat this as a workspace. Yes, this project is a Cargo workspace _and_ an NPM workspace! The `bindAll` step of `initialize.js` puts the generated NPM packages in `packages/*`. At the bottom of `package.json`, add: ```diff "dependencies": { "solid-js": "^1.8.11" - } + }, + "workspaces": [ + "packages/*" + ] } ``` Also make sure that this project is set up to allow the `import` statements in the `initialize.js` script. Add this at the top-level: ```diff "description": "", + "type": "module", "scripts": { ``` While you're here, you can also update the `name` (maybe `"soroban-template-solid"`) and `description`. #### E. `src/contracts/util.ts` In the `.gitignore` above, you may have noticed that this file is explicitly included. Copy it in from [the official template](https://github.com/stellar/soroban-astro-template/blob/main/src/contracts/util.ts). You'll need to create the `src/contracts` folder. As you can see, it just makes it easy to `import { rpcUrl, networkPassphrase } from 'util'` in the files generated in the `importAll` step. #### F. README You might want to start by copying [the official template's README](https://github.com/stellar/soroban-astro-template/blob/main/README.md) to explain how to `cp .env.example .env`. From there, this might also be a good time to explain anything else that makes your template unique! Why might people want to use it? ### 4. `git commit` Commit changes! ```bash git add . git commit -m "add initialize.js script and supporting changes" ``` ### 5. Try it! That's it! You have an NPM project with an `initialize.js` script (and its supporting cast). That's all you really need! Let's try it. First, make sure you're running a local Stellar network. Start Docker Desktop or whatever alternative you prefer, then: ```bash stellar container start local ``` Let's include one extra contract. Go to [the increment example](https://github.com/stellar/soroban-examples/tree/main/increment) and copy increment files into the root directory. Then we'll run the `dev` script (which, remember, will run the `initialize.js` script firs). ```bash npm run dev ``` After you do this, you may notice lots of new changes in the project. All that Rust, Cargo, and Soroban stuff! ```bash $ git status --short ?? .soroban/ ?? Cargo.lock ?? Cargo.toml ?? contracts/ ?? target/ ``` Q: Should you commit this stuff?A: **_NO!_** Q: Should you _gitignore_ this stuff?A: **_NO!_** The `.soroban` and `target` folders [are gitignored by the core Rust/Cargo template](https://github.com/stellar/stellar-cli/tree/main/cmd/soroban-cli/src/utils/contract-template). In projects that specify yours as their `--frontend-template`, that `.gitignore` will be merged with yours. No need to include those lines twice. And projects that use yours will want to commit the `Cargo.*` files and `contracts`, but your _template_ should not include them. That can cause problems with project initialization later. Nor should it gitignore them, because your `.gitignore` will be part of projects started with your template! It would cause problems and confusion, if people didn't realize that some of their most important files were gitignored! Alright. Go ahead and open the app in your browser. Solid runs its dev server on `http://localhost:3000`. Does it load? It should! We didn't actually use the new `src/contracts/*` stuff in the app itself! ### 6. Wait. Should the template actually use the contracts? Good question! You know that the `hello_world` contract will always be there. So you could demonstrate to your users how to import and use it in an app file (like `src/App.tsx`, in the Solid template). But remember: no other contracts are guaranteed to be there! So don't commit any changes that import and use something like `increment`! Here's a way you could demonstrate how to import and use the `hello_world` contract in the Solid template's `App.tsx`: ```diff +import greeter from "./contracts/hello_world"; const App: Component = () => { return (
+
{ + e.preventDefault(); + const { result } = await greeter.hello({ + to: e.currentTarget.toWhom.value, + }); + alert(result); + }} + > + + +
``` ### 7. Make it as complex as you want! This is _your_ template. Go ahead and add helpful dependencies and utility files that you always want, like wallet management or whatever. If you want to add a whole complex stack of UI components, styles, and state-management libraries, go ahead! The official template strives for simplicity. For shallowness, even. It's meant as a teaching tool, for people with all different levels of JS and Soroban experience. Yours has no such constraints! It should be useful to _you_. And templates are cheap to make! If you want to have one `soroban-template-framework-x-basic` for a broader audience, and another `soroban-template-framework-x-opinionated` for your own projects, you can do that! ## Wrapping up Some things we did in this section: - Searched GitHub for new frontend templates - Learned that some templates might not be on GitHub - Saw how frontend templates work and interact with the "backend" template that always comes with `stellar contract init` - Built our own template by adding an `initialize.js` script to a basic NPM template After you're done building your own frontend template, what's next? You choose! You can: - See more complex example contracts in the [Example Contracts](../../smart-contracts/example-contracts/README.mdx) section. - Learn more about the [internal architecture and design](../../../learn/fundamentals/contract-development/README.mdx) of Soroban. --- ## Implement state archival in dapps When developing decentralized applications on Stellar, state archival is part of what we need to consider due to how data is stored on the network. This guide will help you understand how to work with state archival in your dapp. Some state archival terminology we will be using in this guide are described in the [state archival section](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#terms-and-semantics). ## Why managing state archival is important for applications Managing state archival is crucial for Stellar dapps for several reasons: - Data accessibility: Archived data becomes inaccessible, potentially breaking application functionality, so it's important to manage data lifecycle and know when to restore. - Cost efficiency: Different storage types have varying fees and archival behaviors, allowing developers to optimize costs. Due to this, some data may be more cost-effective to store in a way that causes it to be archived after a certain period. - Data lifecycle management: Proper management ensures that important data remains accessible while allowing temporary data to expire. - Application continuity: Ensuring contract instances and Wasm code remain live is essential for uninterrupted dapp operation. It is essential to check for contract availability before attempting to interact with the contract after a long period of inactivity. ## Methods of implementing state archival on the client side ### 1. Extending TTL from the smart contract This method involves invoking the [`extend_ttl()` method](https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Instance.html#method.extend_ttl) from your smart contract to extend the TTL of the contract instance and its associated data. This method is useful when you want to keep the data accessible for a longer period. To use this method, your contract must not be archived at the time of calling. `extend_ttl()` has two important parameters (T,N): - T is threshold, the current ledger height at which the extension should happen. - N is the new ledger height at which the data will expire. - Current TTL must be less than T for the extension to happen. - If N is less than the current ledger height, the TTL will not be extended and the call will be regarded as a no-op. - If N is greater than the current ledger height, the TTL will be extended to N. Let's see a sample of how we can implement this in a smart contract: ```rust #![no_std] /// This is a simple contract that just extends TTL for its keys. /// It's main purpose is to demonstrate how TTL extension can be tested, use soroban_sdk::{contract, contractimpl, contracttype, Env}; #[contracttype] pub enum DataKey { MyKey, } #[contract] pub struct TtlContract; #[contractimpl] impl TtlContract { /// Creates a contract entry in every kind of storage. pub fn setup(env: Env) { env.storage().persistent().set(&DataKey::MyKey, &0); env.storage().instance().set(&DataKey::MyKey, &1); env.storage().temporary().set(&DataKey::MyKey, &2); } /// Extend the persistent entry TTL to 5000 ledgers, when its /// TTL is smaller than 1000 ledgers. pub fn extend_persistent(env: Env) { env.storage() .persistent() .extend_ttl(&DataKey::MyKey, 1000, 5000); } /// Extend the instance entry TTL to become at least 10000 ledgers, /// when its TTL is smaller than 2000 ledgers. pub fn extend_instance(env: Env) { env.storage().instance().extend_ttl(2000, 10000); } /// Extend the temporary entry TTL to become at least 7000 ledgers, /// when its TTL is smaller than 3000 ledgers. pub fn extend_temporary(env: Env) { env.storage() .temporary() .extend_ttl(&DataKey::MyKey, 3000, 7000); } } mod test; ``` :::info The above contract shows how to extend the TTL of a `Persistent`, `Instance`, and `Temporary` data entry. The `extend_ttl()` method is used to extend the TTL of the data entry to a new ledger height. ::: #### Using the Extend TTL method in your Contract When we create DApps, we do not typically create buttons to extend the TTL of data entries. Instead, we can create a function that extends the TTL of the data entries when the DApp is used. For example, we can increase the TTL of a temporary data entry called `highestBid` in a bidding DApp when a new bid is placed. This will ensure that the bid data remains accessible for as long as it is needed. ```rust #![no_std] use soroban_sdk::{contract, contractimpl, contracttype, Env}; #[contracttype] pub enum DataKey { HighestBid, } #[contract] pub struct BiddingContract; #[contractimpl] impl BiddingContract { /// Creates a contract entry in every kind of storage. pub fn setup(env: Env) { env.storage().temporary().set(&DataKey::HighestBid, &0); } /// Place a bid and extend the TTL of the highest bid data entry. pub fn place_bid(env: Env, bid: u64) { let highest_bid: u64 = env.storage().temporary().get(&DataKey::HighestBid).unwrap_or(0); if bid > highest_bid { env.storage().temporary().set(&DataKey::HighestBid, &bid); env.storage().temporary().extend_ttl(&DataKey::HighestBid, 1000, 5000); } } } ``` ### 2. Restoring archived data When developing a dapp on Stellar, you may encounter situations where contract data or the contract instance has been archived due to inactivity. Let's walk through the process of restoring archived data using the JavaScript SDK and Freighter wallet. #### Prerequisites - Stellar SDK: `npm install @stellar/stellar-sdk` - Freighter API: `npm install @stellar/freighter-api` - A Stellar RPC endpoint (e.g., `https://soroban-testnet.stellar.org`) #### Step 1: Set up the SDK and Freighter First, import the necessary components: ```javascript isConnected, setAllowed, getAddress, signTransaction, } from "@stellar/freighter-api"; const rpcUrl = "https://soroban-testnet.stellar.org"; const server = new StellarSdk.rpc.Server(rpcUrl); const networkPassphrase = StellarSdk.Networks.TESTNET; // Use PUBLIC for production ``` This setup provides the foundation for interacting with the Stellar network and Freighter wallet. #### Step 2: Create a helper function for restoration Let's create a helper function that attempts to submit a transaction, and if it fails due to archived data, it will restore the data and retry: ```javascript async function submitOrRestoreAndRetry(contractId, method, ...args) { try { let hasFreighter = await isConnected(); if (!hasFreighter) { return alert("Freighter wallet is required for transactions"); } const isAllowed = await setAllowed(); if (!isAllowed) { return alert("Please allow the transaction in Freighter wallet"); } const accountId = await getAddress(); const contract = new StellarSdk.Contract(contractId); const account = await server.getAccount(accountId.address); const fee = StellarSdk.BASE_FEE; const transaction = new StellarSdk.TransactionBuilder(account, { fee, networkPassphrase, }) .addOperation(contract.call(method, ...args)) .setTimeout(30) .build(); let preparedTransaction = await server.prepareTransaction(transaction); let signedXDR = await signTransaction(preparedTransaction.toXDR(), { networkPassphrase, }); let signedTransaction = StellarSdk.TransactionBuilder.fromXDR( signedXDR.signedTxXdr, networkPassphrase, ); // Try to send the transaction const sim = await server.simulateTransaction(signedTransaction); // Other failures are out of scope of this tutorial. if (!Api.isSimulationSuccess(sim)) { throw sim; } // If simulation didn't fail, we don't need to restore anything! Just send it. if (Api.isSimulationRestore(sim)) { console.log("Data archived. Attempting restoration..."); // Prepare restoration transaction const restoreTx = new StellarSdk.TransactionBuilder(account, { fee }) .setNetworkPassphrase(networkPassphrase) .addOperation(StellarSdk.Operation.restoreFootprint({})) .setTimeout(30) .build(); let preparedRestoreTx = await server.prepareTransaction(restoreTx); let signedRestoreXDR = await signTransaction(preparedRestoreTx.toXDR()); let signedRestoreTx = StellarSdk.TransactionBuilder.fromXDR( signedRestoreXDR.signedTxXdr, networkPassphrase, ); await server.sendTransaction(signedRestoreTx); console.log("Restoration complete. Retrying original transaction..."); // Retry the original transaction return submitOrRestoreAndRetry(contractId, method, ...args); } const result = await server.sendTransaction(signedTransaction); return result; } catch (error) { console.error("Transaction failed:", error); throw error; } } ``` This function now uses Freighter for signing transactions. It first checks if Freighter is connected and authorized, then proceeds with the transaction. If restoration is needed (indicated by a `HostStorageError`), it creates a separate restoration transaction, signs it with Freighter, and submits it before retrying the original transaction. #### Step 3: Use the helper function in your dapp You can now use this function to make contract calls that automatically handle restoration: ```javascript async function performContractAction(contractId, method, ...args) { try { const result = await submitOrRestoreAndRetry(contractId, method, ...args); console.log("Transaction successful:", result); return result; } catch (error) { console.error("Error performing contract action:", error); // Handle the error appropriately in your UI } } ``` #### Step 4: Handling contract instance restoration For restoring an entire contract instance, you might need a separate function: Here we will be using the [`getLedgerEntries` method](../../../data/apis/rpc/api-reference/methods/getLedgerEntries.mdx#2-request-the-contractcode-using-the-retrieved-ledgerkey) to get the WASM code of the contract and also the [`restoreFootprint` operation](../../../learn/fundamentals/transactions/list-of-operations.mdx#restore-footprint) to restore the contract instance. ```javascript async function restoreContractInstance(contractId) { try { let hasFreighter = await isConnected(); if (!hasFreighter) { return alert("Freighter wallet is required for transactions"); } const isAllowed = await setAllowed(); if (!isAllowed) { return alert("Please allow the transaction in Freighter wallet"); } const accountId = await getAddress(); const account = await server.getAccount(accountId.address); const fee = StellarSdk.BASE_FEE; const contract = new StellarSdk.Contract(contractId); const instance = contract.getFootprint(); window.ins = instance; // Fetch the WASM entry from the ledger const wasmEntry = await server.getLedgerEntries(instance); const restoreTx = new StellarSdk.TransactionBuilder(account, { fee: StellarSdk.BASE_FEE, }) .setNetworkPassphrase(StellarSdk.Networks.TESTNET) .setSorobanData( // Set the restoration footprint (remember, it should be in the // read-write part!) new StellarSdk.SorobanDataBuilder() .setReadWrite([ instance, ...wasmEntry.entries.map((entry) => entry.key), ]) .build(), ) .setTimebounds(0, Date.now() + 10000) .addOperation(StellarSdk.Operation.restoreFootprint({})) .build(); let preparedTx = await server.prepareTransaction(restoreTx); let signedXDR = await signTransaction(preparedTx.toXDR(), { networkPassphrase: networkPassphrase, }); let signedTx = StellarSdk.TransactionBuilder.fromXDR( signedXDR.signedTxXdr, networkPassphrase, ); return server.sendTransaction(signedTx); } catch (error) { console.error("Error restoring contract instance:", error); throw error; } } // Helper function to get the ledger key for the WASM entry function getWasmLedgerKey(entry) { return StellarSdk.xdr.LedgerKey.contractCode( new StellarSdk.xdr.LedgerKeyContractCode({ hash: entry.val().instance().wasmHash(), }), ); } ``` :::info This function specifically restores a contract instance and its associated Wasm code. It retrieves the contract's footprint and Wasm entry, creates a restoration transaction, which is then signed using Freighter and submitted to the network. ::: ## When to use these functions 1. [`performContractAction`](#step-3-use-the-helper-function-in-your-dapp) helper can be used when trying to invoke a smart contract function. It can help to restore persistent data associated with the call. 2. [`restoreContractInstance`](#step-4-handling-contract-instance-restoration) helper can be used during app initialization after the app has not been used for a long time. Using an indexer to get this info (when last app was used) is a great approach. ## Conclusion By implementing these state archival and restoration techniques, your dapp will be able to handle situations where contract data or instances have been archived, ensuring a smoother user experience even after periods of inactivity. The use of wallets like Freighter for transaction signing provides a secure and user-friendly way for users to interact with your dapp. Remember to handle errors appropriately and provide clear feedback to users throughout the restoration process. You may also want to implement a loading indicator in your UI while restoration is in progress, as it may take a moment to complete. Understanding and effectively managing state archival is crucial for creating robust and efficient Stellar-based dapps that can maintain functionality and data integrity over time. --- ## Work with contract specs in Java, Python, and PHP ## Introduction Soroban smart contracts are powerful tools for building decentralized applications on the Stellar network. To interact with these contracts effectively, it's crucial to understand their specifications and how to use them in your programming language of choice. A typical contract specification (spec) includes: 1. Data types used by the contract 2. Function definitions with their inputs and outputs 3. Error types that the contract may return These details guide how you interact with the contract, regardless of the programming language you're using. ## Prerequisites Before diving into contract interactions, ensure you have the following: - Stellar CLI ([`stellar`](../../smart-contracts/getting-started/setup.mdx#install-the-stellar-cli)) installed - A Soroban-compatible SDK for your programming language. View the [list of available SDKs](../../../tools/sdks/README.mdx) to find one that suits your needs - Access to a Stellar [RPC server](../../..//README.mdx) (local or on a test network) For this guide, we will focus on the [Java](../../../tools/sdks/client-sdks.mdx#java-sdk), [Python](../../../tools/sdks/client-sdks.mdx#python-sdk), and [PHP](../../../tools/sdks/client-sdks.mdx#php-sdk) SDKs for reference, but the concepts can also be applied to other languages. ## What are contract specs? A contract spec is just like an ABI (Application Binary Interface) in Ethereum. It is a standardized description of a smart contract's interface, typically in JSON format or XDR format. It defines the contract's functions, data structures, events, and errors in a way that external applications can understand and use. This specification serves as a crucial bridge between the smart contract and client applications, enabling them to interact without needing to know the contract's internal implementation details. # Stellar Contract Spec When you compile a smart contract using the Rust SDK, the resulting Wasm file includes a special section containing a complete description of your contract's interface types. This is often referred to as the contract's `spec` or `contract spec`. Stellar smart contract specification, known as the `contract spec`, is a foundational element for interacting with contracts and for building dApps on the Stellar network. The contract Spec provides a robust and fully typed definitions for interacting with smart contracts, offering functionality equivalent to Ethereum's ABI while addressing its limitations. The contract spec serves as a standardized interface for interacting with Stellar smart contracts. Similar to Ethereum's ABIs, but with key advantages: - On-chain Availability: Every contract spec is stored on-chain - Developer Comments: Comments from the contract author are preserved - Seamless communication between contracts and external applications - Ecosystem-wide compatibility with tools like wallets, explorers, and SDKs This standardization simplifies integrations and accelerates the development process. ## Fully Typed Contract Definitions The Contract Spec enforces fully typed definitions for all contract functions, inputs, and outputs. This ensures that: - Developers can define contract behavior explicitly, reducing ambiguity. - Type mismatches and runtime errors are minimized, leading to more reliable smart contracts. - Tools can provide intelligent suggestions and validations during development. By embedding type safety at the protocol level, Stellar’s Contract Spec creates a more predictable and robust development environment. ## Comparison to Ethereum ABI Stellar’s Contract Spec shares many similarities with Ethereum’s ABI but also introduces enhancements: | Feature | Ethereum ABI | Stellar Contract Spec | | ----------------------- | ------------------ | --------------------------- | | Fully typed contracts | Partial | Yes | | Decoding and validation | Manual or external | Built-in | | Security focus | Moderate | High (type safety enforced) | ## Generating contract specs [The Stellar CLI](https://github.com/stellar/stellar-cli) provides a command to generate a contract spec from a contract's source code. This process is easy but requires you to have the Wasm binary of the contract. Sometimes, you may not have access to the contract's source code or the ability to compile it. In such cases, you must use the [`stellar contract fetch`](../../../tools/cli/stellar-cli.mdx#stellar-contract-fetch) command to download the contract's Wasm binary and generate the spec. Finally, we use the [`stellar bindings`](../../../tools/cli/stellar-cli.mdx#stellar-contract-bindings-json) command to generate the contract spec from the Wasm binary. The Stellar Lab has a [Contract Explorer](https://lab.stellar.org/smart-contracts/contract-explorer) also provides the ability to view and to download a contract's contract spec. ### Fetching the contract binary ```bash stellar contract fetch --network-passphrase 'Test SDF Network ; September 2015' --rpc-url https://soroban-testnet.stellar.org --id CONTRACT_ID --out-file contract.wasm ``` ### Generating the contract spec from Wasm ```bash stellar contract bindings json --wasm contract.wasm > abi.json ``` ## Understanding the contract specification The ABI (Application Binary Interface) specification for Stellar smart contracts includes several key components that define how to interact with the contract. Let's examine these in detail with examples: 1. **Functions:** Functions are defined with their name, inputs, and outputs. They represent the callable methods of the contract. They can be used for writing data to the contract and reading data from the contract. Example: ```json { "type": "function", "name": "mint", "inputs": [ { "name": "contract", "value": { "type": "address" } }, { "name": "minter", "value": { "type": "address" } }, { "name": "to", "value": { "type": "address" } }, { "name": "amount", "value": { "type": "i128" } } ], "outputs": [ { "type": "result", "value": { "type": "tuple", "elements": [] }, "error": { "type": "error" } } ] } ``` This defines a `mint` function that takes four parameters and returns either an empty tuple or an error. Notice the type of each parameter: `address` for Stellar account addresses, `i128` for 128-bit integers, etc. 2. **Structs:** Structs define complex data types with multiple fields. Example: ```json { "type": "struct", "name": "ClaimableBalance", "fields": [ { "name": "amount", "value": { "type": "i128" } }, { "name": "claimants", "value": { "type": "vec", "element": { "type": "address" } } }, { "name": "time_bound", "value": { "type": "custom", "name": "TimeBound" } }, { "name": "token", "value": { "type": "address" } } ] } ``` This defines a `ClaimableBalance` struct with four fields. 3. **Unions:** Unions represent variables that can be one of several types. Example: ```json { "type": "union", "name": "DataKey", "cases": [ { "name": "Init", "values": [] }, { "name": "Balance", "values": [] } ] } ``` This defines a `DataKey` union that can be either `Init` or `Balance`. 4. **Custom Types:** Custom types refer to other defined types in the ABI. Example: ```json { "name": "time_bound", "value": { "type": "custom", "name": "TimeBound" } } ``` This refers to a custom `TimeBound` type defined elsewhere in the ABI. 5. **Vector Types:** Vectors represent arrays of a specific type. Example: ```json { "name": "claimants", "value": { "type": "vec", "element": { "type": "address" } } } ``` This defines a vector of addresses. 6. **Primitive Types:** These include basic types like `i128` (128-bit integer), `u64` (64-bit unsigned integer), `address`, etc. Example: ```json { "name": "amount", "value": { "type": "i128" } } ``` These specifications are crucial for encoding and decoding data when interacting with the contract. For example: - When calling the `mint` function, you must provide four parameters: three addresses and a 128-bit integer. - If a function returns a `ClaimableBalance`, you would expect to receive a struct with an amount (i128), a vector of addresses (claimants), a TimeBound object, and an address (token). - If a function could return an `Error`, it will most likely fail at simulation and you won't need to decode the result. ## Soroban types Before we dive into interacting with Stellar smart contracts, it is important to note that Soroban has its own set of types that are used to interact with the contracts as described in [this guide](../../../learn/fundamentals/contract-development/types/built-in-types.mdx). Here are some of the common types: - `u32`: Unsigned 32-bit integer - `u64`: Unsigned 64-bit integer - `i32`: Signed 32-bit integer - `i64`: Signed 64-bit integer - `u128`: Unsigned 128-bit integer - `i128`: Signed 128-bit integer - `bool`: Boolean - `string`: UTF-8 encoded string - `vec`: Variable-length array - `address`: Stellar account address - `map`: Key-value map - `symbol`: A small string used mainly for function names and map keys In this guide and the SDKs, these types are represented as `ScU32`, `ScU64`, `ScI32`, `ScI64`, `ScU128`, `ScI128`, `ScBool`, `ScString`, `ScVec`, `ScAddress`, `ScMap`, and `ScSymbol` respectively. Every other complex type can be derived using these basic types but these types do not really map to values in the programming languages. The Stellar SDKs provide helper classes to work with these types. ## Working with native Soroban types One of the most common tasks when working with Stellar smart contracts is converting between Stellar smart contract types and native types in your programming language. In this guide, we will go over some common conversions and show how they can be used to invoke contracts with the help of the contract spec. In most SDKs, the `ScVal` class or function is used to convert between Soroban types and native types. :::note The JSON code block shows the contract spec, while RUST code blocks show the contract for each example. ::: ### 1. Invoking a contract function with no parameters We will be using the `increment` function of the sample [increment contract](https://github.com/stellar/soroban-examples/tree/main/increment) to exemplify this. The `increment` function takes no parameters and increments the counter by 1. In this scenario, there is no need for conversions and passing the value `null` as contract arguments is sufficient in most SDKs. ```rust #[contractimpl] impl IncrementContract { /// Increment increments an internal counter, and returns the value. pub fn increment(env: Env) -> u32 { // Get the current count. let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0. log!(&env, "count: {}", count); // Increment the count. count += 1; // Save the count. env.storage().instance().set(&COUNTER, &count); env.storage().instance().extend_ttl(50, 100); // Return the count to the caller. count } } ``` ```json [ { "type": "function", "doc": "Increment increments an internal counter, and returns the value.", "name": "increment", "inputs": [], "outputs": [ { "type": "u32" } ] } ] ``` ```python # pip install --upgrade stellar-sdk from stellar_sdk import SorobanServer, soroban_rpc, Account, Asset, Keypair, Network, TransactionBuilder def send_transaction() -> soroban_rpc.SendTransactionResponse: server = SorobanServer(server_url='https://soroban-testnet.stellar.org', client=None) root_keypair = Keypair.from_secret( "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ) root_account = Account(account=root_keypair.public_key, sequence=1) contract_id = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE" transaction = ( TransactionBuilder( source_account=root_account, network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, base_fee=100, ) .append_invoke_contract_function_op( contract_id=contract_id, function_name="increment", parameters=[], source=root_keypair.public_key, ) # mark this transaction as valid only for the next 30 seconds .set_timeout(30) .build() ) transaction.sign(root_keypair) response = server.send_transaction(transaction) return response response = send_transaction() print("status", response.status) print("hash:", response.hash) print("status:", response.status) print("errorResultXdr:", response.error_result_xdr) ``` ```java // implementation 'network.lightsail:stellar-sdk:3.1.0' public class SendTransactionExample { public static void main(String[] args) { SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org"); try { // Replace with your source account's public key String sourcePublicKey = "GBSBL6FBPX5UHKL4AZCPUU6PXKUBYMKRUN3L4YQ4V2CCWSE7YMN2HYPB"; // Replace with your source account's secret key String sourceSecretSeed = "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; KeyPair sourceKeyPair = KeyPair.fromSecretSeed(sourceSecretSeed); // Decode the source account's public key to TransactionBuilderAccount TransactionBuilderAccount account = server.getAccount(sourceKeyPair.getAccountId()); // Define the contract ID and function to invoke String contractId = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"; String functionName = "increment"; InvokeHostFunctionOperation operation = InvokeHostFunctionOperation.invokeContractFunctionOperationBuilder( contractId, functionName, null ) .build(); Transaction unpreparedTransaction = new TransactionBuilder(account, Network.TESTNET) .setBaseFee(Transaction.MIN_BASE_FEE) .addOperation(operation) .setTimeout(300) .build(); Transaction transaction; try { transaction = server.prepareTransaction(unpreparedTransaction); } catch (PrepareTransactionException e) { throw new RuntimeException("Prepare transaction failed", e); } catch (NetworkException e) { throw new RuntimeException("Network error", e); } // Sign the transaction transaction.sign(sourceKeyPair); // Send the transaction using the SorobanServer SendTransactionResponse response = server.sendTransaction(transaction); System.out.println("Status: " + response.getStatus()); System.out.println("Transaction Hash: " + response.getHash()); System.out.println("Latest Ledger: " + response.getLatestLedger()); System.out.println("Ledger Close Time: " + response.getLatestLedgerCloseTime()); } catch (Exception e) { System.err.println("An error has occurred:"); e.printStackTrace(); } } } ``` ```php getAccount($accountAId); $network = Network::testnet(); $contractId = "CAMN24E6KNIXQBYPJJ4K7XRCUUJUMYSJRCSLZ2WOO6WJTSKIXAZWNYHK"; $invokeContractHostFunction = new InvokeContractHostFunction($contractId, "increment", null); $builder = new InvokeHostFunctionOperationBuilder($invokeContractHostFunction); $op = $builder->build(); $transaction = (new TransactionBuilder($accountA)) ->addOperation($op)->build(); $request = new SimulateTransactionRequest($transaction); $simulateResponse = $server->simulateTransaction($request); $transaction->setSorobanTransactionData($simulateResponse->getTransactionData()); $transaction->addResourceFee($simulateResponse->minResourceFee); $transaction->sign($accountAKeyPair, $network); $server->sendTransaction($transaction); } } ``` :::info Subsequent examples will show code blocks for using the contract spec only to reduce redundancy. ::: ### 2. Invoking a contract function with one or more parameters Generally, this involves passing in a native `array` (not a `ScVec`) of parameters to the contract function. We will be using the `hello` function of the sample [Hello World contract](https://github.com/stellar/soroban-examples/tree/main/hello_world) to exemplify this. We know from the spec that the `hello` function takes a string parameter and returns a vector of strings. In this scenario, we need to convert the string parameter to a `ScString` type before passing it to the contract. This process is convenient using the `ScVal` class or function in most SDKs. ```rust #[contract] pub struct HelloContract; #[contractimpl] impl HelloContract { pub fn hello(env: Env, to: String) -> Vec { vec![&env, String::from_str(&env, "Hello"), to] } } ``` ```json [ { "type": "function", "doc": "", "name": "hello", "inputs": [ { "doc": "", "name": "to", "value": { "type": "string" } } ], "outputs": [ { "type": "vec", "element": { "type": "string" } } ] } ] ``` ```python from stellar_sdk import Keypair, Network, SorobanServer, TransactionBuilder, scval ..... tx = ( TransactionBuilder(source, network_passphrase, base_fee=100) .set_timeout(300) .append_invoke_contract_function_op( contract_id=contract_id, function_name="hello", parameters=[ scval.to_string("John"), ] ).build()) ``` ```java // ..... List contractArgs = new ArrayList(); contractArgs.add(Scv.toString("John")); InvokeHostFunctionOperation operation = InvokeHostFunctionOperation .invokeContractFunctionOperationBuilder(contractId, "hello", contractArgs).build(); TransactionBuilder transaction = new TransactionBuilder(source, Network.TESTNET); Transaction tx = transaction.addOperation(operation).build(); ``` ```php $arg = \Soneso\StellarSDK\Xdr\XdrSCVal::forString("John"); $invokeContractHostFunction = new InvokeContractHostFunction($contractId, "hello", [$arg]); $builder = new InvokeHostFunctionOperationBuilder($invokeContractHostFunction); $op = $builder->build(); $transaction = (new TransactionBuilder($accountA)) ->addOperation($op)->build(); ``` ### 3. Getting responses from contracts Data returned from contracts is also in `ScVal` format and need to be converted to native types in your programming language. We will still be using the `hello` function of the sample [Hello World contract](https://github.com/stellar/soroban-examples/tree/main/hello_world) to exemplify this. We know from the Spec that the `hello` function takes a string parameter and returns a vec of strings. In this scenario, we need to convert the value returned from an `ScVec` of `ScString` type to `array` of `string` before making use of it. Steps: - Extract an `ScVec` from the return value - Extract each `ScString` from the `ScVec` - Convert each `ScString` to a native string This process is convenient using the `ScVal` class or function in most SDKs. Ideally, to retrieve this value, we need to use the [`getTransaction`](../../../data/apis/rpc/api-reference/methods/getTransaction.mdx) RPC method using the response hash of the transaction that invoked the contract function. ```rust #[contract] pub struct HelloContract; #[contractimpl] impl HelloContract { pub fn hello(env: Env, to: String) -> Vec { vec![&env, String::from_str(&env, "Hello"), to] } } ``` ```json [ { "type": "function", "doc": "", "name": "hello", "inputs": [ { "doc": "", "name": "to", "value": { "type": "string" } } ], "outputs": [ { "type": "vec", "element": { "type": "string" } } ] } ] ``` ```python from stellar_sdk import SorobanServer, soroban_rpc from stellar_sdk import xdr as stellar_xdr from stellar_sdk.soroban_rpc import GetTransactionStatus def get_transaction(hash: str) -> soroban_rpc.GetTransactionResponse: server = SorobanServer(server_url='https://soroban-testnet.stellar.org', client=None) tx = server.get_transaction(hash) return tx get_transaction_data = get_transaction("7e47c6ba2ebe53e156bc50c48e34302d49c91c04c465e8cd2b8a25219c2c8121") if get_transaction_data.status == GetTransactionStatus.SUCCESS: transaction_meta = stellar_xdr.TransactionMeta.from_xdr( get_transaction_data.result_meta_xdr ) result = transaction_meta.v4.soroban_meta.return_value output = [] for x in result.vec.sc_vec: decoded_string = x.str.sc_string.decode() output.append(decoded_string) print(f"transaction result: {output}") else: print(f"Transaction failed: {get_transaction_data.result_xdr}") ``` ```java public static void main(String[] args) { SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org"); try { GetTransactionResponse tx = server .getTransaction("7e47c6ba2ebe53e156bc50c48e34302d49c91c04c465e8cd2b8a25219c2c8121"); if (tx.getStatus() == GetTransactionResponse.GetTransactionStatus.SUCCESS) { List output = new ArrayList(); String base64Xdr = tx.getResultMetaXdr(); // convert the string to a result SCVal[] result = TransactionMeta.fromXdrBase64(base64Xdr).getV4() .getSorobanMeta().getReturnValue().getVec() .getSCVec(); for (SCVal x : result) { output.add(x.getStr().getSCString().toString()); } System.out.println("transaction result: " + output.toString()); } else { System.out.println("Transaction failed: " + tx.getStatus()); } } catch (Exception e) { System.err.println("An error has occurred:"); e.printStackTrace(); } } ``` ```php getTransaction($txhash); $status = $statusResponse->status; $resultArr = []; if ($status == GetTransactionResponse::STATUS_FAILED) { print ("Transaction failed: " . $statusResponse->error . PHP_EOL); } else if ($status == GetTransactionResponse::STATUS_SUCCESS) { $resultValue = $statusResponse->getResultValue(); $resVec = $resultValue->vec; foreach ($resVec as $strVal) { $resultArr[] = $strVal->str; } print_r($resultArr); } return $resultArr; } } ``` ## Working with complex data types As described in [this guide](../../../learn/fundamentals/contract-development/types/custom-types.mdx), there are some other variants of data structure supported by Soroban. They are - `Struct` with named fields - `Struct` with unnamed fields - `Enum` (Unit and Tuple Variants) - `Enum` (Integer Variants) We would be looking at how these variants translate to the spec and how to construct them in the different SDKs. ### Struct with named fields Structs with named values when converted to ABI or spec are represented as a `ScMap` where each value has the key in `ScSymbol` and the value in the underlying type. ```rust #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct State { pub count: u32, pub last_incr: u32, } ``` ```json [ { "type": "struct", "doc": "", "name": "State", "fields": [ { "doc": "", "name": "count", "value": { "type": "u32" } }, { "doc": "", "name": "last_incr", "value": { "type": "u32" } } ] } ] ``` ```python from stellar_sdk import scval scval.to_map( { scval.to_symbol("count"): scval.to_u32(0), scval.to_symbol("last_incr"): scval.to_u32(0), } ) ``` ```java LinkedHashMap map = new LinkedHashMap(); map.put(Scv.toSymbol("count"), Scv.toUint32(0)); map.put(Scv.toSymbol("last_incr"), Scv.toUint32(0)); SCVal val = Scv.toMap(map); ``` ```php XdrSCVal::forU32(0), XdrSCVal::forSymbol("last_incr") => XdrSCVal::forU32(0), ] ); ``` ### Struct with unnamed fields Structs with unnamed values when converted to ABI or spec are represented as a `ScVal` where each value has the underlying type. ```rust #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct State(pub u32, pub u32); ``` ```json [ { "type": "struct", "doc": "", "name": "State", "fields": [ { "doc": "", "value": { "type": "u32" } }, { "doc": "", "value": { "type": "u32" } } ] } ] ``` ```python from stellar_sdk import scval scval.to_vec( [ scval.to_uint32(0), scval.to_uint32(0), ] ) ``` ```java List vec = new ArrayList(); vec.add(Scv.toUint32(0)); vec.add(Scv.toUint32(0)); SCVal val = Scv.toVec(vec); ``` ```php ### Enum (unit and tuple variants) Enums are generally represented with `ScVec`, their unit types are represented as `ScSymbol` and their tuple variants are represented as the underlying types. ```rust #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub enum Enum { A, B(u32), } ``` ```json [ { "type": "union", "doc": "", "name": "Enum", "cases": [ { "doc": "", "name": "A", "values": [] }, { "doc": "", "name": "B", "values": [ { "type": "u32" } ] } ] } ] ``` ```python from stellar_sdk import scval scval.to_vec( [ scval.to_symbol("A"), scval.to_map( { scval.to_symbol("B"): scval.to_uint32(0), } ), ] ) ``` ```java List vec = new ArrayList(); vec.add(Scv.toSymbol("A")); LinkedHashMap map = new LinkedHashMap(); map.put(Scv.toSymbol("B"), Scv.toUint32(0)); vec.add(Scv.toMap(map)); SCVal val = Scv.toVec(vec); ``` ```php XdrSCVal::forU32(0), ] ), ] ); ``` ### Enum (integer variants) Enums are generally represented with `ScVec`, the integer variant has no keys so it's just a `ScVec` of the underlying type. ```rust #[contracttype] #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(u32)] pub enum Enum { A = 1, B = 2, } ``` ```json [ { "type": "struct", "doc": "", "name": "Enum", "fields": [ { "doc": "", "name": "A", "type": "u32" }, { "doc": "", "name": "B", "type": "u32" } ] } ] ``` ```python from stellar_sdk import scval scval.to_vec( [ scval.to_uint32(0), scval.to_uint32(0), ] ) ``` ```java List vec = new ArrayList(); vec.add(Scv.toUint32(0)); vec.add(Scv.toUint32(0)); SCVal val = Scv.toVec(vec); ``` ```php ### A complex example Let's use the [timelock contract](https://github.com/stellar/soroban-examples/blob/main/timelock/src/lib.rs) example to show how to interact with a contract that has complex data types. This example uses a `TimeBound` struct that has a `TimeBoundKind` enum as one of its fields, which are parameters to the `deposit` function. This example combines most of the concepts we have discussed so far. ```rust #[derive(Clone)] #[contracttype] pub enum TimeBoundKind { Before, After, } #[derive(Clone)] #[contracttype] pub struct TimeBound { pub kind: TimeBoundKind, pub timestamp: u64, } #[contracttype] #[contractimpl] impl ClaimableBalanceContract { pub fn deposit( env: Env, from: Address, token: Address, amount: i128, claimants: Vec
, time_bound: TimeBound, ) {} } ``` ```json [ { "type": "union", "doc": "", "name": "TimeBoundKind", "cases": [ { "doc": "", "name": "Before", "values": [] }, { "doc": "", "name": "After", "values": [] } ] }, { "type": "struct", "doc": "", "name": "TimeBound", "fields": [ { "doc": "", "name": "kind", "value": { "type": "custom", "name": "TimeBoundKind" } }, { "doc": "", "name": "timestamp", "value": { "type": "u64" } } ] }, { "type": "function", "doc": "", "name": "deposit", "inputs": [ { "doc": "", "name": "from", "value": { "type": "address" } }, { "doc": "", "name": "token", "value": { "type": "address" } }, { "doc": "", "name": "amount", "value": { "type": "i128" } }, { "doc": "", "name": "claimants", "value": { "type": "vec", "element": { "type": "address" } } }, { "doc": "", "name": "time_bound", "value": { "type": "custom", "name": "TimeBound" } } ], "outputs": [] } ] ``` ```python from stellar_sdk import scval, SorobanServer, Keypair, Network, TransactionBuilder secret = "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" rpc_server_url = "https://soroban-testnet.stellar.org:443" contract_id = "CAIKIZOT2LXM2WBEPGTZTPHHTGVHGLEOSI4WE6BOHWIBHJOKHPMCOPLO" network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE kp = Keypair.from_secret(secret) soroban_server = SorobanServer(rpc_server_url) source = soroban_server.load_account(kp.public_key) # Let's build a transaction that invokes the `deposit` function. tx = ( TransactionBuilder(source, network_passphrase, base_fee=1000) .set_timeout(300) .append_invoke_contract_function_op( contract_id=contract_id, function_name="deposit", parameters=[ scval.to_address(kp.public_key), scval.to_address("GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), scval.to_int128(1), scval.to_vec( [ scval.to_address("GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), ] ), scval.to_map( { scval.to_symbol("kind"): scval.to_vec( [ scval.to_symbol("Before"), ] ), scval.to_symbol("timestamp"): scval.to_uint64(12346), } ), ], ) .build() ) ``` ## Reading contract events Reading contract events is similar to reading transaction results. You can use the [`getEvents`](../../../data/apis/rpc/api-reference/methods/getEvents.mdx) RPC method to get the list of events associated with a contract. One common convention is that small strings like function names, enum keys, and event topics are represented as `ScSymbol` in the contract spec. However, event topics can be any `scval` type depending on the contract implementation. In the example below, we will be encoding the mint to `ScSymbol` before querying it, and also encoding the addresses to `ScAddress`. Even after getting the event, we will need to parse the topics and value to get the actual values again from xdr base 64 to their corresponding types before then converting it to native types. ```rust let address_1: &Address = "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".into(); let address_2: &Address = "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".into(); let count: i128 = 1; env.events() .publish((symbol_short!("mint"), address_1, address_2), count); ``` ```python from stellar_sdk import SorobanServer, scval, xdr as stellar_xdr from stellar_sdk.exceptions import NotFoundError, BadResponseError from stellar_sdk.soroban_server import EventFilter, EventFilterType def get_events(): server = SorobanServer("https://soroban-testnet.stellar.org") # Define the request parameters start_ledger = 835020 contract_id = "CDLYESZILKBHBRSPKQCQ3Q4K4N6MBI6UIQR3QXJ5L6WSYXC4EMTHSNNX" topic = [ scval.to_symbol("mint").to_xdr(), scval.to_address("GALIALRZJ5EU2IJJSIQEA3D3ZIEHK5HPBHZJFUEPTGQU3MYEKKIUINTY").to_xdr(), scval.to_address("GC45QSBFYHGQUIWWQEOZ43INQGXX57CSSAABWRZ325H7MNFIFWZ56FD4").to_xdr(), ] try: # Use the get_events method directly events_response = server.get_events( start_ledger=start_ledger, filters=[ EventFilter( event_type=EventFilterType.CONTRACT, contract_ids=[contract_id], topics=[topic] ) ], limit=20 ) # Process the response print(f"Latest ledger: {events_response.latest_ledger}") for event in events_response.events: print(f"Event ID: {event.id}") print(f"Contract ID: {event.contract_id}") for _topic in event.topic: if _topic is None: continue sc_val = stellar_xdr.SCVal.from_xdr(_topic) if sc_val.sym is not None: print(f"Topic: {scval.from_symbol(sc_val)}") if sc_val.address is not None: print(f"Topic: {scval.from_address(sc_val).address}") if event.value is not None: value_sc_val = stellar_xdr.SCVal.from_xdr(event.value) if value_sc_val.i128 is not None: print(f"Value: {scval.from_int128(value_sc_val)}") print("---") except NotFoundError: print("No events found for the given parameters.") except BadResponseError as e: print(f"Error occurred: {str(e)}") ``` ```bash curl -X POST \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "getEvents", "params": { "startLedger": 1190000, "filters": [ { "type": "contract", "contractIds": [ "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" ], "topics": [ [ "AAAADwAAAARtaW50", "AAAAEgAAAAAAAAAAFoAuOU9JTSEpkiBAbHvKCHV07wnyktCPmaFNswRSkUQ=", "AAAAEgAAAAAAAAAAudhIJcHNCiLWgR2ebQ2Br378UpAAG0c710/2NKgts98=", ] ] } ], "pagination": { "limit": 20 } } }' \ https://soroban-testnet.stellar.org ``` ## Wrapping Up As we've seen, working with Soroban smart contracts across different programming languages isn't rocket science, but it does require some careful attention to detail. The key takeaways: - Always start with a solid understanding of your contract's spec - Get comfortable with converting between native types and Soroban's quirky data structures - Don't be intimidated by complex data types - they're just puzzles waiting to be solved - When in doubt, consult your SDK's documentation for language-specific nuances --- ## Contract Events Learn how to emit, ingest, and use events published from a Stellar smart contract. --- ## Consume previously ingested events Once events have been ingested into a database, for instance as done in the [ingest guide], they can be consumed without having the need to query again Stellar RPC. In the following, we will show how we can consume these events. Let's get started! ## First, get some events in a DB Continuing right where we left in the [ingest guide], we will use the ORM models to add a few more events. ```python from sqlalchemy import create_engine engine = create_engine("sqlite://", echo=True) ``` Remember that events published by Soroban are XDR encoded. We can use [stellar-sdk] to convert back and forth between values and XDR representation. In the following, we will use a topic called `transfer` and we will need some values and addresses. We can generate some test data: ```python stellar_sdk.scval.to_symbol("transfer").to_xdr() # 'AAAADwAAAAh0cmFuc2Zlcg==' stellar_sdk.scval.to_int32(10_000).to_xdr() # 'AAAABAAAJxA=' stellar_sdk.scval.to_int32(5_000).to_xdr() # 'AAAABAAAE4g=' stellar_sdk.scval.to_int32(1_000).to_xdr() # 'AAAABAAAA+g=' stellar_sdk.scval.to_address("GA7YNBW5CBTJZ3ZZOWX3ZNBKD6OE7A7IHUQVWMY62W2ZBG2SGZVOOPVH").to_xdr() # 'AAAAEgAAAAAAAAAAP4aG3RBmnO85da+8tCofnE+D6D0hWzMe1bWQm1I2auc=' stellar_sdk.scval.to_address("GAFYGBHKVFP36EOIRGG74V42F3ORAA2ZWBXNULMNDXAMMXQH5MCIGXXI").to_xdr() # 'AAAAEgAAAAAAAAAAC4ME6qlfvxHIiY3+V5ou3RADWbBu2i2NHcDGXgfrBIM=' ``` Now we can make some events using our ORM and send them to the database: ```python from sqlalchemy.orm import sessionmaker contract_id = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" Session = sessionmaker(engine) with Session.begin() as session: event_1 = Event( ledger=1, contract_id=contract_id, topics={ # transfer "topic_1": "AAAADwAAAAh0cmFuc2Zlcg==", # GA7YNBW5CBTJZ3ZZOWX3ZNBKD6OE7A7IHUQVWMY62W2ZBG2SGZVOOPVH "topic_2": "AAAAEgAAAAAAAAAAP4aG3RBmnO85da+8tCofnE+D6D0hWzMe1bWQm1I2auc=" }, value="AAAABAAAJxA=" ) event_2 = Event( ledger=1, contract_id=contract_id, topics={ # transfer "topic_1": "AAAADwAAAAh0cmFuc2Zlcg==", # GAFYGBHKVFP36EOIRGG74V42F3ORAA2ZWBXNULMNDXAMMXQH5MCIGXXI "topic_2": "AAAAEgAAAAAAAAAAC4ME6qlfvxHIiY3+V5ou3RADWbBu2i2NHcDGXgfrBIM=" }, value="AAAABAAAE4g=" ) session.add_all([event_1, event_2]) ``` ```text INFO sqlalchemy.engine.Engine BEGIN (implicit) INFO sqlalchemy.engine.Engine INSERT INTO "SorobanEvent" (contract_id, ledger, topics, value) VALUES (?, ?, ?, ?) RETURNING id INFO sqlalchemy.engine.Engine [...] ('CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC', 1, '{"topic_1": "AAAADwAAAAh0cmFuc2Zlcg==", "topic_2": "AAAAEgAAAAAAAAAAP4aG3RBmnO85da+8tCofnE+D6D0hWzMe1bWQm1I2auc="}', 'AAAABAAAJxA=') INFO sqlalchemy.engine.Engine INSERT INTO "SorobanEvent" (contract_id, ledger, topics, value) VALUES (?, ?, ?, ?) RETURNING id INFO sqlalchemy.engine.Engine [...] ('CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC', 1, '{"topic_1": "AAAADwAAAAh0cmFuc2Zlcg==", "topic_2": "AAAAEgAAAAAAAAAAC4ME6qlfvxHIiY3+V5ou3RADWbBu2i2NHcDGXgfrBIM="}', 'AAAABAAAE4g=') INFO sqlalchemy.engine.Engine COMMIT ``` :::info Here, we are storing XDR encoded values. We could have instead decided to store decoded values in the database. XDR being a compressed format, choosing when to decode the value is a tradeoff between CPU usage and memory consumption. ::: ## Consuming events Using the same model we used to ingest events into the database, we can query the database to iterate over all events present in the table. ```python from sqlalchemy import orm with orm.Session(engine) as session: stmt = sqlalchemy.select(Event) for event in session.scalars(stmt): print(event.topics, event.value) ``` ```text INFO sqlalchemy.engine.Engine BEGIN (implicit) INFO sqlalchemy.engine.Engine SELECT "SorobanEvent".id, "SorobanEvent".contract_id, "SorobanEvent".ledger, "SorobanEvent".topics, "SorobanEvent".value FROM "SorobanEvent" INFO sqlalchemy.engine.Engine [...] () ... ['AAAADwAAAAh0cmFuc2Zlcg==', 'AAAAEgAAAAAAAAAAbskHLxwXdlUVH3X3pVMFqpLHYpwmDD/PoaqYnQkX7J4=', 'AAAAEgAAAAGqo5kAOYww4Z8QWIx9TqXkXUvjFUg8mpEbsg03vV+8/w==', 'AAAADgAAAAZuYXRpdmUAAA=='] AAAACgAAAAAAAAAAAAAAC6Q7dAA= ['AAAADwAAAAh0cmFuc2Zlcg==', 'AAAAEgAAAAAAAAAAL6/diRR4by9YIZCM/+O0/BGYKWlSn2CvTEiHBptJs+k=', 'AAAAEgAAAAGqo5kAOYww4Z8QWIx9TqXkXUvjFUg8mpEbsg03vV+8/w==', 'AAAADgAAAAZuYXRpdmUAAA=='] AAAACgAAAAAAAAAAAAAAAAvrwgA= {'topic_1': 'AAAADwAAAAh0cmFuc2Zlcg==', 'topic_2': 'AAAAEgAAAAAAAAAAP4aG3RBmnO85da+8tCofnE+D6D0hWzMe1bWQm1I2auc='} AAAABAAAJxA= {'topic_1': 'AAAADwAAAAh0cmFuc2Zlcg==', 'topic_2': 'AAAAEgAAAAAAAAAAC4ME6qlfvxHIiY3+V5ou3RADWbBu2i2NHcDGXgfrBIM='} AAAABAAAE4g= INFO sqlalchemy.engine.Engine ROLLBACK ``` :::note Notice previous events being present and having a slightly different formatting. While we are using a schema, it is still easy to corrupt a database. This is only shown for demonstration purposes. ::: SQLAlchemy allows to make advanced queries. For example, we could filter a single event based on some specific fields. ```python with orm.Session(engine) as session: stmt = sqlalchemy.select(Event).where(Event.ledger == 1) for event in session.scalars(stmt): print(event.topics, event.value) ``` ```text INFO sqlalchemy.engine.Engine BEGIN (implicit) INFO sqlalchemy.engine.Engine SELECT "SorobanEvent".id, "SorobanEvent".contract_id, "SorobanEvent".ledger, "SorobanEvent".topics, "SorobanEvent".value FROM "SorobanEvent" WHERE "SorobanEvent".ledger = ? INFO sqlalchemy.engine.Engine [...] (1,) {'topic_1': 'AAAADwAAAAh0cmFuc2Zlcg==', 'topic_2': 'AAAAEgAAAAAAAAAAP4aG3RBmnO85da+8tCofnE+D6D0hWzMe1bWQm1I2auc='} AAAABAAAJxA= {'topic_1': 'AAAADwAAAAh0cmFuc2Zlcg==', 'topic_2': 'AAAAEgAAAAAAAAAAC4ME6qlfvxHIiY3+V5ou3RADWbBu2i2NHcDGXgfrBIM='} AAAABAAAE4g= INFO sqlalchemy.engine.Engine ROLLBACK ``` ## Streaming events Depending on our application, we might want to consume events periodically by calling the database to see if there is anything new. Or fetch data as needed by our application. There is another possibility: event listeners! While we are at it, we can make the results more readable or usable in Python by using the conversion helper provided by [stellar-sdk]. ```python @sqlalchemy.event.listens_for(Event, "after_insert") def event_handler(mapper, connection, target): topics = target.topics value = stellar_sdk.scval.to_native(target.value) for key, topic in topics.items(): topics[key] = stellar_sdk.scval.to_native(topic) print(f"Event listener: {topics} {value}") ``` Next time a record gets inserted into the database, this event handler will be called. Let's try this: ```python with Session.begin() as session: event_3 = Event( ledger=2, contract_id=contract_id, topics={ # transfer "topic_1": "AAAADwAAAAh0cmFuc2Zlcg==", # GA7YNBW5CBTJZ3ZZOWX3ZNBKD6OE7A7IHUQVWMY62W2ZBG2SGZVOOPVH "topic_2": "AAAAEgAAAAAAAAAAP4aG3RBmnO85da+8tCofnE+D6D0hWzMe1bWQm1I2auc=" }, value="AAAABAAAJxA=" ) session.add(event_3) ``` ```text INFO sqlalchemy.engine.Engine BEGIN (implicit) INFO sqlalchemy.engine.Engine INSERT INTO "SorobanEvent" (contract_id, ledger, topics, value) VALUES (?, ?, ?, ?) INFO sqlalchemy.engine.Engine [...] ('CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC', 2, '{"topic_1": "AAAADwAAAAh0cmFuc2Zlcg==", "topic_2": "AAAAEgAAAAAAAAAAP4aG3RBmnO85da+8tCofnE+D6D0hWzMe1bWQm1I2auc="}', 'AAAABAAAJxA=') Event listener: {'topic_1': 'transfer', 'topic_2':
} 10000 INFO sqlalchemy.engine.Engine COMMIT ``` Congratulations, you are ready to consume events from Stellar RPC! ## Going further Using the techniques we just presented would probably be enough for a lot of use cases. Still, for the readers wanting to go further there are a few things to look into. ### Asynchronous programming So far, we have used SQLAlchemy in a synchronous way. If we were to have an endpoint in the backend calling the database, this endpoint would block during the database call. SQLAlchemy supports asynchronous programming with `async` and `await` keywords. As a general thought, it's simpler to start with a synchronous logic and then move on to adding support for async when everything works as expected. Debugging concurrent application brings an additional layer of complexity. SQLAlchemy allows you to simply change from a synchronous session to an asynchronous one without having to change your models nor queries making it a very easy task to use one or the other. ### Idempotency considerations Depending on your application, you might want to look into the concept of idempotency. Or simply put: guarantee that an event is consumed only once. For instance, if you use events for bookkeeping purposes on a payment application, processing twice the same event could result in a double spend. In such cases, you will want your system to be idempotent to guarantee that this scenario would be covered. There is a large body of technical literature around this topic and there is no one solution fits all. It might be enough for your application to add a column in the database to mark a message as processed, though you would need to account for network issues happening while you process a given event. Using SQLAlchemy with a database like PostgreSQL could help as if done properly operations can be made atomic. i.e. you can ensure that a certain chain of actions has been performed before committing a transaction to the database. While researching this topic, you could search for _message brokers_ like RabbitMQ of Kafka---to cite widely used solutions. [ingest guide]: ingest.mdx [stellar-sdk]: https://stellar-sdk.readthedocs.io/ --- ## Ingest events published from a contract Stellar RPC provides a [`getEvents` method] which allows you to query events from a smart contract. However, the data retention window for these events is 7 days at most. If you need access to a longer-lived record of these events you'll want to "ingest" the events as they are published, maintaining your own record or database as events are ingested. There are many strategies you can use to ingest and keep the events published by a smart contract. Among the simplest might be using a community-developed tool such as [Mercury](https://mercurydata.app) which will take all the infrastructure work off your plate for a low subscription fee. Or, [indexers](../../../data/indexers/README.mdx) can provide ingested event data for affordable prices. Another approach we'll explore here is using a cron job to query Stellar RPC periodically and store the relevant events in a locally stored SQLite database. We are going to use an Object Relational Mapper (ORM), allowing us to write database query directly in Python or JavaScript. ## Setup In a [virtual environment](https://docs.python.org/3/tutorial/venv.html), install the Python dependencies: ```sh pip install sqlalchemy stellar-sdk ``` ## Setup the Database Client To access the database, we will use [SQLAlchemy](https://www.sqlalchemy.org), which is a frequently used Python library to query database. We are going to ingest events in a table named `StellarEvent`. In SQLAlchemy, this translates into a class, also called a database model: ```python from typing import Any from sqlalchemy import orm, JSON class Base(orm.DeclarativeBase): # needed to tell SQLAlchemy to translate a dictionary into a JSON entry type_annotation_map = { dict[str, Any]: JSON, } class Event(Base): __tablename__ = "StellarEvent" id: orm.Mapped[int] = orm.mapped_column(primary_key=True) contract_id: orm.Mapped[str] ledger: orm.Mapped[int] topics: orm.Mapped[dict[str, Any]] value: orm.Mapped[str] ``` We will use an in-memory-only SQLite database for this guide, but thanks to the use of an ORM, we could be using any other supported database. We would simply need to change the connection string. ```python from sqlalchemy import create_engine engine = create_engine("sqlite://", echo=True) # the following creates the table in the DB Base.metadata.create_all(engine) ``` :::tip By setting `echo=True` we can understand what is happening on the database. Creating the database table leads to the following logs: ::: ```text BEGIN (implicit) PRAGMA main.table_info("StellarEvent") ... PRAGMA temp.table_info("StellarEvent") ... CREATE TABLE StellarEvent ( id INTEGER NOT NULL, contract_id VARCHAR NOT NULL, ledger INTEGER NOT NULL, topics JSON NOT NULL, value VARCHAR NOT NULL, PRIMARY KEY (id) ) ... COMMIT ``` The finer details of choosing a Prisma configuration are beyond the scope of this document. You can get a lot more information in the [Prisma quickstart](https://www.prisma.io/docs/getting-started/quickstart). Here is our Prisma schema's model: ```text model StellarEvent { id String @id type String ledger Int contract_id String topic_1 String? topic_2 String? topic_3 String? topic_4 String? value String } ``` :::info Using a database model is very convenient as it allows us to control the database schema programmatically. If we need to change the schema, by adding a new columns for instance, then using an ORM allows us to use very powerful migration tools. ::: We'll use this model to create and query for the events stored in our database. ## Query Events from Stellar RPC First, we'll need to query the events from Stellar RPC. This simple example makes an RPC request using the [`getEvents` method], filtering for all `transfer` events that are emitted by the native XLM contract. :::note We are making some assumptions here. We'll assume that your contract sees enough activity, and that you are querying for events frequently enough that you aren't in danger of needing to figure out the oldest ledger Stellar RPC is aware of. The approach we're taking is to find the largest (most recent) ledger sequence number in the database and query for events starting there. Your use-case may require some logic to determine what the latest ledger is, and what the oldest ledger available is, etc. ::: If we start from scratch, there is no known ledger so we can try to ingest roughly the last 7 days assuming a ledger closes every 6s. ```python soroban_server = stellar_sdk.SorobanServer("https://soroban-testnet.stellar.org") ledger = soroban_server.get_latest_ledger().sequence - int(3600 / 6 * 24 * 7) ``` Later on, we will be able to start from the latest ingested ledger by making a query to our DB. ```python with orm.Session(engine) as session: stmt = sqlalchemy.select(Event.ledger).where( Event.contract_id == contract_id ).order_by(Event.ledger.desc()) ingested_ledger = session.scalars(stmt).first() if ingested_ledger: ledger = ingested_ledger ``` Let's get events from Stellar RPC! ```python from stellar_sdk.soroban_rpc import EventFilter, EventFilterType contract_id = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" res = soroban_server.get_events( ledger, filters=[ EventFilter( type=EventFilterType.CONTRACT, contract_ids=[contract_id], topics=[["AAAADwAAAAh0cmFuc2Zlcg==", "*", "*", "*"]], ) ], ) events = res.events ``` We use the `@stellar/stellar-sdk` library: ```javascript const server = new Server("https://soroban-testnet.stellar.org"); const prisma = new PrismaClient(); let latestEventIngested = await prisma.stellarEvent.findFirst({ orderBy: [ { ledger: "desc", }, ], }); let { events } = await server.getEvents({ startLedger: latestEventIngested.ledger, filters: [ { type: "contract", contractIds: ["CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"], topics: [["AAAADwAAAAh0cmFuc2Zlcg==", "*", "*", "*"]], }, ], }); ``` ## Store Events in the Database Now, we'll check if the `events` object contains any new events we should store, and we do exactly that. We're storing the event's topics and values as base64-encoded strings here, but you could decode the necessary topics and values into the appropriate data types for your use-case. :::tip Make your life easier with a SQLAlchemy [`sessionmaker`](https://docs.sqlalchemy.org/en/20/orm/session_basics.html#using-a-sessionmaker) when making transactions (e.g. add more than one record in a single database call.) ::: ```python from sqlalchemy.orm import sessionmaker Session = sessionmaker(engine) with Session.begin() as session: events_ = [] for event in events: topic_ = event.topic events_.append(Event(contract_id=contract_id, ledger=event.ledger, topics=topic_, value=event.value)) session.add_all(events_) ``` ```text BEGIN (implicit) INFO sqlalchemy.engine.Engine COMMIT INFO sqlalchemy.engine.Engine BEGIN (implicit) INFO sqlalchemy.engine.Engine INSERT INTO "StellarEvent" (contract_id, ledger, topics, value) VALUES (?, ?, ?, ?) RETURNING id INFO sqlalchemy.engine.Engine [...] ('CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC', 3311, '["AAAADwAAAAh0cmFuc2Zlcg==", "AAAAEgAAAAAAAAAAJY16rJOcKxQayCR7ayNA80hW5q1U4ypIGOY7NktBfKU=", "AAAAEgAAAAHXkotywnA8z+r365/0701QSlWouXn8m0UOoshCtNHOYQ==", "AAAADgAAAAZuYXRpdmUAAA=="]', 'AAAACgAAAAAAAAAAAAAAAAAAAGQ=') INFO sqlalchemy.engine.Engine INSERT INTO "StellarEvent" (contract_id, ledger, topics, value) VALUES (?, ?, ?, ?) RETURNING id INFO sqlalchemy.engine.Engine [...] ('CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC', 3325, '["AAAADwAAAAh0cmFuc2Zlcg==", "AAAAEgAAAAAAAAAAJY16rJOcKxQayCR7ayNA80hW5q1U4ypIGOY7NktBfKU=", "AAAAEgAAAAHXkotywnA8z+r365/0701QSlWouXn8m0UOoshCtNHOYQ==", "AAAADgAAAAZuYXRpdmUAAA=="]', 'AAAACgAAAAAAAAAAAAAAAAAAAGQ=') ... COMMIT ``` ```javascript if (events?.length) { events.forEach(async (event) => { await prisma.stellarEvent.create({ data: { id: event.id, type: event.type, ledger: event.ledger, contract_id: event.contractId!.toString(), topic_1: event.topic[0]?.toXDR('base64') || null, topic_2: event.topic[1]?.toXDR('base64') || null, topic_3: event.topic[2]?.toXDR('base64') || null, topic_4: event.topic[3]?.toXDR('base64') || null, value: event.value.toXDR('base64') }, }); }); } ``` ## Run the Script with Cron A cron entry is an excellent way to automate this script to gather and ingest events every so often. You could configure this script to run as (in)frequently as you want or need. This example would run the script every 24 hours at 1:14 pm: ```sh 14 13 * * * python /absolute/path/to/script.py ``` ```sh 14 13 * * * node /absolute/path/to/script.js ``` Here's another example that will run the script every 30 minutes: ```sh 30 * * * * python /absolute/path/to/script.py ``` ```sh 30 * * * * node /absolute/path/to/script.js ``` [`getEvents` method]: ../../../data/apis/rpc/api-reference/methods/getEvents.mdx --- ## Publish events from a Rust contract An event can contain topics, alongside the data it is publishing. The topics data can be any value or type you want. :::info[Whisk Changes] With the release of Whisk, Protocol 23, the syntax for publishing smart contract events has changed. In order to provide the most up-to-date information, this guide has been updated to include the new patterns. Find more detailed information in the [Rust SDK documentation]. ::: The strategy here is to first create some `struct`s that will define how our events are shaped. Then, within the contract's function, we can create and publish those structs. ```rust // This event will be published with the following structure: // `["COUNTER", "increment"], data = count: u32` #[contractevent(topics = ["COUNTER", "increment"], data_format = "single-value")] pub struct Increment { count: u32, } // Events without explicit topics will use the struct name for the sole topic. // By default, the event data will follow the struct shape. // This event will be published with the following structure: // `["borrow"], data = {addr: Address, amount: i128}` #[contractevent] pub struct Borrow { addr: Address, amount: i128, } // Event topics can also be noted in the struct, so they're dynamic. // This event will be published with the following structure: // `["deposit", addr: Address, token: Address], data = [amount: i128, time: u64]` #[contractevent(data_format = "vec")] pub struct Deposit { #[topic] addr: Address, #[topic] token: Address, amount: i128, time: u64, } // This function does nothing beside publish events. pub fn events_function(env: Env, invoker: Address, token: Address) { Increment { count: 8675309, }.publish(&env); Borrow { addr: invoker.clone(), amount: 123_0000000, }.publish(&env); Deposit { addr: invoker, token: token, amount: 321_0000000, time: env.ledger().timestamp(), }.publish(&env); } ``` A more realistic example can be found in the way the [token interface] works. For example, the interface requires an event to be published every time the `transfer` function is invoked, with the following information: ```rust #[contractevent(data_format = "single-value")] pub struct Transfer { #[topic] from: Address, #[topic] to: Address, amount: i128, } pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { // transfer logic omitted here Transfer { from, to, amount }.publish(&env); } ``` [token interface]: ../../../tokens/token-interface.mdx [Rust SDK documentation]: https://docs.rs/soroban-sdk/latest/soroban_sdk/_migrating/v23_contractevent/index.html --- ## Fees & Metering Fees and metering in Soroban smart contracts work differently than the fees for "regular" Stellar transactions. The Stellar network still provides cheap, accessible transaction and that now includes smart contract metering! --- ## Analyzing smart contract cost and efficiency Several factors influence how quickly and efficiently your smart contracts execute on the Stellar network. This guide will help you understand these factors and provide tips on how to write cost-effective contracts. ## How to optimize smart contract cost: Complex contracts with numerous conditions, loops, and computations require more processing power on Stellar. This can lead to higher gas costs (transaction fees) and slower execution times. ### 1. Efficient loop and storage calls usage A contract that requires multiple loops and conditions to execute will cost more than a simple contract that executes a single operation. #### Not Optimal Contract ❎ ```rust #![no_std] use soroban_sdk::{contract, contractimpl, log, vec, Map, Env}; #[contract] pub struct ExampleContract; #[contractimpl] impl ExampleContract { // Function to update values in storage inefficiently pub fn update_values(env: Env, values: Vec) { for &value in values.iter() { let current_count = env.storage().persistent().get("total_count"); env.storage().persistent().set("total_count", &(current_count + value)); } } } ``` :::danger **Problem**: Each iteration of the loop performs a separate read and write operation to update total_count in storage. **Inefficient**: This results in multiple expensive storage operations (read and write) within the loop, increasing gas costs significantly as the array size (values.len()) grows. ::: #### Optimal Contract ✅ ```rust #![no_std] use soroban_sdk::{contract, contractimpl, log, vec, Env, Map}; #[contract] pub struct ExampleContract; #[contractimpl] impl ExampleContract { // Function to update values in storage efficiently pub fn update_values(env: Env, values: Vec) { let mut total_count = env.storage().persistent().get("total_count"); for &value in values.iter() { total_count += value; } env.storage().persistent().set("total_count", &total_count); } } ``` :::tip In this optimized approach, we'll accumulate the changes outside the loop and perform a single storage update and also a single read operation. This reduces the number of storage operations and the overall gas cost of the contract. ::: ### 2. Proper use of batch operations From the first example, we can see that batch operations are more efficient than individual operations. This is because batch operations reduce the number of external calls to the blockchain, which can be costly. However, there are some scenarios where the use of batch operations can be further optimized. Examples are shown below. #### Not Optimal Contract ❎ ```rust #![no_std] use soroban_sdk::{contract, contractimpl, Address, Env, Symbol}; use soroban_sdk::token::Client as TokenClient; #[contract] pub struct TokenTransferContract; #[contractimpl] impl TokenTransferContract { // Inefficient way: Multiple individual transfers pub fn transfer_tokens_inefficient( env: Env, token: Address, from: Address, to: Vec
, amount_each: i128, ) { let token_client = TokenClient::new(&env, &token); for recipient in to.iter() { token_client.transfer(&from, recipient, &amount_each); } } } ``` :::danger This function performs individual transfers for each recipient. While straightforward, it's inefficient because each transfer is a separate subcontract call, potentially leading to higher gas costs and slower execution. ::: #### Optimal Contract ✅ ```rust #![no_std] use soroban_sdk::{contract, contractimpl, Address, Env, Symbol}; use soroban_sdk::token::Client as TokenClient; #[contract] pub struct TokenTransferContract; #[contractimpl] impl TokenTransferContract { // Efficient way: Batch transfer pub fn transfer_tokens_efficient( env: Env, token: Address, from: Address, to: Vec
, amount_each: i128, ) { let token_client = TokenClient::new(&env, &token); let total_amount = amount_each * (to.len() as i128); // Perform a single transfer for the total amount token_client.transfer(&from, &env.current_contract_address(), &total_amount); // Then distribute from the contract for recipient in to.iter() { token_client.transfer(&env.current_contract_address(), recipient, &amount_each); } } } ``` :::tip This function optimizes the process by: - First transferring the total amount to the contract itself in a single operation. - Then distributing the tokens from the contract to each recipient. Internal distributions are cheaper due to the reduction in the number of costly external blockchain transactions. By transferring the total amount to the contract and then distributing it internally, the contract minimizes the number of external calls, reducing gas costs and improving efficiency. ::: ### 3. Use of events over storage Events are a cost-effective way to store data that doesn't need to be accessed frequently. Events are cheaper than storage operations and can be used to store data that doesn't need to be accessed frequently. #### Default Contract ```rust #![no_std] use soroban_sdk::{ contract, contractimpl, log, symbol_short, vec, Address, Env, Symbol,Map, Vec }; #[contract] pub struct GameContract; #[contractimpl] impl GameContract { // Function to record a game move and update storage pub fn record_move(env: Env, player: Address, move_type: Symbol) { let mut player_moves: Map> = env.storage().persistent().get("player_moves"); let moves = player_moves.get(&player); moves.push(move_type.clone()); player_moves.set(player, moves); env.storage().persistent().set("player_moves", &player_moves); } // Function to unlock an achievement and update storage pub fn unlock_achievement(env: Env, player: Address, achievement: Symbol) { let mut player_achievements: Map> = env.storage().persistent().get("player_achievements"); let achievements = player_achievements.get(&player); achievements.push(achievement.clone()); player_achievements.set(player, achievements); env.storage().persistent().set("player_achievements", &player_achievements); } } ``` :::danger We cannot store everything in storage like we would do in a traditional database. This approach is not cost-effective as it involves multiple storage operations for each player move and saves each achievement to the storage ::: #### Optimized Using Events ```rust #![no_std] use soroban_sdk::{ contract, contractimpl, log, symbol_short, vec, Address, Env, Symbol,Map, Vec, }; #[contract] pub struct GameContract; #[contractimpl] impl GameContract { // Function to record a game move and emit an event pub fn record_move(env: Env, player: Address, move_type: Symbol) { // Emit event for the game move env.events().publish(("game_move",), (&player, move_type.clone())); } // Function to unlock an achievement and emit an event pub fn unlock_achievement(env: Env, player: Address, achievement: Symbol) { // Emit event for the unlocked achievement env.events().publish(("achievement_unlocked",), (&player, achievement.clone())); } } ``` :::tip In this optimized approach, we use events to store data that doesn't need to be accessed frequently. This reduces the number of storage operations and the overall gas cost of the contract. ::: #### Trade-offs: The current approach using events is indeed optimized for gas efficiency, but it comes with its own set of trade-offs. Let's see different approaches and their implications: ##### Current Approach (Using Events) - Pros: - Extremely gas-efficient as it minimizes storage operations - Useful for off-chain applications that can listen to and process events - Ideal for data that doesn't need to be accessed on-chain frequently - Cons: - Data is not directly accessible within the contract - Relies on external systems to capture and process the event data - Not suitable if you need to query or validate past moves within the contract ##### Storing Latest Move in Persistent Storage and History in Temporary Storage This approach offers a balance between accessibility and efficiency. - Pros: - Latest move is always accessible on-chain - Historical data is available within the same transaction - More flexible for in-contract logic that might need recent history - Cons: - Slightly higher gas cost than using only events - Temporary storage is cleared after each transaction, so historical data is not permanently accessible on-chain ### 4. Use of efficient data structures Heap allocated arrays are slow and costly. Prefer fixed-sized arrays or `soroban_sdk::vec!`. This is crucial for large arrays, as exceeding the current linear memory size (a multiple of 64KB) triggers `wasm32::memory_grow`, which is highly computationally intensive. ##### Example (Heap Allocated Array) ❎ ```rust let mut v1 = alloc::vec![]; ``` ##### Example (Fixed-Sized Array) ✅ ```rust let mut v2 = [0; 100]; ``` Storing many items in a `Vec` can be inefficient due to the linear time complexity of membership checks. However, there are some alternatives to having a cumbersome `Vec`: ##### Example (Inefficient Data Storage) ❎ ```rust #![no_std] use soroban_sdk::{contract, contractimpl, Address, Env, Symbol, Vec}; #[derive(Clone)] pub struct PlayerMove { player: Address, move_type: Symbol, } #[derive(Clone)] pub struct PlayerAchievement { player: Address, achievement: Symbol, } #[contract] pub struct NonOptimalGameContract; #[contractimpl] impl NonOptimalGameContract { // Function to record a move for a specific player pub fn record_move(env: Env, player: Address, move_type: Symbol) { let mut all_moves: Vec = env.storage().persistent().get("all_moves").unwrap_or_default(); all_moves.push(PlayerMove { player: player.clone(), move_type }); env.storage().persistent().set("all_moves", &all_moves); } // Function to get moves for a specific player pub fn get_moves(env: Env, player: Address) -> Vec { let all_moves: Vec = env.storage().persistent().get("all_moves").unwrap_or_default(); all_moves .iter() .filter(|m| m.player == player) .map(|m| m.move_type.clone()) .collect() } // Function to unlock an achievement for a specific player pub fn unlock_achievement(env: Env, player: Address, achievement: Symbol) { let mut all_achievements: Vec = env.storage().persistent().get("all_achievements").unwrap_or_default(); if !all_achievements.iter().any(|a| a.player == player && a.achievement == achievement) { all_achievements.push(PlayerAchievement { player: player.clone(), achievement }); env.storage().persistent().set("all_achievements", &all_achievements); } } // Function to get achievements for a specific player pub fn get_achievements(env: Env, player: Address) -> Vec { let all_achievements: Vec = env.storage().persistent().get("all_achievements").unwrap_or_default(); all_achievements .iter() .filter(|a| a.player == player) .map(|a| a.achievement.clone()) .collect() } } ``` :::danger[This non-optimal version has several inefficiencies:] - Single Vec for All Data: All moves and achievements are stored in single Vecs (all_moves and all_achievements), regardless of the player. This means every operation requires loading and saving the entire dataset, which becomes increasingly expensive as the number of entries grows. - Linear Search Operations: Retrieving moves or achievements for a specific player requires iterating through the entire Vec, which has O(n) time complexity. This becomes very slow and gas-intensive as the number of entries increases. - Redundant Data Storage: The player's address is stored repeatedly for each move and achievement, leading to unnecessary data duplication. - Inefficient Achievement Checking: Before adding a new achievement, the code iterates through all achievements to check for duplicates, which is an O(n) operation. - Gas Inefficiency: Every operation (adding a move, unlocking an achievement, retrieving data) requires loading and saving the entire dataset, which is extremely gas-inefficient. - Scalability Issues: As the number of players and their actions increase, the performance of this contract will degrade significantly. - Lack of Data Separation: Moves and achievements are not clearly separated in the storage structure, making it harder to manage and potentially leading to confusion in more complex scenarios. ::: ##### Alternative: using keyed vecs ✅ ```rust #![no_std] use soroban_sdk::{contract, contractimpl, Address, Env, Symbol, Vec}; #[derive(Clone)] pub enum DataKey { Moves(Address), Achievements(Address), } #[contract] pub struct GameContract; #[contractimpl] impl GameContract { // Function to record a move for a specific player pub fn record_move(env: Env, player: Address, move_type: Symbol) { let key = DataKey::Moves(player.clone()); let mut moves: Vec = env.storage().persistent().get(&key).unwrap_or_default(); moves.push(move_type.clone()); env.storage().persistent().set(&key, &moves); } // Function to get moves for a specific player pub fn get_moves(env: Env, player: Address) -> Vec { let key = DataKey::Moves(player); env.storage().persistent().get(&key).unwrap_or_default() } // Function to unlock an achievement for a specific player pub fn unlock_achievement(env: Env, player: Address, achievement: Symbol) { let key = DataKey::Achievements(player.clone()); let mut achievements: Vec = env.storage().persistent().get(&key).unwrap_or_default(); if !achievements.contains(&achievement) { achievements.push(achievement); env.storage().persistent().set(&key, &achievements); } } // Function to get achievements for a specific player pub fn get_achievements(env: Env, player: Address) -> Vec { let key = DataKey::Achievements(player); env.storage().persistent().get(&key).unwrap_or_default() } } ``` :::tip Here's a breakdown of the optimizations and benefits: - Address-Specific Storage: Each player's moves and achievements are stored separately, allowing for efficient retrieval of player-specific data. The DataKey enum provides a clear structure for organizing different types of data associated with addresses. - Efficient Data Access: By using the enum as a key, we can directly access the data for a specific player without needing to search through a larger data structure. This approach is particularly efficient when you need to frequently access or update data for individual players. - Flexible Data Structure: The use of `Vec` for both moves and achievements allows for an unlimited number of entries for each player. This structure is suitable for storing ordered lists of moves or unique achievements. - Gas Efficiency: By storing data separately for each player, we avoid having to load and modify a large, all-encompassing data structure for every operation. This can lead to significant gas savings, especially as the number of players and amount of data grows. - Clear Separation of Concerns: The enum clearly separates different types of data (moves vs. achievements), making the code more readable and maintainable. - Easy Extensibility: If you need to add new types of data associated with players, you can easily extend the DataKey enum without changing the existing structure. ::: ### 5. Use of appropriate `env.storage` mechanisms There are three types of storage mechanisms in Stellar: #### [`env.storage().persistent()`](https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Storage.html#method.persistent) Persistent storage is used to store data that needs to be retained across contract invocations. Data stored in persistent storage is maintained between contract invocations and is accessible to all contract functions. Storage for data here is intended to stay in the ledger indefinitely until explicitly deleted. Expired entries can be restored but cannot be recreated. #### Use cases - Data requiring long-term persistence, such as token balances and user properties. - When data needs to be stored indefinitely and must survive even if it expires and needs restoration. - Examples: Token balances, user properties. #### Cost - Highest cost compared to others due to long-term persistence. #### Lifetime - Data behaves as if it were stored forever but can expire and be restored. ### [`env.storage().temporary()`](https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Storage.html#method.temporary) Temporary storage is used to store data that is only needed during the current contract invocation. Data stored in temporary storage is cleared at the end of the contract invocation and is not accessible to other contract functions. Storage for data here is done with a limited lifespan in the ledger. Entries will be removed after their lifetime ends and can be recreated with different values. #### Use cases - Data that only needs to exist temporarily, such as oracle data, claimable balances, and offers. - When data only needs to exist for a limited time and can be recreated if needed. - Examples: Oracle data, claimable balances, offers. #### Cost - Cheaper than persistent storage due to the limited lifespan of data. (Cheapest cost). #### Lifetime - Data exists for a predefined period and is then removed. ### [`env.storage().instance()`](https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Storage.html#method.instance) Storage here is done for a small amount of persistent data tightly coupled with the contract instance. Data is loaded from the ledger every time the contract instance is loaded and it is limited by the ledger entry size, typically in the order of 100 KB serialized. #### Use cases - Small data directly associated with the contract, such as admin details, configuration settings, and tokens. For small, frequently used data that is integral to the contract instance and benefits from being loaded every time the contract is used. - Examples: Contract admin details, configuration settings, tokens operated by the contract. #### Cost - Likely cheaper than storing data separately in persistent storage. #### Lifetime - Similar lifetime properties to persistent storage but does not appear in the ledger footprint. ### 5. Contract size The size of your smart contract's `wasm` binary influences the cost of running your smart contract on the Stellar network. Larger `wasm` binaries require more processing power and memory to execute, leading to higher gas costs. Larger binaries also cost more gas to deploy and invoke. To optimize the size of your `wasm` binary, you can: - Remove unnecessary code - Minimize the use of external dependencies - Use built-in tools to optimize the size of your `wasm` binary ## Other tips to optimize smart contract cost: ### 1. Use of built-in tools One way to optimize the size of your `wasm` binary is by using `--optimize` when building your contracts with the [Stellar CLI](../../../tools/cli/stellar-cli.mdx). This flag will optimize the size of your `wasm` binary by removing unnecessary code and reducing the size of the binary. ```bash stellar contract build --optimize ``` Another way to optimize your smart contract is to use the `stellar contract invoke` command with the `--cost` flag. This command will provide you with a detailed breakdown of the cost of running your smart contract on the Stellar network. ```bash stellar contract invoke \ --id CC6MWZMG2JPQEENRL7XVICAY5RNMHJ2OORMUHXKRDID6MNGXSSOJZLLF \ --source-account alice \ --network testnet \ --cost \ -- \ increment ``` Reference: [Stellar CLI Guides](../../../tools/cli/cookbook/README.mdx). ### 2. Manual code review Perform a manual code review of your smart contract to identify areas where you can optimize the code. Look for redundant loops, conditions, and storage operations that can be minimized or removed. ### 3. Unit testing with gas measurements Use unit tests to measure the gas cost of your smart contract functions. This will help you identify functions that are consuming a lot of gas and optimize them accordingly. Also, using [simulateTransaction](../../../data/apis/rpc/api-reference/methods/simulateTransaction.mdx) rpc helper method can give you an insight of the gas cost of your contract functions. ### 4. Static analysis tools Tools like [Clippy](https://doc.rust-lang.org/clippy) (part of the Rust compiler) can identify potential performance issues during the compilation stage. These tools can warn about: - Unnecessary allocations - Redundant code ### 5. Reconsidering storage locations #### State Variables - **Cost**: State variables are stored directly on the blockchain's storage. The cost is primarily influenced by the amount of data stored (measured in bytes) and the frequency of read and write operations. - **Considerations**: Writing data to state variables typically incurs higher gas costs compared to reading data. Complex data structures (e.g., arrays, mappings) or large amounts of data increase storage costs and gas consumption. Stellar charges gas fees for each byte of data stored and updated, making efficient data management crucial for cost optimization. #### Event Logs - **Cost**: Emitting events in Rust contracts does not incur direct storage costs since events are not permanently stored on the blockchain. Instead, they are included in transaction logs. - **Considerations**: Emitting events consumes gas, primarily due to the computational resources required to execute event emission and log generation. Events are useful for off-chain applications and event-driven architectures but do not contribute directly to on-chain storage costs. #### External Data Sources - **Cost**: Interacting with external data sources like oracles involves transaction fees for data retrieval and processing. - **Considerations**: Oracle calls incur gas costs for contract execution, which can vary depending on the complexity and frequency of data fetching. Contract developers should consider gas limits and optimize oracle interactions to minimize costs while ensuring reliable data integration. #### Immutable Data and Constants - **Cost**: Constants and immutable variables incur negligible storage costs since they are typically stored as part of the contract's bytecode or metadata. - **Considerations**: Constants and immutable data are crucial for contract configuration and parameterization but do not impact transaction or storage costs significantly. They are pre-defined during contract deployment and do not change during contract execution, avoiding additional gas costs for storage updates. #### Off-Chain Storage and IPFS - **Cost**: Storing data off-chain using solutions like IPFS avoids direct on-chain storage costs but incurs costs for data retrieval and IPFS network usage. - **Considerations**: Contracts store only the hash or reference to off-chain data on-chain, minimizing on-chain storage costs. Gas costs may apply when retrieving and processing off-chain data, depending on the complexity and frequency of access. Off-chain storage solutions offer scalability and flexibility but require careful consideration of network fees and data availability. --- ## Freighter Wallet [Freighter](https://www.freighter.app) is a browser extension wallet provided by the Stellar Development Foundation. It provides users a way to interact with Soroban tokens directly from the web browser. --- ## Connect to the Testnet 1. Install the Freighter [browser extension](https://www.freighter.app). 2. Create a keypair or import an existing account using a mnemonic phrase to complete setup. 3. Next, switch to Testnet. Testnet is available from the network dropdown. 4. If your account does not exist on the selected network, Freighter will prompt you to fund it the account using Friendbot. Alternatively, you can do so in the [Stellar Lab](https://lab.stellar.org/account/create). --- ## Enable Soroban tokens With a funded Stellar account, you can now add Soroban tokens to your Freighter wallet. 1. On the Freighter account screen, click this `Manage assets` button in the `...` options dropdown in the top left of the screen. 2. You will now see a button to `Add an asset` at the bottom of the screen. Click this `Add an asset` button. 3. On the next screen, enter the Token ID of the token you want to add to Freighter and click the `Add` button when it appears. Click `Confirm` to add it to your wallet. 4. You will now see your token's balance on Freighter's account page. Clicking on the balance will show a history of payments sent using this token. --- ## Integrate Freighter with a React dapp Wallets are an essential part of any dapp. They allow users to interact with the blockchain and sign transactions. In this section, you'll learn how to integrate the Freighter wallet into your React dapps. ### WalletData Component In the [example crowdfund dapp](https://github.com/stellar/soroban-example-dapp), the `WalletData` component plays a key role in wallet integration. Let's break down the code and understand its functionality: ```tsx title="/components/moleculres/wallet-data/index.tsx" export function WalletData() { const mounted = useIsMounted(); const account = useAccount(); return ( <> {mounted && account ? ( {account.displayName} ) : ( )} ); } ``` Here's a breakdown of the code: - The `mounted` variable is obtained using the [`useIsMounted` hook](https://github.com/stellar/soroban-example-dapp/blob/main/hooks/useIsMounted.ts), indicating whether the component is currently mounted or not. - The [`useAccount` hook](https://github.com/stellar/soroban-example-dapp/blob/main/hooks/useAccount.ts) is used to fetch the user's account data, and the `data` property is destructured from the result. - Conditional rendering is used to display different content based on the component's mount status and the availability of account data. - If the component is mounted and the account data is available, the user's wallet data is displayed. This includes the account's display name. - If the component is not mounted or the account data is not available, a [`ConnectButton` component](https://github.com/stellar/soroban-example-dapp/blob/main/components/atoms/connect-button/index.tsx) is rendered, allowing the user to connect with Freighter. --- ## Prompt Freighter to sign transactions as a JS dapp developer If you're building a JS dapp, easily sign Soroban transactions using the [Freighter browser extension](https://www.freighter.app) and its corresponding client library [@stellar/freighter-api](https://www.npmjs.com/package/@stellar/freighter-api): 1. Follow the setup instructions to [connect to the Testnet](./connect-testnet.mdx), if required during development. 2. Now, you can use the `signTransaction` [method](https://docs.freighter.app/docs/guide/usingFreighterWebApp) from `@stellar/freighter-api` in your dapp to sign Soroban XDRs using the account in Freighter. 3. Upon calling `signTransaction`, Freighter will open and prompt the user to sign the transaction. Approving the transaction will return an object containing the signed XDR to the requesting dapp. --- ## Send Soroban token payments Once you have added a Soroban token to your Freighter wallet, you can now send a payment of that token directly from Freighter. 1. On the Freighter account screen, click the `Send` icon in the upper part of the screen. 2. Enter a recipient public key. Click `Continue`. 3. Select your token from the asset list at the bottom of the screen and enter a token amount. Click `Continue`. 4. Enter a memo (optional). Click `Review Send`. 5. Review the details of your payment. Click `Send`. --- ## Sign authorization entries In order to take advantage of [contract authorization](../../../learn/fundamentals/contract-development/authorization.mdx), you can use Freighter's API to sign an authorization entry. A good example of how signing an authorization entry works can be found in the [`authorizeEntry` helper of `stellar-sdk`](https://github.com/stellar/js-stellar-base/blob/e3d6fc3351e7d242b374c7c6057668366364a279/src/auth.js#L97). Like in the helper, you can construct a [`HashIdPreimageSorobanAuthorization`](https://github.com/stellar/js-stellar-base/blob/a9567e5843760bfb6a8b786592046aee4c9d38b2/types/next.d.ts#L6895) and use the xdr representation of that structure to call `await freighterApi.signAuthEntry(preimageXdr)`. This call will return a `Buffer` of the signed hash of the `HashIdPreimageSorobanAuthorization` passed in, which can then be used to submit to the network during a contract authorization workflow. --- ## Sign Soroban XDRs With a funded Testnet account, you can now sign Soroban XDRs using dApps that are integrated with Freighter. An example of an integrated dApp is Stellar's [Lab](https://lab.stellar.org/transaction/sign?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;). 1. On the Lab's transaction signer, enter a Soroban XDR into the form field. 2. Click `Sign with Freighter`. 3. Freighter will open with the details of the XDR. Click `Approve` to sign or `Reject` to dismiss without a signature. 4. If approved, Freighter will transmit a signed XDR back to the Lab. --- ## RPC Using and interacting with the Stellar RPC is an important part of the smart contract development lifecycle. Read more about the RPC in our [RPC documentation](../../../data/apis/rpc/README.mdx). --- ## Generate ledger key parameters with a symbol key using the Python SDK In the [`increment` example contract] stores an integer value in a ledger entry that is identified by a key with the symbol `COUNTER`. The value of this ledger key can be derived using the following code snippets. ```python from stellar_sdk import xdr, scval, Address def get_ledger_key_symbol(contract_id: str, symbol_text: str) -> str: ledger_key = xdr.LedgerKey( type=xdr.LedgerEntryType.CONTRACT_DATA, contract_data=xdr.LedgerKeyContractData( contract=Address(contract_id).to_xdr_sc_address(), key=scval.to_symbol(symbol_text), durability=xdr.ContractDataDurability.PERSISTENT ), ) return ledger_key.to_xdr() print( get_ledger_key_symbol( "CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI", "COUNTER" ) ) ``` [`increment` example contract]: ../../smart-contracts/getting-started/storing-data.mdx --- ## Retrieve a contract code ledger entry using the JavaScript SDK When you deploy a contract, first the code is "installed" (i.e., it is uploaded onto the blockchain). This creates a `LedgerEntry` containing the Wasm byte-code, which is uniquely identified by its hash (that is, the hash of the uploaded code itself). Then, when the contract is "deployed," we create a `LedgerEntry` with a reference to that code's hash. So fetching the contract code is a two-step process: 1. First, we look up the contract itself, to see which code hash it is referencing. 2. Then, we can look up the raw Wasm byte-code using that hash. ```javascript function getLedgerKeyContractCode(contractId) { const instance = new Contract(contractId).getFootprint(); return instance.toXDR("base64"); } console.log( getLedgerKeyContractCode( "CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI", ), ); // OUTPUT: AAAABgAAAAGfjJVEBc55drW3U87N1Py0Rw0/nlqUA6tQ6r28khEl4gAAABQAAAAB ``` We then take our output from this function, and use it as the element in the `keys` array parameter in our call to the `getLedgerEntries` method. ```json { "jsonrpc": "2.0", "id": 8675309, "method": "getLedgerEntries", "params": { "keys": ["AAAABgAAAAGfjJVEBc55drW3U87N1Py0Rw0/nlqUA6tQ6r28khEl4gAAABQAAAAB"] } } ``` And the response we get contains the `LedgerEntryData` that can be used to find the `hash` we must use to request the Wasm byte-code. This hash is the `LedgerKey` that's been associated with the deployed contract code. ```json { "jsonrpc": "2.0", "id": 8675309, "result": { "entries": [ { "key": "AAAABgAAAAGfjJVEBc55drW3U87N1Py0Rw0/nlqUA6tQ6r28khEl4gAAABQAAAAB", "xdr": "AAAABgAAAAAAAAABn4yVRAXOeXa1t1POzdT8tEcNP55alAOrUOq9vJIRJeIAAAAUAAAAAQAAABMAAAAA5DNtbckOGVRsNVb8L7X/lIhAOy2o5G6GkLKXvc7W8foAAAAA", "lastModifiedLedgerSeq": 261603 } ], "latestLedger": 262322 } } ``` Now take the `xdr` field from the previous response's `result` object, and create a `LedgerKey` from the hash contained inside. ```javascript function getLedgerKeyWasmId(contractCodeLedgerEntryData) { const entry = xdr.LedgerEntryData.fromXDR( contractCodeLedgerEntryData, "base64", ); const wasmHash = entry .contractData() .val() .instance() .executable() .wasmHash(); let ledgerKey = xdr.LedgerKey.contractCode( new xdr.LedgerKeyContractCode({ hash: wasmHash, }), ); return ledgerKey.toXDR("base64"); } console.log( getLedgerKeyWasmId( "AAAABgAAAAAAAAABn4yVRAXOeXa1t1POzdT8tEcNP55alAOrUOq9vJIRJeIAAAAUAAAAAQAAABMAAAAA5DNtbckOGVRsNVb8L7X/lIhAOy2o5G6GkLKXvc7W8foAAAAA", ), ); // OUTPUT: AAAAB+QzbW3JDhlUbDVW/C+1/5SIQDstqORuhpCyl73O1vH6 ``` Now, finally we have a `LedgerKey` that correspond to the Wasm byte-code that has been deployed under the `ContractId` we started out with so very long ago. This `LedgerKey` can be used in a final request to the Stellar-RPC endpoint. ```json { "jsonrpc": "2.0", "id": 8675309, "method": "getLedgerEntries", "params": { "keys": ["AAAAB+QzbW3JDhlUbDVW/C+1/5SIQDstqORuhpCyl73O1vH6"] } } ``` And the response we get contains (even more) `LedgerEntryData` that we can decode and parse to get the actual, deployed, real-life contract byte-code. We'll leave that exercise up to you. You can check out what is contained using the ["View XDR" page of the Stellar Lab](https://lab.stellar.org/xdr/view?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;). ```json { "jsonrpc": "2.0", "id": 8675309, "result": { "entries": [ { "key": "AAAAB+QzbW3JDhlUbDVW/C+1/5SIQDstqORuhpCyl73O1vH6", "xdr": "AAAABwAAAADkM21tyQ4ZVGw1Vvwvtf+UiEA7LajkboaQspe9ztbx+gAAAkgAYXNtAQAAAAEVBGACfn4BfmADfn5+AX5gAAF+YAAAAhkEAWwBMAAAAWwBMQAAAWwBXwABAWwBOAAAAwUEAgMDAwUDAQAQBhkDfwFBgIDAAAt/AEGAgMAAC38AQYCAwAALBzUFBm1lbW9yeQIACWluY3JlbWVudAAEAV8ABwpfX2RhdGFfZW5kAwELX19oZWFwX2Jhc2UDAgqnAQSSAQIBfwF+QQAhAAJAAkACQEKOutCvhtQ5QgEQgICAgABCAVINAEKOutCvhtQ5QgEQgYCAgAAiAUL/AYNCBFINASABQiCIpyEACyAAQQFqIgBFDQFCjrrQr4bUOSAArUIghkIEhCIBQgEQgoCAgAAaQoSAgICgBkKEgICAwAwQg4CAgAAaIAEPCwAACxCFgICAAAALCQAQhoCAgAAACwQAAAALAgALAHMOY29udHJhY3RzcGVjdjAAAAAAAAAAQEluY3JlbWVudCBpbmNyZW1lbnRzIGFuIGludGVybmFsIGNvdW50ZXIsIGFuZCByZXR1cm5zIHRoZSB2YWx1ZS4AAAAJaW5jcmVtZW50AAAAAAAAAAAAAAEAAAAEAB4RY29udHJhY3RlbnZtZXRhdjAAAAAAAAAAFAAAAAAAbw5jb250cmFjdG1ldGF2MAAAAAAAAAAFcnN2ZXIAAAAAAAAGMS43Ni4wAAAAAAAAAAAACHJzc2RrdmVyAAAALzIwLjMuMSNiYTA0NWE1N2FmOTcxZmM4M2U0NzU3NDZiNTlhNTAzYjdlZjQxNjQ5AA==", "lastModifiedLedgerSeq": 368441, "liveUntilLedgerSeq": 2442040 } ], "latestLedger": 370940 } } ``` --- ## Retrieve a contract code ledger entry using the Python SDK When you deploy a contract, first the code is "installed" (i.e., it is uploaded onto the blockchain). This creates a `LedgerEntry` containing the Wasm byte-code, which is uniquely identified by its hash (that is, the hash of the uploaded code itself). Then, when the contract is "deployed," we create a `LedgerEntry` with a reference to that code's hash. So fetching the contract code is a two-step process: 1. First, we look up the contract itself, to see which code hash it is referencing. 2. Then, we can look up the raw Wasm byte-code using that hash. ```python from stellar_sdk import xdr, Address def get_ledger_key_contract_code(contract_id: str) -> str: ledger_key = xdr.LedgerKey( type=xdr.LedgerEntryType.CONTRACT_DATA, contract_data=xdr.LedgerKeyContractData( contract=Address(contract_id).to_xdr_sc_address(), key=xdr.SCVal(xdr.SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE), durability=xdr.ContractDataDurability.PERSISTENT ) ) return ledger_key.to_xdr() print( get_ledger_key_contract_code( "CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI" ) ) # OUTPUT: AAAABgAAAAGfjJVEBc55drW3U87N1Py0Rw0/nlqUA6tQ6r28khEl4gAAABQAAAAB ``` We then take our output from this function, and use it as the element in the `keys` array parameter in our call to the `getLedgerEntries` method. ```json { "jsonrpc": "2.0", "id": 8675309, "method": "getLedgerEntries", "params": { "keys": ["AAAABgAAAAGfjJVEBc55drW3U87N1Py0Rw0/nlqUA6tQ6r28khEl4gAAABQAAAAB"] } } ``` And the response we get contains the `LedgerEntryData` that can be used to find the `hash` we must use to request the Wasm byte-code. This hash is the `LedgerKey` that's been associated with the deployed contract code. ```json { "jsonrpc": "2.0", "id": 8675309, "result": { "entries": [ { "key": "AAAABgAAAAGfjJVEBc55drW3U87N1Py0Rw0/nlqUA6tQ6r28khEl4gAAABQAAAAB", "xdr": "AAAABgAAAAAAAAABn4yVRAXOeXa1t1POzdT8tEcNP55alAOrUOq9vJIRJeIAAAAUAAAAAQAAABMAAAAA5DNtbckOGVRsNVb8L7X/lIhAOy2o5G6GkLKXvc7W8foAAAAA", "lastModifiedLedgerSeq": 261603 } ], "latestLedger": 262322 } } ``` Now take the `xdr` field from the previous response's `result` object, and create a `LedgerKey` from the hash contained inside. ```python from stellar_sdk import xdr def get_ledger_key_wasm_id(contract_code_ledger_entry_data: str) -> str: # First, we dig the wasm_id hash out of the xdr we received from RPC contract_code_wasm_hash = xdr.LedgerEntryData.from_xdr( contract_code_ledger_entry_data ).contract_data.val.instance.executable.wasm_hash # Now, we can create the `LedgerKey` as we've done in previous examples ledger_key = xdr.LedgerKey( type=xdr.LedgerEntryType.CONTRACT_CODE, contract_code=xdr.LedgerKeyContractCode( hash=contract_code_wasm_hash ), ) return ledger_key.to_xdr() print( get_ledger_key_wasm_id( "AAAABgAAAAAAAAABn4yVRAXOeXa1t1POzdT8tEcNP55alAOrUOq9vJIRJeIAAAAUAAAAAQAAABMAAAAA5DNtbckOGVRsNVb8L7X/lIhAOy2o5G6GkLKXvc7W8foAAAAA" ) ) # OUTPUT: AAAAB+QzbW3JDhlUbDVW/C+1/5SIQDstqORuhpCyl73O1vH6 ``` Now, finally we have a `LedgerKey` that correspond to the Wasm byte-code that has been deployed under the `ContractId` we started out with so very long ago. This `LedgerKey` can be used in a final request to the Stellar-RPC endpoint. ```json { "jsonrpc": "2.0", "id": 8675309, "method": "getLedgerEntries", "params": { "keys": ["AAAAB+QzbW3JDhlUbDVW/C+1/5SIQDstqORuhpCyl73O1vH6"] } } ``` And the response we get contains (even more) `LedgerEntryData` that we can decode and parse to get the actual, deployed, real-life contract byte-code. We'll leave that exercise up to you. You can check out what is contained using the ["View XDR" page of the Stellar Lab](https://lab.stellar.org/xdr/view?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;). ```json { "jsonrpc": "2.0", "id": 8675309, "result": { "entries": [ { "key": "AAAAB+QzbW3JDhlUbDVW/C+1/5SIQDstqORuhpCyl73O1vH6", "xdr": "AAAABwAAAADkM21tyQ4ZVGw1Vvwvtf+UiEA7LajkboaQspe9ztbx+gAAAkgAYXNtAQAAAAEVBGACfn4BfmADfn5+AX5gAAF+YAAAAhkEAWwBMAAAAWwBMQAAAWwBXwABAWwBOAAAAwUEAgMDAwUDAQAQBhkDfwFBgIDAAAt/AEGAgMAAC38AQYCAwAALBzUFBm1lbW9yeQIACWluY3JlbWVudAAEAV8ABwpfX2RhdGFfZW5kAwELX19oZWFwX2Jhc2UDAgqnAQSSAQIBfwF+QQAhAAJAAkACQEKOutCvhtQ5QgEQgICAgABCAVINAEKOutCvhtQ5QgEQgYCAgAAiAUL/AYNCBFINASABQiCIpyEACyAAQQFqIgBFDQFCjrrQr4bUOSAArUIghkIEhCIBQgEQgoCAgAAaQoSAgICgBkKEgICAwAwQg4CAgAAaIAEPCwAACxCFgICAAAALCQAQhoCAgAAACwQAAAALAgALAHMOY29udHJhY3RzcGVjdjAAAAAAAAAAQEluY3JlbWVudCBpbmNyZW1lbnRzIGFuIGludGVybmFsIGNvdW50ZXIsIGFuZCByZXR1cm5zIHRoZSB2YWx1ZS4AAAAJaW5jcmVtZW50AAAAAAAAAAAAAAEAAAAEAB4RY29udHJhY3RlbnZtZXRhdjAAAAAAAAAAFAAAAAAAbw5jb250cmFjdG1ldGF2MAAAAAAAAAAFcnN2ZXIAAAAAAAAGMS43Ni4wAAAAAAAAAAAACHJzc2RrdmVyAAAALzIwLjMuMSNiYTA0NWE1N2FmOTcxZmM4M2U0NzU3NDZiNTlhNTAzYjdlZjQxNjQ5AA==", "lastModifiedLedgerSeq": 368441, "liveUntilLedgerSeq": 2442040 } ], "latestLedger": 370940 } } ``` --- ## Contract Storage Smart contract storage is available to affordably accommodate a wide range of uses. Learn more in the [state archival section](../../../learn/fundamentals/contract-development/storage/state-archival.mdx). --- ## How to choose the right storage type for your use case ## Storage types Smart contracts can persist data in the Stellar ledger using the storage interface (`env.storage()` in Soroban SDK). There are three types of storage available on the Stellar network: | Storage type | SDK API | Cost | Behavior when TTL expires | Number of keys | | --- | --- | --- | --- | --- | | Persistent | `env.storage().persistent()` | Most expensive | Data is archived | Unlimited | | Temporary | `env.storage().temporary()` | Less expensive | Data is removed forever | Unlimited | | Instance | `env.storage().instance()` | More expensive | Data is archived | Limited by entry size limit | Every storage type can be thought of as a map from arbitrary keys to arbitrary values. The data is physically stored in the Stellar ledger. Every storage 'map' is completely independent from every other storage 'map'. It is possible to store different data for the same key in every storage. For example, in temporary storage, key `123_u32` may have a value of `100_u32`, while in persistent storage the same `123_u32` key may have a value of `abcd`. All the storage types have almost exactly the same functionality, besides the differences highlighted in the table above, specifically cost, behavior when TTL expires, and limit for the number of keys stored per contract. ### A note on TTL If you're wondering what 'TTL' means, here is a quick intro on TTLs and state archival in Stellar. State archival is a special mechanism defined by the Stellar protocol that ensures that the active ledger state size doesn't grow indefinitely. In simple terms, every stored contract data entry, as well as contract code (Wasm) entry, has a certain 'time-to-live' (TTL) assigned. TTL is just a number of ledgers for which the entry is considered to be 'active', and after that number of ledgers, the entry is considered to have TTL expired and thus no longer active. Different storage types handle TTL expiration differently: data will either be moved to the archive ('cold', off-chain storage), or automatically removed from the ledger (in case of temporary storage). The data stored in the archive can be restored on-chain later and thus become active again. TTL also may be extended however many times are necessary, for a fee. Read more about state archival [here](../../../learn/fundamentals/contract-development/storage/state-archival.mdx). ## Persistent storage This storage type is used for storing data on the network over an indefinitely long time period. For persistent storage, when the TTL reaches zero, the entry is moved to the archival storage. It can then be restored when needed — usually [automatically](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#contract-data-automatic-restoration), by including the entry in a transaction's restore list (which transaction simulation typically populates for you), or manually using the `RestoreFootprintOp` operation. Persistent storage is the default storage type use-case-wise. Instance and temporary storage are only useful for the specific use cases described in the respective sections. Stellar protocol also uses persistent storage to store the contracts and their Wasm executables, so that they always can be accessed or restored. Examples of data that may be stored on persistent storage include user balances, token metadata, voting and governance decisions, and all data that need to either remain accessible forever, or until explicitly deleted. Let's look at a contract for a loyalty points system where users can accumulate points and redeem them for rewards. Each user's point balance will be stored in persistent storage. ```rust use soroban_sdk::{contractimpl, contracttype, Address, Env}; #[contracttype] pub enum DataKey { Points(Address), } #[contract] pub struct LoyaltyPointsContract; #[contractimpl] impl LoyaltyPointsContract { // This function redeems points according to the user's balance pub fn redeem_points(env: Env, user: Address, points: u64) -> bool { user.require_auth(); let key = DataKey::Points(user.clone()); let current_points: u64 = env.storage().persistent().get(&key).unwrap_or(0); if current_points >= points { let new_points = current_points - points; env.storage().persistent().set(&key, &new_points); true } else { false } } // This function retrieves the user's points balance pub fn get_points(env: Env, user: Address) -> u64 { let key = DataKey::Points(user); env.storage().persistent().get(&key).unwrap_or(0) } // ... // This example omits functions that add points to the users, see the next // section for these. } ``` ## Instance storage Instance storage is a small, limited-size map attached to the contract instance. It is physically stored in the same ledger entry as the contract itself and shares TTL with it. Like persistent storage, instance storage stores data permanently. However, it has a few pros and cons compared to it: - Pro: Data TTL is tied to the contract instance, thus making state archival behavior much easier - Pro/Con: Data is loaded automatically together with the contract, thus reducing the transaction footprint size, but increasing the number of bytes read every time the contract is loaded (_usually_ this still will result in a lower fee) - Con: The total size of all the keys and values in the instance storage is limited by the ledger entry size limit. See the current value of the limit [on the Stellar Lab](https://lab.stellar.org/network-limits). The total number of keys supported is on the order of tens to hundreds depending on the underlying data size and the current network limit. Keep in mind, that the network limit can never go down, so the instance can't ever become non-valid. Instance storage is best suited for data that has a well-known size limit and is either very small, or is necessary for every contract invocation. For example, contract administrator entry (small and should be maintained together with the contract instance), or a token pair for a liquidity pool (necessary for almost every operation on a liquidity pool). Let's look at additional functions for the loyalty points contract from the previous section, specifically the functions that define the contract admin and add points to the users: ```rust use soroban_sdk::{contractimpl, Address, Env}; #[contracttype] pub enum DataKey { Points(Address), Admin } #[contractimpl] impl LoyaltyPointsContract { // Initialize a contract with the administrator address. pub fn __constructor(env: Env, admin: Address) { env.storage().instance().set(&DataKey::Admin, &admin); } pub fn add_points(env: Env, user: Address, points: u64) { // Load the admin from the instance storage and make sure it has // authorized this invocation. let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); admin.require_auth(); let key = DataKey::Points(user.clone()); let current_points: u64 = env.storage().persistent().get(&key).unwrap_or(0); let new_points = current_points + points; env.storage().persistent().set(&key, &new_points); } } ``` ## Temporary storage As the name implies, temporary storage stores data only for a certain time period, and then discards it automatically when TTL expires. **Temporary entries are gone forever when their TTL expires**. The benefit of temporary storage is the smaller cost and the ability to set a very low TTL, both of which result in lower rent fees compared to persistent storage. See the current information on the minimal lifetimes and rent fees [on the Stellar Lab](https://lab.stellar.org/network-limits). Temporary storage is suitable for data that is only necessary for a relatively short and well-defined time period. 'Well-defined' here means that when creating an entry, the contract should be able to define its maximum necessary TTL. :::caution While TTL for the temporary data can be extended, it is unsafe to rely on the extensions to preserve data. There is always a risk of losing temporary data. Only the TTL extension made when the entry is created is guaranteed to happen. ::: :::caution It is also unsafe to rely on an entry expiring as it can be extended by anyone. When a time bound has to be enforced, always include it in the data as well. **Temporary storage is a cost optimization, it's not a mechanism of enforcing any time-based invariants.** ::: Temporary storage is best suited for easily replaceable data, or data that is only relevant within a certain time period. For example, oracle price feed data that is only relevant for a few minutes, or limited time authorizations such as token allowances, session tokens, auctions, timelocks, etc. Nonces for the Soroban signatures are also stored in the temporary storage, at least until the signature itself expires. Let's look at how temporary storage may be implemented in a contract that runs auctions periodically, and users can place bids that are only valid only until some user-defined time point: ```rust use soroban_sdk::{contracttype, contractimpl, Env, Address}; #[contracttype] pub enum DataKey { Bid(Address), } #[contracttype] pub struct Bid { value: i128, // The bid is no longer valid after this ledger sequence number. It's // important to store this value alongside the entry, as the entry may live // longer than expected. expiration_ledger_seq: u32, } #[contractimpl] impl AuctionContract { // This function lets a user place a bid that lives only until the auction ends. pub fn place_bid(env: Env, user: Address, bid: i128, bid_expiration_ledger_seq: u32) { user.require_auth(); let bid_key = DataKey::Bid(user.clone()); // Store the bid in the temporary storage. env.storage().temporary().set(&bid_key, &Bid { value: bid, expiration_ledger_seq: bid_expiration_ledger_seq, }); // Compute the TTL that the bid requires. let bid_ttl = bid_expiration_ledger_seq .checked_sub(e.ledger().sequence()) .unwrap(); // Extend the TTL for the bid, such that it's guaranteed to live at // least until the `bid_expiration_ledger_seq` that the user has // requested. This operation is will fail in // case if extension is longer than the protocol allows, so there is // no need to further validate `bid_ttl`. env.storage().temporary().extend_ttl(&bid_key, bid_ttl, bid_ttl); } // This function returns a user's bid (0 if it has expired). pub fn get_bid(env: Env, user: Symbol) -> i64 { let maybe_bid: Bid = env.storage().temporary().get(&DataKey::Bid(user)); if let Some(bid) = maybe_bid { if bid.expiration_ledger_seq <= e.ledger().sequence() { bid.value } else { // Even though the entry is still in the storage, it has // logically expired. Somebody must have extended the entry in // order to trick our contract, so return 0. 0 } } else { // There is no bid for the user - it either hasn't existed at all, // or has been removed from the temporary storage. In either case, // just return 0. 0 } } // ... // The functions that initialize and run the auctions are omitted. } ``` --- ## Migrate contract storage data when upgrading data structures When a contract is upgraded and a stored data structure gains new fields, the data already written to the ledger still uses the old layout. Naively reading those old entries with the new type causes the host to trap. This guide introduces the version marker pattern as the correct solution, covers lazy versus eager migration strategies and how to test them, and explains why the "intuitive" approach fails. ## Versioned Enum Pattern Suppose a contract stores `DataV1` entries and is upgraded to use `DataV2`, which adds an optional field `c`: ```rust #[contracttype] pub struct DataV1 { a: i64, b: i64 } #[contracttype] pub struct DataV2 { a: i64, b: i64, c: Option } ``` The recommended approach in this circumstance is to implement a versioned enum that can hold either a `V1` or `V2` data struct. ```rust #[contracttype] pub enum Data { V1(DataV1), V2(DataV2), } #[contracttype] pub enum DataKey { Data(u64), } ``` ### Migration Logic The migration logic enumerates the two data formats and converts `V1` data to `V2` format, and passes `V2` format through. If it's already `V1`, it maps fields `a` and `b` over and sets the new `c` field to `None` (the field that was added in `V2`). If it's already `V2`, it passes through unchanged. This is a lazy migration - old data is upgraded on read, not in a bulk migration. ```rust impl Data { pub fn into_v2(self) -> DataV2 { match self { Data::V1(v1) => DataV2 { a: v1.a, b: v1.b, c: None }, Data::V2(v2) => v2, } } } ``` ### Reading with version awareness The value is read from storage and then `into_v2()` ensures that the returned value is in the `V2` format. ```rust pub fn read_data(e: Env, id: u32) -> Option { let data_enum: Data = e.storage().persistent().get(&DataKey::Data(id))?; Some(data_enum.into_v2()) } ``` ### Writing always uses the current version The write function `write_data()` takes a data argument in the `DataV2` format. ```rust pub fn write_data(e: Env, id: u32, data: DataV2) { e.storage().persistent().set(&DataKey::Data(id), &Data::V2(data)); } ``` ### Testing migrations Testing data migration requires simulating state written by an old contract version and verifying that the new contract reads it correctly. In this test data in the `V1` format is first stored. Then it's read using the `read_data` function, which converts data in the `V1` format to V2 format with `into_v2()` before returning the result. The result is tested with `assert_eq!()`, and stored with the same `id` as it was stored with, which means the `V1` formatted data is overwritten with the same data in `V2` format. Then the data is read from storage to verify it's stored in the `V2` format, and finally the data is read using the `read_data()` function to verify that the data is also returned in the `V2` format by the read function. ```rust #[test] fn test_write_upgrades_v1_entry_to_v2_1() { let env = Env::default(); let id: u32 = 7; let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); // Inject a V1 entry directly, simulating legacy on-chain state. env.as_contract(&contract_id, || { env.storage() .persistent() .set(&DataKey::Data(id), &Data::V1(DataV1 { a: 5, b: 6 })); }); // Read it - into_v2() migrates lazily; c must be None. let migrated = client.read_data(&id).unwrap(); assert_eq!(migrated.a, 5); assert_eq!(migrated.b, 6); assert_eq!(migrated.c, None); // Write it back - write_data always stores Data::V2(...). client.write_data(&id, &migrated); // Confirm the stored enum variant is now V2, not V1. let stored: Data = env.as_contract(&contract_id, || { env.storage().persistent().get(&DataKey::Data(id)) }) .unwrap(); match stored { Data::V2(v2) => { assert_eq!(v2.a, 5); assert_eq!(v2.b, 6); assert_eq!(v2.c, None); } Data::V1(_) => panic!("expected Data::V2 after write_data, found Data::V1"), } // Subsequent reads go through the V2 branch and return identical values. let result = client.read_data(&id).unwrap(); assert_eq!(result.a, 5); assert_eq!(result.b, 6); assert_eq!(result.c, None); } ``` ## Version Marker Pattern An alternative solution is to store a version number alongside each data entry, keyed by the same identifier. The contract reads the version first, then branches on the result to decode the payload with the correct type. ### Key layout Define two variants in your key enum - one for the version marker and one for the payload - both keyed by the same `id`: ```rust #[contracttype] pub enum DataKey { DataVersion(u32), // version marker, keyed by id Data(u32), // data, keyed by the same id } ``` Each logical record occupies two storage slots. Because the version is stored per-record rather than globally, each entry is independently versioned. There is no all-or-nothing upgrade requirement. ### Reading with version awareness Before decoding a storage entry, read its version marker. Use `unwrap_or(1)` to handle entries that were written before versioning was introduced. The absence of a version key is itself a signal that the entry is version 1: ```rust fn read_data(env: &Env, id: u32) -> DataV2 { let version: u32 = env.storage().persistent() .get(&DataKey::DataVersion(id)) .unwrap_or(1); // default to v1 for entries without version marker match version { 1 => { let v1: DataV1 = env.storage().persistent().get(&DataKey::Data(id)).unwrap(); DataV2 { a: v1.a, b: v1.b, c: None } } _ => env.storage().persistent().get(&DataKey::Data(id)).unwrap(), } } ``` ### Writing always uses the current version Every write stamps the entry with the current version number. An entry that was originally `DataV1` will carry a `DataVersion` marker of `2` the next time it is written back: ```rust fn write_data(env: &Env, id: u32, data: &DataV2) { env.storage().persistent().set(&DataKey::DataVersion(id), &2u32); env.storage().persistent().set(&DataKey::Data(id), data); } ``` ### Lazy vs eager migration Once version-aware read/write logic is in place, there are two strategies for converting old entries. #### Lazy migration (convert on read) In lazy migration, old entries are left untouched on the ledger. When a record is read, its version is detected and it is up-converted in memory. When that record is later written back, it is stamped with the new version. No explicit migration step is needed - conversion happens as records are accessed in normal contract use. Lazy migration is generally preferred on blockchains. Leaving old entries untouched has no upfront cost and no risk of hitting instruction or ledger-entry limits at upgrade time. Records that are never accessed again are never migrated, which is usually acceptable. The `read_data` function shown above already implements lazy migration. Each time an old `DataV1` entry is read and then passed to `write_data`, the entry is silently upgraded in place. #### Eager migration (batch conversion) In eager migration, an explicit admin function iterates all known records and rewrites them in the new format immediately after the upgrade is deployed: ```rust pub fn migrate_all(env: &Env, ids: Vec) { // Caller should be an authorized admin. for id in ids.iter() { let version: u32 = env.storage().persistent() .get(&DataKey::DataVersion(id)) .unwrap_or(1); if version < 2 { // read_data up-converts to DataV2 in memory. let migrated = read_data(&env, id); // write_data stamps the entry as version 2. write_data(&env, id, &migrated); } } } ``` Eager migration is rarely practical for large datasets on Soroban. Each rewrite consumes fees and burns instructions, and a single transaction cannot migrate an unbounded number of records - the contract will hit instruction or ledger-entry limits. If the batch must span multiple transactions, the contract is in a mixed-version state throughout the window, which means version-aware read logic is still required anyway. Eager migration is occasionally appropriate when the total number of records is small and known in advance (for example, a fixed registry of a few dozen entries), or when you need to permanently drop old version branches from the read path. :::caution Never remove a version branch from `read_data` while old entries of that version can still exist on the ledger. Doing so will cause any remaining old entries to trap when accessed. ::: ### Testing migrations Testing data migration requires simulating state written by an old contract version and verifying that the new contract reads it correctly. The Soroban test environment allows you to set storage state directly. Use this to write `DataV1` entries (without a `DataVersion` key) and verify that `read_data` up-converts them correctly: ```rust #[cfg(test)] use super::*; use soroban_sdk::Env; #[test] fn test_reads_v1_entry_as_v2() { let env = Env::default(); let id: u32 = 42; let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); // Simulate what the old contract wrote: a DataV1 payload, // no DataVersion entry (old contracts did not write one). let v1_data = DataV1 { a: 10, b: 20 }; env.as_contract(&contract_id, || { env.storage().persistent().set(&DataKey::Data(id), &v1_data); }); let result = read_data(&env, id); assert_eq!(result.a, 10); assert_eq!(result.b, 20); assert_eq!(result.c, None); } #[test] fn test_reads_v2_entry_correctly() { let env = Env::default(); let id: u32 = 99; let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); let v2_data = DataV2 { a: 1, b: 2, c: Some(3) }; write_data(&env, id, &v2_data); let result = read_data(&env, id); assert_eq!(result.a, 1); assert_eq!(result.b, 2); assert_eq!(result.c, Some(3)); } #[test] fn test_write_upgrades_v1_entry_to_v2() { let env = Env::default(); let id: u32 = 7; let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); // Write a v1 entry directly, as the old contract would have. let v1_data = DataV1 { a: 5, b: 6 }; env.as_contract(&contract_id, || { env.storage().persistent().set(&DataKey::Data(id), &v1_data); }); // Read it - lazy migration produces a DataV2 in memory. let migrated = read_data(&env, id); assert_eq!(migrated.c, None); // Write it back - this stamps the entry as version 2. write_data(&env, id, &migrated); env.as_contract(&contract_id, || { let stored_version: u32 = env.storage().persistent() .get(&DataKey::DataVersion(id)) .unwrap(); }); assert_eq!(stored_version, 2); // Subsequent reads should take the v2 branch. let result = read_data(&env, id); assert_eq!(result.a, 5); assert_eq!(result.b, 6); assert_eq!(result.c, None); } ``` The three test cases cover the three states a record can be in after an upgrade: - A `DataV1` entry with no version marker (pre-versioning era records) - A `DataV2` entry written by the new contract - A `DataV1` entry that is read and then written back (the lazy migration round-trip) ## Why intuitive approaches fail The techniques presented here may not immediately seem necessary. The "apparent" obvious solutions may be to programmatically handle the discrepancies in data types, rather than modify any of the underlying data structures, or adjust how the storage entries are read or written. :::warning We've outlined a couple of the more "obvious" approaches to this problem, to illustrate _why_ these anti-patterns are not ideal. Please do not use the following code snippets as examples to be emulated. Rather, read the context of them, and learn why to avoid them. ::: ### Approach 1: Read old entries directly with the new type You may think the most natural approach is to read the stored bytes directly as `DataV2` and expect `c` to default to `None`: ```rust let key = DataKey::DataV2(1u32); // Reading a DataV1 entry with the DataV2 type. // A developer might expect c = None for old entries - but this traps. let data: DataV2 = env.storage().persistent().get(&key).unwrap(); // Error(Object, UnexpectedSize) ``` This traps with `Error(Object, UnexpectedSize)`. The Soroban host validates the field count of the XDR-encoded value against the type definition before returning anything to the contract. Because `DataV1` has two fields and `DataV2` has three, the host rejects the entry before the SDK can handle it. ### Approach 2: Use `try_from_val` as a fallback Another approach is to use `try_from_val` expecting to catch a deserialization error and recover: ```rust let raw: Val = env.storage().persistent().get(&key).unwrap(); if let Ok(v2) = DataV2::try_from_val(&env, &raw) { v2 } else { // This branch is never reached - the host traps before returning Err. let v1 = DataV1::try_from_val(&env, &raw).unwrap(); DataV2 { a: v1.a, b: v1.b, c: None } } ``` This also traps at the host level. The field count validation happens in the host environment during deserialization - it does not produce a Rust `Err` that the SDK can intercept. There is no way to catch or recover from the mismatch at the contract level. The root issue is that a contract cannot determine which type an existing storage entry was written as just by reading it. That information must be stored explicitly. --- ## Storage strategies in production contracts How experienced teams lay out contract state on Stellar — from a first counter to protocol-scale data layouts. The strategies below are ordered from simplest to most complex and grounded in current production contracts and official examples. They are not mutually exclusive: each one applies to a _piece_ of state, not to a whole contract, and production contracts routinely compose several — a single lending pool keeps its config in instance storage (Strategy 1), one entry per user position (Strategy 2), and a bounded reserve list (Strategy 6). Use the [decision path](#decision-path-composing-strategies) to pick a strategy per piece of state. :::info[Protocol and limits snapshot] **Checked:** July 20, 2026. **Protocol:** Stellar Protocol 27. Mainnet resource limits are network settings that validators can change, and the strategy list itself can grow with the ecosystem. Verify any limit quoted here — such as the 200 disk-read-entry and 200 write-entry caps per transaction — with `stellar network settings --network mainnet` or on [Stellar Laboratory](https://lab.stellar.org/network-limits). The current values are tabulated in the [appendix](#appendix-mainnet-limits). ::: ## Why storage is the hard part In Stellar smart contracts, state is not a free-form database. It is a set of **ledger entries**: independent key→value pairs. Keys and values can be any Soroban-serializable type — built-ins such as `Symbol`, numbers, `Address`, bytes, vectors, and maps, or `#[contracttype]` structs and enums. The serialized contract-data ledger key, including its fixed fields and user key, is capped at **250 bytes**. The entire serialized contract-data ledger entry is capped at **64 KiB**, so the value has less than 64 KiB available after entry overhead. Each transaction declares the entries it may read and write in its _footprint_. Entries also have a TTL (time-to-live, in ledgers); **rent** is charged when an entry is created or grows and when its TTL is extended. On expiry, persistent and instance entries are archived, while temporary entries are deleted permanently. That model has three consequences that drive everything below: - **You cannot iterate what you cannot name.** There is no "give me all keys starting with X". To enumerate, you build the index yourself. - **Reads and writes are limited per transaction.** Under the current Mainnet settings, a transaction can read up to 200 entries from disk and write up to 200 entries. - **State is not free forever.** Every byte kept alive has a recurring cost, and every entry needs a TTL management plan. ### The three storage tiers `env.storage()` gives you three APIs with the same interface but different _lifecycles_. Watch what happens to each tier's entry when its TTL runs out: In the animation, expired instance and persistent entries end **archived** — no longer accessible to transactions, but recoverable — while an expired temporary entry gets **deleted**. The two restore buttons simulate restoration of archived instance/persistent entries. On the real network, there are two ways to restore an archived entry: - **Automatic** (since protocol 23): simulate and submit an ordinary invocation that touches the archived key. The RPC server adds that key — plus the contract code entry, if it is archived too — to the transaction's **restore list**, and the network restores everything on the list before the contract executes. - **Manual**: submit a `RestoreFootprintOp` naming the exact ledger keys; the CLI wraps this as `stellar contract restore`. Either way, restoration charges rent and resource fees, and the restored entry comes back at the minimum persistent TTL. Expiry and restoration are only part of the story. Side by side, here is how the three tiers compare across their whole lifecycle — where each one lives, what it costs, and roughly what it is good for: | | `instance()` | `persistent()` | `temporary()` | | --- | --- | --- | --- | | Lives in | the contract instance's own entry | its own entry per key | its own entry per key | | On TTL expiry | the single instance entry is archived; contract code has its own TTL and may be archived separately | only that key's entry is archived | **deleted forever** | | Automatic restoration | simulate and submit an invocation; RPC adds the archived instance and contract code, if needed, to the restore list | simulate and submit an invocation that accesses the key; RPC adds that exact key to the restore list | impossible | | Manual restoration | submit `RestoreFootprintOp` for the instance and code ledger keys; `stellar contract restore --id C...` restores the instance | submit `RestoreFootprintOp` for that key; `stellar contract restore --id C... --key ...` wraps it | impossible | | TTL extension | one instance call covers all instance data and also extends contract code | each key is extended separately | each key is extended separately | | Rent | full rate on the whole instance entry | full rate per entry | **half** rate per entry | | Loaded when | **every invocation of this contract** | only when in the footprint | only when in the footprint | | Right for | small, global, contract-lifetime config | data you must never lose | data with a natural deadline, or safely regenerable | Five subtleties that bite newcomers: - **Nothing extends a TTL automatically.** The host never bumps a TTL on access — on any tier. Every extension is an explicit `extend_ttl()` call by the contract (or a transaction operation). What looks automatic in well-run contracts is the bump-on-access pattern (see Strategy 5), applied to instance and persistent entries alike. - **Anyone can extend any entry's TTL.** `ExtendFootprintTTLOp` has no access control, so expiry is never a security boundary: if your logic assumes an authorization lapses when its entry expires, a bad actor can keep that entry alive indefinitely. A deadline your contract must enforce belongs in the entry's _value_, checked in code — the TTL only manages storage lifecycle (Strategy 4). - **Instance storage is one entry.** Everything in `instance()` shares the contract instance's single ledger entry: it is all loaded on every invocation, it must fit the 64 KiB cap together, and any two transactions that write it run sequentially (the network parallelizes only non-conflicting transactions). Keep it small and mostly read-only. TTLs are all-at-once too: `instance().extend_ttl()` extends everything together, and applies the same policy to the separate contract code entry with an independent threshold check. - **Temporary is not "scratch space".** It is durable across transactions until its TTL runs out — but expiry _permanently deletes_ it: no archive, no recovery. Use it only for data whose loss is acceptable or whose validity provably ends at a known ledger. In exchange it rents at half price and never needs restoring. - **Restoration is driven by the transaction, not by contract code.** An archived key that is in the footprint but not in the restore list fails the transaction before the contract runs — contract code never observes, and cannot restore, an archived entry. Instance storage restores as one contract instance entry (contract code separately, if also archived); persistent entries restore per key. That is the machinery every strategy below manages. The rest of this guide walks through the twelve strategies themselves, from simplest to most involved, each in the same shape: - **You need** — the situation you are in. - **The catch** — why the obvious approach hurts. - **The pattern** — the working code. - **Trade-offs** — what it costs. The italic line under each heading is a quick-facts summary; the storage tiers it names are where production contracts most commonly apply the pattern, **not a requirement** — most patterns work on any tier whose lifecycle fits the data. ## Strategy 1: Singleton config in instance storage _instance · O(1) · no separate footprint entry_ **You need.** A handful of values every call uses: admin address, token addresses, a pause flag, pool reserves, a counter. **The catch.** Nearly every call needs these values. If each lived in its own persistent entry, every call would declare and read several entries and manage several TTLs. Instance values add no separate footprint entries, although their bytes still contribute to resource use. **The pattern.** Put them in `instance()` storage under unit-like enum keys, and extend the instance TTL as part of normal operation: ```rust #[contracttype] pub enum DataKey { Admin, Token0, Token1, Reserve0, Reserve1 } pub fn __constructor(e: Env, admin: Address) { e.storage().instance().set(&DataKey::Admin, &admin); } // contract paths that maintain instance liveness end with the extend_ttl call: pub fn deposit(e: Env, from: Address, amount: i128) { // ...business logic that relies on instance state... e.storage().instance().extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); } ``` **Trade-offs** - O(1), with no separate footprint entry for each field. - Everything in it is loaded on every invocation of the contract and shares the 64 KiB cap. A small signer set is reasonable; a user map is not. - Instance writes serialize concurrent transactions. Hot mutable data with many independent writers (per-user balances) does **not** belong here — AMM reserves are acceptable because every swap conflicts logically anyway. **In the wild** - [`stellar/soroban-examples`](https://github.com/stellar/soroban-examples) — [`liquidity_pool/src/lib.rs`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/liquidity_pool/src/lib.rs#L10-L26) stores tokens, reserves, and total shares in instance storage; [`account/src/lib.rs`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/account/src/lib.rs#L27-L53) stores its signer entries and signer count there. - [`soroswap/core`](https://github.com/soroswap/core) — [`contracts/pair/src/storage.rs`](https://github.com/soroswap/core/blob/bb90a65556d8eee0dc698ac75de0f280e547fedc/contracts/pair/src/storage.rs#L7-L40) keeps `Token0/Token1/Reserve0/Reserve1/Factory/KLast` in instance storage: every swap touches reserves, so co-locating them minimizes footprint. - [`blend-capital/blend-contracts-v2`](https://github.com/blend-capital/blend-contracts-v2) — [`pool/src/storage.rs`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L208-L223): admin, backstop, config under `Symbol` keys. - [`OpenZeppelin/stellar-contracts`](https://github.com/OpenZeppelin/stellar-contracts) — fungible-token metadata in [`packages/tokens/src/fungible/storage.rs`](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/tokens/src/fungible/storage.rs#L33-L126) and the ownable `Owner` key in [`packages/access/src/ownable/storage.rs`](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/access/src/ownable/storage.rs#L13-L51) use instance storage. ## Strategy 2: One entry per entity — keyed by a `DataKey` enum _persistent · O(1) lookup · independently addressed entries · no enumeration_ **You need.** Per-user (or per-asset, per-order, per-anything) state: balances, positions, configs. Unbounded population. **The catch.** A naive `Map` under one key hits three walls: the 64 KiB entry cap; the write-bytes budget, because every update rewrites the map; and contention, because every user's transaction writes the same entry. **The pattern.** Give every entity its own ledger entry, using an enum variant with a parameter as the key. This is Soroban's equivalent of Solidity's `mapping`: ```rust #[contracttype] pub enum DataKey { Balance(Address), // one persistent entry per holder } pub fn read_balance(e: &Env, addr: Address) -> i128 { let key = DataKey::Balance(addr); if let Some(balance) = e.storage().persistent().get::<_, i128>(&key) { e.storage().persistent().extend_ttl(&key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT); balance } else { 0 // absent entry == zero; never write zeros you don't need } } ``` Note the two idioms: **absent-means-default** (don't create entries to store zero) and **bump-on-access** (see Strategy 5). **Trade-offs** - O(1) lookup and update; different users' entries do not conflict with each other, although other shared entries in those transactions still can. - You lose enumeration entirely — there is no way to list all `Balance(..)` keys on-chain. If you need "all holders", add an index (Strategy 8) or index events off-chain. - Aggregate rent grows with the population — unbounded users mean an unbounded total — but per-interaction cost stays flat. Rent falls on the transaction's fee payer when an entry is created, enlarged, restored, or extended, so with bump-on-access each user funds their own entry. **In the wild** - [`stellar/soroban-examples`](https://github.com/stellar/soroban-examples) — [`token/src/balance.rs`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/token/src/balance.rs#L4-L22), the canonical per-holder persistent balance layout; [`liquidity_pool`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/liquidity_pool/src/lib.rs#L10-L62) (`Shares(Address)`). - [`OpenZeppelin/stellar-contracts`](https://github.com/OpenZeppelin/stellar-contracts) — fungible [`Balance(Address)`](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/tokens/src/fungible/storage.rs#L34-L60) entries are persistent and extended explicitly; the smart account keeps [one persistent entry per signer](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/accounts/src/smart_account/storage.rs#L36-L41) (`SignerData(u32)`, deduplicated through a `SignerLookup` hash entry) and [extends the TTL on every read](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/accounts/src/smart_account/storage.rs#L1421-L1432). - [`blend-capital/blend-contracts-v2`](https://github.com/blend-capital/blend-contracts-v2) — one persistent [`Positions(Address)`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L176-L196) entry per user and separate [`ResConfig(Address)`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L338-L370) and [`ResData(Address)`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L432-L456) entries per reserve. ## Strategy 3: Composite keys — multi-dimensional lookups _persistent · temporary · O(1) · serialized ledger key ≤ 250 B_ **You need.** State indexed by _two or more_ dimensions: allowance of `(owner, spender)`, position of `(pool, user)`, mint quota of `(contract, minter, epoch)`. **The catch.** Same as Strategy 2 — nested maps in one entry don't scale — plus a new constraint: the **250-byte serialized ledger-key cap**. That size includes the contract ID, durability, and XDR-serialized user key, so the user key has less than 250 bytes available. Two addresses plus a few integers fit easily, but never put unbounded strings or vectors in a key. **The pattern.** Put a struct (or tuple variant) inside the key: ```rust #[contracttype] pub struct AllowanceDataKey { pub from: Address, pub spender: Address } #[contracttype] pub enum DataKey { Allowance(AllowanceDataKey) } ``` A positional tuple variant works just as well: ```rust #[contracttype] pub enum DataKey { Allowance(Address, Address) } // (from, spender) ``` The difference is size versus safety: the tuple form is smaller (120 B vs 160 B for this key — meaningful under the 250 B cap), while named fields won't let you accidentally swap `from` and `spender` when building the key. **Variation — time-bucketed keys.** When one dimension is time, put the bucket in the key so data self-partitions: each epoch writes a new key, and old buckets expire on their own (the lifecycle half is Strategy 4). With no enumeration, a bucket is readable only if you can recompute its key — so bucket by time only when old data is disposable or its keys are derivable. The [`mint-lock`](https://github.com/stellar/soroban-examples/tree/14c069cccf85e552d4d80c8122e00a61a5723aae/mint-lock) example implements this end-to-end: - [`StorageKey::MinterStats(contract, minter, epoch_length, epoch)`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/mint-lock/src/lib.rs#L28-L31) — `epoch_length` sits in the key, so a config change does not orphan old buckets. - [`mint()`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/mint-lock/src/lib.rs#L134-L163) — derives the epoch from the ledger sequence (`sequence / epoch_length`), tracks consumption under that key, and extends the temporary entry only to the end of its epoch. **Trade-offs** - Still O(1) per lookup; each combination is an independent entry for conflict analysis, although shared entries in the same transactions may still overlap. - Key design is API design: you can only look up by the _exact_ full key. If you also need "all spenders for an owner", that's an enumeration problem (Strategy 8). - More dimensions create more entries, increasing aggregate rent and resource use. **In the wild** - [`stellar/soroban-examples`](https://github.com/stellar/soroban-examples) — [`token/src/allowance.rs`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/token/src/allowance.rs#L5-L6) ([`(from, spender)`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/token/src/storage_types.rs#L12-L15)), [`mint-lock`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/mint-lock/src/lib.rs#L27-L28) (epoch-bucketed minter stats). - [`blend-capital/blend-contracts-v2`](https://github.com/blend-capital/blend-contracts-v2) — [`UserBalance(PoolUserKey{pool,user})`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/backstop/src/storage.rs#L69-L77), [`UserEmis(UserReserveKey{user,reserve_id})`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L112-L138), and [auctions keyed by user plus auction type](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L119-L140). - [`soroswap/core`](https://github.com/soroswap/core) — [`PairAddressesByTokens(Pair(Address, Address))`](https://github.com/soroswap/core/blob/bb90a65556d8eee0dc698ac75de0f280e547fedc/contracts/factory/src/storage.rs#L15) with the [sorted token tuple](https://github.com/soroswap/core/blob/bb90a65556d8eee0dc698ac75de0f280e547fedc/contracts/factory/src/pair.rs#L21-L27) → O(1) pair lookup. - [`OpenZeppelin/stellar-contracts`](https://github.com/OpenZeppelin/stellar-contracts) — access-control membership uses [`HasRole(Address, Symbol)`](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/access/src/access_control/storage.rs#L23). ## Strategy 4: Temporary storage for data with a deadline _temporary · half rent · deleted on TTL expiry_ **You need.** State that is only meaningful until some ledger: allowances with expiry, live auctions, oracle prices, per-epoch quotas, pending admin proposals. **The catch.** Rent and lifecycle. Park ephemeral data in persistent storage and you pay full-rate rent while it is live; when its TTL expires, it archives rather than being deleted. Removing it explicitly costs a write. **The pattern.** Use `temporary()` storage and align the TTL with the business deadline — then _enforce that deadline in the value_: ```rust // SEP-41 token allowance (SEP-41 is Soroban's standard token interface) // Write side: reject deadlines already in the past, then match the TTL to it. if amount > 0 && live_until_ledger < e.ledger().sequence() { panic!("live_until_ledger is less than ledger seq when amount > 0"); } e.storage().temporary().set(&key, &AllowanceValue { amount, live_until_ledger }); if amount > 0 { let live_for = live_until_ledger.checked_sub(e.ledger().sequence()).unwrap(); e.storage().temporary().extend_ttl(&key, live_for, live_for); } // Read side: anyone can extend the entry's TTL, so a live entry can outlast // the deadline — a past live_until_ledger means no allowance. let allowance = match e.storage().temporary().get::<_, AllowanceValue>(&key) { Some(a) if a.live_until_ledger >= e.ledger().sequence() => a.amount, _ => 0, }; ``` The TTL alone is not the whole guarantee, for two reasons: the network enforces a **minimum TTL on creation** (currently 17,280 ledgers, about a day, for temporary entries), and anyone can extend any entry's TTL with an `ExtendFootprintTTLOp`. So defensive code **also stores the deadline in the value and checks it** — as above, `live_until_ledger` lives inside `AllowanceValue`. The network guarantees deletion once the TTL runs out; your code enforces validity until then. **Trade-offs** - Half-price rent; entries are deleted on TTL expiry without a cleanup transaction. - Deleted means deleted — never put funds-bearing or must-not-lose state here. If nobody extends the entry and it expires early, your code must treat "absent" identically to "expired". - Minimum TTL is about 1 day — shorter deadlines must be enforced by the value check, not the TTL. **In the wild** - [`stellar/soroban-examples`](https://github.com/stellar/soroban-examples) — [`token/src/allowance.rs`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/token/src/allowance.rs#L4-L47), the canonical example; [`mint-lock`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/mint-lock/src/lib.rs#L153-L163) epoch stats extended to the end of each epoch. - [`reflector-network/reflector-contract`](https://github.com/reflector-network/reflector-contract) — [`oracle/src/prices.rs`](https://github.com/reflector-network/reflector-contract/blob/4c6368f5d66ae848adb9cfa2591198b54c4db6e1/oracle/src/prices.rs#L168-L188) stores price updates in temporary entries keyed by timestamp. Its TTL is [derived from the configured retention period, a five-second-ledger estimate, and a 2× safety factor](https://github.com/reflector-network/reflector-contract/blob/4c6368f5d66ae848adb9cfa2591198b54c4db6e1/oracle/src/prices.rs#L183-L184); the instance cache is a bounded `Vec`. - [`blend-capital/blend-contracts-v2`](https://github.com/blend-capital/blend-contracts-v2) — [auctions](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L637-L647), [queued reserve configs](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L401-L408), and `ProposedAdmin` use temporary storage; [`ProposedAdmin` is initialized with a 10-day TTL](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L239-L247). - [`OpenZeppelin/stellar-contracts`](https://github.com/OpenZeppelin/stellar-contracts) — [fungible allowances](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/tokens/src/fungible/storage.rs#L18-L20) and pending role or ownership transfers use temporary storage and enforce a [stored `live_until_ledger`](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/access/src/role_transfer/storage.rs#L132-L134). ## Strategy 5: TTL management — bump-on-access _persistent · instance · rent tracks usage_ **You need.** Persistent data that must not archive while in use — without paying maximum rent on everything forever. **The catch.** Every persistent entry archives when its TTL expires. A later transaction can restore it by including it in the restore list, paying restoration rent and resource fees. Extending a TTL costs rent proportional to entry size and the ledgers added, so bumping everything to the maximum on every touch is wasteful, while never bumping guarantees eventual archival. The current maximum is 3,110,400 ledgers — approximately 180 days at today's ~5-second ledger close time. **The pattern.** A common policy uses two constants: when code explicitly calls `extend_ttl()` and fewer than `THRESHOLD` ledgers remain, extend the entry to `BUMP`. Contracts often call it after successful reads and writes: ```rust pub(crate) const DAY_IN_LEDGERS: u32 = 17280; pub(crate) const BALANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; // target ~30 days at the current close time pub(crate) const BALANCE_LIFETIME_THRESHOLD: u32 = BALANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; // when < ~29 days remain at the current close time pub fn balance(e: &Env, addr: Address) -> i128 { let key = DataKey::Balance(addr); if let Some(balance) = e.storage().persistent().get::<_, i128>(&key) { // reading counts as activity: extend the TTL once below the threshold e.storage().persistent().extend_ttl(&key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT); balance } else { 0 } } ``` `extend_ttl(threshold, extend_to)` changes nothing when the current TTL is at least `threshold`; otherwise it extends the TTL to `extend_to`. With `THRESHOLD = BUMP − 17,280`, repeated calls can extend an active entry at most once per 17,280 ledgers. Examples from current source: | Codebase | Data class | Threshold → Bump | | --- | --- | --: | | `stellar/soroban-examples` token | instance / balances | 6 → 7 d / 29 → 30 d | | `soroswap/core` | instance / pair registry / LP balances | 29 → 30 / 59 → 60 / 119 → 120 d | | `blend-capital/blend-contracts-v2` | instance / shared protocol / user data | 30 → 31 / 45 → 46 / **100 → 120 d** | Blend uses longer TTL targets for user data than for shared entries: user positions target 120 days' worth of ledgers, while shared reserve data targets 46. These day labels remain approximations based on 17,280 ledgers per day. The same threshold → bump call exists on temporary storage and suits regenerable caches that should stay live _while hot_ — but a temporary entry that misses its bump is deleted permanently rather than archived, so never rely on it for data you cannot rebuild. **Trade-offs** - Bump-on-access ties extensions to activity. The fee-paying account pays the extension rent; idle persistent data eventually archives. - It cannot keep alive what nobody touches. For data that must stay live through total inactivity, an off-chain keeper can submit `ExtendFootprintTTLOp`; otherwise, a future transaction must include an archived entry in its restore list. - An `extend_ttl()` call is an explicit resource operation even when it is placed in a getter; rent is charged when it actually extends the TTL. Simulate the final transaction so its resource declaration and fees include the call. **In the wild.** Every contract with instance or persistent state deals with TTL management in some capacity. Three examples: - [`stellar/soroban-examples`](https://github.com/stellar/soroban-examples) — the token example's [`balance.rs`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/token/src/balance.rs#L4-L14) explicitly extends balance TTLs. - [`blend-capital/blend-contracts-v2`](https://github.com/blend-capital/blend-contracts-v2) — [`get_persistent_default()`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/backstop/src/storage.rs#L97-L112) combines reads with explicit extensions. - [`soroswap/core`](https://github.com/soroswap/core) — [`get_persistent_extend_or_error()`](https://github.com/soroswap/core/blob/bb90a65556d8eee0dc698ac75de0f280e547fedc/contracts/factory/src/storage.rs#L36-L49) reads and extends in one helper. ## Strategy 6: Bounded collections — a capped Vec or Map in one entry _persistent · 1 read to iterate · hard cap in code_ **You need.** A small list the _whole contract_ shares and iterates: the assets a lending pool supports, a reward-zone registry, a withdrawal queue. **The catch.** Iterating N separate entries costs N footprint entries and N reads per transaction. An unbounded `Vec` in one entry eventually hits the 64 KiB entry limit, and every writer of the list conflicts with every other. **The pattern.** Keep the collection in one entry **and enforce a hard cap in code**, chosen so the entry stays small and iteration stays cheap: ```rust pub const MAX_RESERVES: u32 = 30; // Blend pool reserve-list cap pub fn push_res_list(e: &Env, asset: &Address) -> u32 { let mut res_list = get_res_list(e); if res_list.len() >= MAX_RESERVES { panic_with_error!(e, PoolError::BadRequest) } let new_index = res_list.len(); res_list.push_back(asset.clone()); e.storage().persistent().set(&Symbol::new(e, RES_LIST_KEY), &res_list); // + extend_ttl new_index } ``` The list entry stores only _identifiers_; per-item detail lives in its own entry (Strategies 2 & 3). Reading "the whole configuration" is then 1 + N reads where N ≤ cap. **Trade-offs** - One read fetches the whole list → O(n) in-memory iteration, no footprint explosion. An in-memory push is O(1), but storing it reserializes and rewrites the whole list entry. - The cap is a **product decision** disguised as an engineering one: Blend pools reject a reserve list beyond 30 entries. Pick caps deliberately from encoded entry size and worst-case iteration cost. - Single-entry writes serialize; fine for admin-touched lists, wrong for user-touched ones. - Removal from an ordered vector is O(n) because later items shift. For O(1) removal where order does not matter, use swap-and-pop (Strategy 8). **In the wild** - [`blend-capital/blend-contracts-v2`](https://github.com/blend-capital/blend-contracts-v2) — the pool stores reserve identifiers in one persistent [`ResList`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L480-L495) entry and [rejects additions beyond 30 reserves](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/constants.rs#L16). Reserve details remain in per-asset entries. ## Strategy 7: Pack vs. split — group state by access pattern _persistent · size the entry to the transaction · split hot from cold_ **You need.** An entity with several related pieces of state — say a user's collateral, debt, and supplied assets — and a decision: one entry holding all of it, or one entry per piece? **The catch.** This is a two-sided trap. **Split too much** and one operation consumes many footprint entries and reads. **Pack too much** and each small change re-serializes a large value, approaches the 64 KiB entry cap, and prevents independent writes from running in parallel. **The pattern.** Group by how transactions use the data: pieces that are nearly always read and written together may belong in one entry; pieces updated independently or by different actors belong apart. Both layouts are transactionally atomic. Packing reduces footprint; it is not required for atomicity. Blend's `Positions` entry is the canonical "pack" example — every borrow, repay, and liquidation must see _all_ of a user's positions to check account health, so they share one entry: ```rust #[contracttype] pub struct Positions { // ONE persistent entry per user pub liabilities: Map, // reserve_index -> dTokens pub collateral: Map, pub supply: Map, } ``` Two details keep Blend's packed `Positions` entry small: 1. The maps key by reserve index (`u32`) rather than asset `Address` — a smaller map key that doubles as a pointer into the pool's bounded reserve list from Strategy 6. 2. The entry is explicitly capped: [`require_under_max`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/pool/pool.rs#L93-L98) rejects any action that would grow a user's count of liability plus collateral positions past the pool's [`max_positions`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L26-L32) config — Strategy 6's hard cap applied _inside_ an entry. Blend's reserves show the split side of the same trade-off: each asset's state is divided into `ResConfig` (admin-set parameters, cold) and `ResData` (rates updated on every accrual, hot), so a rate update writes the small hot entry without rewriting the config. **Trade-offs** | | Packed (1 entry) | Split (n entries) | | --- | --- | --- | | Read whole entity | 1 read | n reads | | Update one field | rewrite whole blob | 1 small write | | Invariant scope | one blob | contract coordinates across entries | | Parallelism between parts | none | possible when transactions do not overlap | | Size ceiling | 64 KiB for the whole entry | 64 KiB per individual entry | **In the wild** - Packed: Blend stores a user's liabilities, collateral, and supply maps in one [`Positions(Address)`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/pool/user.rs#L11-L14) entry; backstop shares and queued withdrawals share one [`UserBalance`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/backstop/src/backstop/user.rs#L19-L21) entry; OZ's smart account packs a rule's metadata, signer IDs, and policy IDs into one [`ContextRuleEntry`](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/accounts/src/smart_account/storage.rs#L56-L71), cutting auth-check reads from three to one. - Split: Blend [`ResConfig`/`ResData`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L126-L139) pairs; OZ fungible [`Balance` vs `Allowance` vs `Meta`](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/tokens/src/fungible/storage.rs#L33-L37); every token's balance-per-entry layout. ## Strategy 8: Enumeration — build the index yourself _persistent · O(1) maintain · pageable reads_ **You need.** To _list_ things on-chain: all trading pairs a factory created, all tokens an address owns, all members of a role. (Strategy 2 deliberately gave this up.) **The catch.** No key iteration exists. An unbounded `Vec` eventually exceeds the 64 KiB entry cap, and O(n) rewrites eventually exceed transaction budgets. Use an index that is O(1) to maintain and pageable to read. **The pattern — three variants by mutability.** **(a) Append-only: counter + indexed keys.** For sets that only grow, store `Total` and one entry per index: ```rust // soroswap factory enum DataKey { TotalPairs, // instance: u32 PairAddressesNIndexed(u32), // persistent: index -> pair address PairAddressesByTokens(Pair), // persistent: (tokenA,tokenB) -> pair address } ``` Appending = write index `n`, bump counter. Enumeration = clients loop `all_pairs(i)` for `i in 0..total` — **pagination is pushed to the caller**, which is the point: no single transaction ever needs the whole set. Note Soroswap maintains _two_ indexes over the same data — direct lookup by tokens _and_ positional enumeration. **(b) Mutable set: double mapping + swap-and-pop.** When items can also be _removed_, maintain two mappings — forward (`index → item`) and reverse (`item → index`) — so removal is O(1), as animated above: ```rust // OpenZeppelin enumerable NFT, abridged if to_be_removed_index != last_token_index { let last_token_id = get_owner_token_id(e, owner, last_token_index); set(OwnerTokens(owner, to_be_removed_index), last_token_id); // move last into hole set(OwnerTokensIndex(last_token_id), to_be_removed_index); // fix reverse pointer } remove(OwnerTokens(owner, last_token_index)); // pop tail remove(OwnerTokensIndex(to_be_removed_id)); // drop removed item's reverse entry ``` **(c) Zero on-chain index: events + off-chain indexer.** If only off-chain consumers need the listing, emit events and let an indexer (RPC `getEvents`, Hubble, etc.) build the list. On-chain cost: zero entries. On-chain readability: none. **Trade-offs** | Variant | add | remove | contains | on-chain page read | keeps order | ledger entries per item | | --- | --- | --- | --- | --- | --- | --- | | (a) counter + index | O(1) | ✗ (or tombstone) | via optional 2nd index | O(page) | yes | 1 (2 with the lookup index) | | (b) double map + swap-pop | O(1) | O(1) | O(1) | O(page) | no | 2 (forward + reverse) | | (c) events only | O(1) | O(1) | ✗ | ✗ | n/a | 0 | O(1) counts steps, not ledger I/O: the swap-and-pop above writes four entries for a non-tail removal (two for a tail removal), and every entry touched counts toward the per-transaction read and write limits. The **ledger entries per item** column is the rent side — the double map keeps two entries alive per item, roughly 2× the storage of Strategy 2. These write and rent costs are the reason to index only what the _contract itself_ needs to list on-chain; when only off-chain consumers read the list, variant (c) is enough. **In the wild** - (a) [`soroswap/core`](https://github.com/soroswap/core) — [`contracts/factory/src/storage.rs`](https://github.com/soroswap/core/blob/bb90a65556d8eee0dc698ac75de0f280e547fedc/contracts/factory/src/storage.rs#L11-L19), the dual-index registry. - (b) [`OpenZeppelin/stellar-contracts`](https://github.com/OpenZeppelin/stellar-contracts) — [access-control role enumeration](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/access/src/access_control/storage.rs#L21-L29) and [`packages/tokens/src/non_fungible/extensions/enumerable/storage.rs`](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/tokens/src/non_fungible/extensions/enumerable/storage.rs#L39-L45) use forward and reverse indexes and move the last item into a removed slot. ## Strategy 9: Pull, don't push — lazy settlement for unbounded holders _persistent · O(1) per user · cumulative index_ **You need.** To distribute something (interest, rewards, emissions) _continuously_ to an unbounded set of holders — without ever touching more than one holder per transaction. **The catch.** The naive design is _push-based_: loop over every holder and update each one's entry. Current Mainnet settings allow 200 written entries per transaction, so the loop dies as soon as the population outgrows the per-transaction limit — and splitting it across transactions only scales so much. On the other hand, the design described here stays O(1) per user no matter how many holders exist. **The pattern.** _pull-based_ approach: one _global_ cumulative-index entry per pool plus one _snapshot_ entry per user: ```rust #[contracttype] pub struct ReserveEmissionData { // ONE entry per reserve pub index: i128, // cumulative rewards-per-share; only ever increases // 👇 the fields below are used to advance the index lazily on any touch: // index += eps * (now - last_time) / total_supply, until expiration pub eps: u64, pub expiration: u64, pub last_time: u64, } #[contracttype] pub struct UserEmissionData { // ONE entry per (user, reserve) pub index: i128, // the global index last time this user was touched pub accrued: i128, } // when settling one user: • accrued += user_shares * (global.index - user.index); // • user.index = global.index; ``` The global `index` answers one question — how much has a single share earned since the pool began — and it only ever increases, bumped lazily whenever anyone interacts. Each user's snapshot records where that index stood at _their_ last settlement, so `global.index − user.index` is exactly the rewards-per-share earned while they weren't looking. A holder who joins late starts at the current index and earns nothing for the time before; one who stays away for months loses nothing, because those two numbers reconstruct every accrual period they missed. Nobody ever iterates. **Trade-offs** - O(1) entries touched per user action — the same cumulative-index pattern EVM DeFi knows as Synthetix's [`rewardPerTokenStored`](https://solidity-by-example.org/defi/staking-rewards/) or MasterChef's [`accSushiPerShare`](https://github.com/sushiswap/masterchef/blob/4153a98c34e06ee3c373fcff566d1048dcd01666/contracts/MasterChef.sol#L36-L50). - Rounding, scaling, and index monotonicity are protocol invariants and need focused tests and review. - Nothing ever runs on a passive user's behalf: rewards keep accruing by formula and settle whenever the user finally shows up, but the pattern cannot push _obligations_ (fees, penalties) onto accounts that never transact. - TTL pairing: Blend gives shared emission data a 45 → 46 day policy and per-user emission data a 100 → 120 day policy — the shared entry is bumped by every user's interaction, while a user's entry is bumped only when that user shows up, so it needs the longer lease. **In the wild** - [`blend-capital/blend-contracts-v2`](https://github.com/blend-capital/blend-contracts-v2) — [pool](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/storage.rs#L84-L98) and [backstop](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/backstop/src/storage.rs#L33-L51) emissions each keep shared emission data and separate per-user emission data; the settlement delta is computed in [`distributor.rs`](https://github.com/blend-capital/blend-contracts-v2/blob/ba22b487b2c5057a4ecc28b05b5193c28e4bd117/pool/src/emissions/distributor.rs#L200-L207). ## Strategy 10: Checkpoints — read state as of a past ledger _persistent · O(1) append · O(log n) historical read_ **You need.** To read a value as of a past ledger, on-chain: a voter's power when a proposal snapshotted, total supply when voting started, the quorum in force back then. Tallying votes with _current_ balances instead lets an attacker acquire tokens mid-vote, vote, and dump them. **The catch.** A per-entity entry (Strategy 2) keeps only the latest value — every write destroys the history. Keeping the whole history as a growing `Vec` or `Map` under one key is the first red flag in the review checklist (64 KiB cap, whole-entry rewrites). And Strategy 9's cumulative index answers "how much accrued per share", not "what was this value at ledger X". **The pattern.** Checkpoint the value: an append-only series of `(ledger, value)` snapshots per subject — Strategy 8(a)'s counter-plus-indexed-entries layout, applied through time: ```rust #[contracttype] pub struct Checkpoint { pub ledger: u32, pub votes: u128 } #[contracttype] pub enum VotesStorageKey { NumCheckpoints(Address), // how many checkpoints a delegate has DelegateCheckpoint(Address, u32), // index -> Checkpoint } // write: push a checkpoint at the current ledger and bump the counter — or // overwrite the tail if it is already at this ledger (counter unchanged) // read: binary-search 0..num for the last checkpoint with ledger <= target ``` Checkpoints before the tail are never rewritten — that is what makes binary search valid. And queries must target a strictly past ledger (OpenZeppelin rejects the rest with `FutureLookup`): the current ledger's value can still change before it closes. **Trade-offs** - Appends are O(1), and same-ledger updates collapse into the tail checkpoint, so series length tracks activity, not call count. Reading the latest value takes two reads: the counter, then the tail checkpoint. - A historical read costs O(log n) _ledger reads_ — each probe is its own footprint entry. Simulation declares them automatically, but they count toward the per-transaction disk-read cap. - Every checkpoint is a persistent entry paying rent, and one archived checkpoint in the search path fails the transaction until it is restored — keep the series' TTLs extended for as long as history can be queried (OpenZeppelin bumps every checkpoint it touches). - Checkpoint only what the contract itself must read historically; off-chain consumers can rebuild history from events (Strategy 8, variant c) at zero on-chain cost. **In the wild** - [`OpenZeppelin/stellar-contracts`](https://github.com/OpenZeppelin/stellar-contracts) — the votes module keeps [per-delegate and total-supply checkpoint series](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/governance/src/votes/storage.rs#L25-L66), [collapses same-ledger writes into the tail](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/governance/src/votes/storage.rs#L444-L489), and answers past-power queries with a [binary search](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/governance/src/votes/storage.rs#L367-L400). The Governor applies the same shape to [quorum changes](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/governance/src/governor/storage.rs#L1088-L1113), so raising the quorum today does not retroactively affect proposals voted under the old one. - [`Phoenix-Protocol-Group/phoenix-contracts`](https://github.com/Phoenix-Protocol-Group/phoenix-contracts) — the stake contract stores each reward token's [distribution history as one persistent `Map` keyed by day](https://github.com/Phoenix-Protocol-Group/phoenix-contracts/blob/aa9bfc000166e3ccdc30281b5181bbd8046fd35d/contracts/stake/src/distribution.rs#L11-L41) — the single-entry variant: simpler while the history is short, but every update rewrites the whole map, which grows for as long as rewards run. ## Strategy 11: Merkle roots and derived state — replace storage with verification _one 32-byte root · derived IDs · sentinel values_ **You need.** To handle large or numerous records — airdrop entitlements, governance proposals, scheduled operations — whose full contents would be expensive or impossible to keep on-chain. **The catch.** Stored bytes add I/O, keeping them live costs rent, and each entry a transaction touches consumes footprint. None of that is mandatory: when callers are willing to hold the data themselves, the chain only needs enough of it to verify what they bring back. **The pattern.** Keep the records off-chain and store only a 32-byte hash that _commits_ to them. Whoever wants the contract to act on a record must send that record back in; the contract re-hashes what it received and compares the result with the stored hash. A match proves the input is byte-for-byte the data that was committed to — so the contract can safely act on data it never stored. Hashing a whole list into one commitment would force every claimer to resend the whole list. A **Merkle tree** fixes that: hash the records pairwise up to a single root, and any one record becomes provable on its own — the claimer sends their record plus one sibling hash per tree level (the _proof_), and the contract re-hashes upward — landing on the stored root proves the record is in the tree. The tree itself never touches the ledger; on-chain state for an arbitrarily large airdrop is one root plus one `Claimed(index)` flag per completed claim: ```rust enum DataKey { RootHash, TokenAddress, Claimed(u32) } // claim(index, receiver, amount, proof) -> re-hash (index, receiver, amount) up the // proof path; if it lands on RootHash: set Claimed(index), transfer amount to receiver ``` For a _single_ record, skip the tree: hash the record itself and use the hash as both fingerprint and storage key — a **derived ID**. OpenZeppelin's Governor never stores a proposal's actions: `propose` hashes them into `proposal_id` and stores only a four-field `ProposalCore` (proposer, vote snapshot, vote end, state) under that ID; to execute, the caller sends the full action list again, and finding a `ProposalCore` under the re-computed hash proves these are exactly the actions that were voted on — the contract invokes them straight from the caller's arguments: ```rust // governor: proposal_id = hash(targets, functions, args, description_hash) // timelock: operation_id = hash(target, fn, args, predecessor, salt) ``` What must stay on-chain can still shrink to a **sentinel**. The Governor's companion Timelock contract, which holds approved calls until a delay passes, keys its operations by the same kind of derived hash — and its entire record of an operation is a single `u32`: `0` means never scheduled, `1` means executed, and any other value is the ledger at which the operation becomes ready. Identity in the key, the whole lifecycle in one integer. The Governor itself never writes Pending, Active, Succeeded, or Defeated at all: it derives them from the stored voting window, the tallies, and the clock, and writes `state` only on irreversible transitions (canceled, queued, executed). **Trade-offs** - The ledger can _verify_ data it never stored, but it cannot _return_ it — availability becomes someone else's job, and if the off-chain copy is lost the records are permanently unusable. Each variant answers this differently: a Merkle distributor must publish the tree somewhere durable (its own auth-gated API) so claimers can fetch their proofs, while the OZ governance contracts emit each proposal's or operation's full inputs in its creation event — a single record is small enough that event history can serve as the backup copy. - Scale is set by what you still write per record. One persistent `Claimed(index)` entry per claim grows without bound — that is OZ's `merkle_distributor`. The official `merkle_distribution` example keeps the flags inside the shared instance entry instead: simpler, but the whole distribution must then fit in one 64 KiB entry. - Verification itself is cheap: O(log n) hashes for a Merkle proof, a single hash for a derived ID. **In the wild** - [`stellar/soroban-examples`](https://github.com/stellar/soroban-examples) — [`merkle_distribution/src/lib.rs`](https://github.com/stellar/soroban-examples/blob/14c069cccf85e552d4d80c8122e00a61a5723aae/merkle_distribution/src/lib.rs#L19-L20) stores the root and `Claimed(index)` flags in instance storage; its claim count is bounded by the instance-entry size. - [`OpenZeppelin/stellar-contracts`](https://github.com/OpenZeppelin/stellar-contracts) — its reusable [`merkle_distributor`](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/contract-utils/src/merkle_distributor/storage.rs#L13-L18) module is the unbounded-distribution variant: one persistent [`Claimed(u32)`](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/contract-utils/src/merkle_distributor/storage.rs#L18) entry per claimed index. [Governor proposal IDs](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/governance/src/governor/storage.rs#L771-L783) and [Timelock operation IDs](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/governance/src/timelock/storage.rs#L403-L413) are derived from caller-supplied inputs; the Timelock stores [one ledger-number sentinel per operation](https://github.com/OpenZeppelin/stellar-contracts/blob/34fd71403d59dfc0947fcede227313f37dd23635/packages/governance/src/timelock/mod.rs#L353-L357). ## Strategy 12: Scale out — contract-per-entity via factory _one contract per entity · own instance & TTLs · factory registry_ **You need.** To distribute growth across contracts — a DEX with many markets, a wallet per user, or a pool per risk profile — beyond what one contract should hold. **The catch.** Even with a sound per-entry layout, one contract concentrates its instance state, upgrade surface, and failure domain. Transactions that write that instance entry conflict, and all state in it shares the 64 KiB entry cap. **The pattern.** Deploy a **contract per entity** from a factory, which keeps the registry and shared config itself (Strategy 8 for the index, Strategy 2 for the reverse lookup). Each pair/pool/wallet gets its own instance storage, its own TTLs, its own parallelism domain: ```rust // soroswap factory: deterministic deployment + dual registry let pair_address = create_contract(&e, pair_wasm_hash, &token_pair); // deploys pair contract put_pair_address_by_token_pair(&e, token_pair, &pair_address); // (tokenA,tokenB) -> addr add_pair_to_all_pairs(&e, &pair_address); // index n -> addr ``` The deployment is deterministic: the salt hashes the sorted token pair, so a pair's address is recomputable from its tokens and the factory address alone — Strategy 11's derived-ID idea, applied to contract addresses. **Trade-offs** - Isolation: pair-local entries for pair A do not conflict with pair-local entries for pair B. Transactions that touch shared token, router, or factory entries still contend. - Sharding buys parallelism, not headroom: network-wide per-ledger resource caps apply across all contracts combined. - Cross-entity operations become cross-contract calls (CPU + footprint per hop); a router contract usually papers over this — Soroswap's router holds exactly one storage key: the factory address. - Fleet upgrades are real operational work (N contracts to upgrade), and each instance needs its own TTL extensions. The upgrade half will be solvable by [CAP-85](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0085.md) in protocol 28 — a beacon pattern: the fleet shares one externally managed executable, so a single update upgrades every instance. **In the wild** - [`soroswap/core`](https://github.com/soroswap/core) — [`contracts/factory`](https://github.com/soroswap/core/blob/bb90a65556d8eee0dc698ac75de0f280e547fedc/contracts/factory/src/lib.rs#L275-L298), [`contracts/pair`](https://github.com/soroswap/core/blob/bb90a65556d8eee0dc698ac75de0f280e547fedc/contracts/pair/src/lib.rs#L86-L89), and [`contracts/router`](https://github.com/soroswap/core/blob/bb90a65556d8eee0dc698ac75de0f280e547fedc/contracts/router/src/storage.rs#L1-L8): the full Uniswap-v2 topology. - [`kalepail/smart-account-kit`](https://github.com/kalepail/smart-account-kit) — [one smart-account contract per user](https://github.com/kalepail/smart-account-kit/blob/a7a1b0b4d94f363253ccb36106375d319c59367b/src/kit/deploy-ops.ts#L84-L122), deployed on passkey registration with a salt derived from the credential ID. ## Decision path: composing strategies For each piece of state, answer every question in order — steps 1–3 pick the lifecycle, steps 4–9 the layout. Several steps can match one piece of state, and the contract as a whole composes the strategies its pieces land on. 1. **Can you avoid storing it?** Data that callers can hold and prove needs only a hash commitment on-chain; data derivable from other state should be recomputed; data read only by off-chain consumers can be emitted as events. - [S11: Merkle roots and derived state — replace storage with verification](#strategy-11-merkle-roots-and-derived-state--replace-storage-with-verification) - [S8: Enumeration — build the index yourself](#strategy-8-enumeration--build-the-index-yourself) (variant c: events + off-chain indexer) 2. **Is it small, global, and read by (almost) every call?** Keep it in instance storage, and keep the whole instance entry under a few KiB. - [S1: Singleton config in instance storage](#strategy-1-singleton-config-in-instance-storage) 3. **Does its validity end at a known ledger, or is losing it acceptable?** Use temporary storage: align the TTL with the deadline, and also store the deadline in the value so the contract can enforce it. - [S4: Temporary storage for data with a deadline](#strategy-4-temporary-storage-for-data-with-a-deadline) 4. **Is it per-entity data with an unbounded population?** Give each entity its own persistent entry, use composite keys when lookups need more than one dimension, and extend TTLs on access. - [S2: One entry per entity — keyed by a `DataKey` enum](#strategy-2-one-entry-per-entity--keyed-by-a-datakey-enum) - [S3: Composite keys — multi-dimensional lookups](#strategy-3-composite-keys--multi-dimensional-lookups) - [S5: TTL management — bump-on-access](#strategy-5-ttl-management--bump-on-access) 5. **Are several pieces usually read and written together?** Pack them into one bounded entry; if they are updated independently or by different actors, split them into separate entries instead. - [S7: Pack vs. split — group state by access pattern](#strategy-7-pack-vs-split--group-state-by-access-pattern) 6. **Must the contract iterate over it?** A small, admin-managed set fits in one bounded-collection entry; a large or user-managed set needs counter-plus-index entries, adding a reverse map with swap-and-pop when items can be removed. - [S6: Bounded collections — a capped Vec or Map in one entry](#strategy-6-bounded-collections--a-capped-vec-or-map-in-one-entry) - [S8: Enumeration — build the index yourself](#strategy-8-enumeration--build-the-index-yourself) 7. **Are you distributing value across the population?** Pull, don't push: keep one global cumulative index and settle each holder lazily when they show up. - [S9: Pull, don't push — lazy settlement for unbounded holders](#strategy-9-pull-dont-push--lazy-settlement-for-unbounded-holders) 8. **Must the contract read a value as of a past ledger?** Checkpoint it: append `(ledger, value)` snapshots and binary-search them at read time. - [S10: Checkpoints — read state as of a past ledger](#strategy-10-checkpoints--read-state-as-of-a-past-ledger) 9. **Is it still too big or too contended?** Shard it: deploy one contract per entity from a factory. - [S12: Scale out — contract-per-entity via factory](#strategy-12-scale-out--contract-per-entity-via-factory) ## Cheatsheet ### Tier picker | Data | Tier | Why | | --- | --- | --- | | Admin, config, token metadata, pause flag | instance | one entry loaded with each contract invocation | | AMM reserves / totals (every swap updates them anyway) | instance | co-located with instance; the write conflict is inherent to the product | | Balances, ownership, positions, long-lived registries | persistent | must never be lost; archive + restore safety net | | Voting power, quorum — anything read "as of ledger X" | persistent | append-only checkpoint series; keep it live while queries can reach it | | Allowances, approvals, auctions, epoch stats, pending proposals | temporary | natural deadline; half rent; deleted on TTL expiry | | Oracle prices / regenerable caches | temporary | loss is cheap, freshness is the point | | Anything derivable (states, IDs, tallies pre-first-vote) | _none_ | recompute; hash-commit; lazy-create | ### Pattern → complexity Every pattern in this guide, costed per operation — use it to compare finalists once the [decision path](#decision-path-composing-strategies) has produced a shortlist. Complexity counts steps inside the contract, not ledger I/O: an O(1) swap-and-pop still writes up to four entries, and every entry touched counts toward the per-transaction read and write caps. **Ledger entries per item** is the rent side — how many entries the pattern keeps alive for each item it stores. An "n/a" cell means the pattern has no such operation. | Pattern | Lookup | Insert | Remove | Enumerate | Ledger entries per item | Strategy | | --- | --- | --- | --- | --- | --- | --- | | Instance singleton | O(1)\* | O(1) | O(1) | n/a | 0 — shares the instance entry | [S1](#strategy-1-singleton-config-in-instance-storage) | | Entry per entity | O(1) | O(1) | O(1) | needs an S8 index | 1 | [S2](#strategy-2-one-entry-per-entity--keyed-by-a-datakey-enum) · [S3](#strategy-3-composite-keys--multi-dimensional-lookups) | | Bounded collection in one entry | O(n) in-mem | O(1) + full rewrite | O(n) + full rewrite | O(n), 1 read | 1 for all n items | [S6](#strategy-6-bounded-collections--a-capped-vec-or-map-in-one-entry) | | Packed struct per entity | O(1), whole blob | rewrite blob | rewrite blob | needs an S8 index | 1 | [S7](#strategy-7-pack-vs-split--group-state-by-access-pattern) | | Counter + index entries | O(1) by index | O(1) | append-only | O(page) | 1 + shared counter | [S8 (a)](#strategy-8-enumeration--build-the-index-yourself) | | Double map + swap-and-pop | O(1) | O(1) | O(1) | O(page), unordered | 2 — forward + reverse | [S8 (b)](#strategy-8-enumeration--build-the-index-yourself) | | Pull-based reward index | O(1) per user | n/a | n/a | n/a | 1 per user + 1 global | [S9](#strategy-9-pull-dont-push--lazy-settlement-for-unbounded-holders) | | Checkpoint series | O(1) latest · O(log n) past | O(1) append | n/a | O(page) by index | 1 per checkpoint + shared counter | [S10](#strategy-10-checkpoints--read-state-as-of-a-past-ledger) | | Merkle commitment | O(log n) verify | root update | root update | data lives off-chain | 0–1 claim flag† | [S11](#strategy-11-merkle-roots-and-derived-state--replace-storage-with-verification) | | Factory / contract-per-entity | O(1) + cross-contract call | deploy | registry only | via registry | its own contract | [S12](#strategy-12-scale-out--contract-per-entity-via-factory) | \* Loaded with every invocation, whether or not the call reads it. † Either packed into the shared instance entry (bounded) or one persistent entry per claim (unbounded). ### 🚩 Red flags in review - **An unbounded `Map` or `Vec` under one key.** It grows toward the 64 KiB entry cap, every update rewrites the whole value, and all writers contend on one entry. Give each item its own entry ([S2](#strategy-2-one-entry-per-entity--keyed-by-a-datakey-enum)); add an index only if the contract must enumerate ([S8](#strategy-8-enumeration--build-the-index-yourself)). - **Variable-length data inside a key.** The serialized ledger key is capped at 250 bytes, so a string or vector in the key can fail at runtime. Compose keys from addresses and integers ([S3](#strategy-3-composite-keys--multi-dimensional-lookups)). - **Entries created to store a default value.** A stored zero pays rent to say nothing. Treat an absent entry as the default ([S2](#strategy-2-one-entry-per-entity--keyed-by-a-datakey-enum)). - **Hot mutable data in `instance()` with many independent writers.** Every write to the shared instance entry serializes those transactions, so ask whether the writes would conflict anyway: AMM reserves belong in instance because every swap must update them regardless of layout, while per-user balances are independent writes and belong in per-entity entries ([S2](#strategy-2-one-entry-per-entity--keyed-by-a-datakey-enum)). - **Funds-critical data in `temporary()`.** Expiry deletes it permanently; there is no restore. Anything the contract must not lose belongs in persistent storage ([S4](#strategy-4-temporary-storage-for-data-with-a-deadline) covers what temporary is for). - **TTL as the only expiry check.** Anyone can extend any entry's TTL, so a TTL never enforces a deadline. Store the deadline in the value and check it in code ([S4](#strategy-4-temporary-storage-for-data-with-a-deadline)). - **Persistent entries with no TTL-extension policy.** Every entry archives once its initial TTL runs out, and a later transaction must pay rent and resource fees to restore it. Extend on access ([S5](#strategy-5-ttl-management--bump-on-access)). - **A loop that updates every user.** It dies at the 200-writes-per-transaction cap as soon as the population outgrows it. Keep one cumulative index and settle each user lazily ([S9](#strategy-9-pull-dont-push--lazy-settlement-for-unbounded-holders)). - **Data paged across entries because it outgrew 64 KiB.** Paging is sometimes the right call, but it is often a symptom of a layout problem — first consider bounding the data ([S6](#strategy-6-bounded-collections--a-capped-vec-or-map-in-one-entry)), splitting it by access pattern ([S7](#strategy-7-pack-vs-split--group-state-by-access-pattern)), replacing it with a hash commitment ([S11](#strategy-11-merkle-roots-and-derived-state--replace-storage-with-verification)), or sharding by contract ([S12](#strategy-12-scale-out--contract-per-entity-via-factory)). ## Appendix: Mainnet limits **Protocol 27, checked 2026-07-20.** Network validators can change these values — re-verify with `stellar network settings --network mainnet` or on [Stellar Laboratory](https://lab.stellar.org/network-limits). | Per transaction | Limit | Practical meaning | | --- | --: | --- | | CPU instructions | 400,000,000 | compute budget for the whole invocation tree | | Memory | 40 MiB | host + guest memory | | **Footprint entries** (read + write) | **400** | max distinct entries one transaction may touch | | Disk-read entries / bytes | 200 / 200,000 | separate from the footprint cap; applies to disk-backed reads | | **Written entries / bytes** | **200** / 132,096 | distinct entries written; sum of written entry sizes (~129 KiB) | | Transaction size | 132,096 B | envelope incl. footprint — big footprints eat your payload | | Events + return value | 16,384 B | contract events plus the top-level return value in metadata | | Per ledger (target ~5 s today) | Limit | | ------------------------------ | --------------: | | CPU instructions | 580,000,000 | | Disk-read entries / bytes | 1,000 / 400,000 | | Write entries / bytes | 1,000 / 286,720 | | Smart-contract transactions | 2,000 | | Soroban transaction bytes | 266,240 B | | Sizes & TTLs | Value | | ---------------------------------------------- | ------------------------: | | Max serialized contract-data ledger-key size | 250 B | | Max contract-data entry size | 65,536 B (64 KiB) | | Max contract Wasm size | 131,072 B (128 KiB) | | **Max entry TTL** | 3,110,400 ledgers ≈ 180 d | | Min TTL: persistent/instance create or restore | 2,073,600 ledgers ≈ 120 d | | Min TTL: temporary create | 17,280 ledgers ≈ 1 d | :::info Two numbers to remember: **`17,280` ledgers is about one day at today's ~5-second target close time**, and **64 KiB is the whole contract-data entry limit**. Close time is a network setting, so day-based TTL constants are approximations, not wall-clock guarantees. Many codebases define `DAY_IN_LEDGERS` for readability. Keep entries well below the size limit because large values increase read, write, and rent costs. ::: ## Repositories reviewed | Repository | What it is | Storage patterns | | --- | --- | --- | | [stellar/soroban-examples](https://github.com/stellar/soroban-examples) | Official examples | Persistent balances, temporary allowances, instance AMM reserves and account signers, temporary epoch-bucketed mint quotas, and an instance-backed Merkle distributor | | [OpenZeppelin/stellar-contracts](https://github.com/OpenZeppelin/stellar-contracts) | Standard library | Persistent fungible balances, temporary allowances and role transfers, role-member and NFT swap-and-pop indexes, derived Governor/Timelock IDs, per-index Merkle claim flags, vote checkpoints, and a per-signer smart-account registry with packed context rules | | [blend-capital/blend-contracts-v2](https://github.com/blend-capital/blend-contracts-v2) | Lending protocol | 30-reserve list, packed positions, split reserve config/data, temporary auctions and queued configs, shared/user TTL gradient, and pool/backstop emissions | | [soroswap/core](https://github.com/soroswap/core) | AMM | Instance pair reserves, dual-index factory registry, deterministic contract-per-pair deployment, and a router with the factory address in instance storage | | [reflector-network/reflector-contract](https://github.com/reflector-network/reflector-contract) | Price oracle | Timestamp-keyed temporary price updates, a TTL derived from retention, and a bounded instance cache | | [kalepail/smart-account-kit](https://github.com/kalepail/smart-account-kit) | Passkey smart-account SDK | Deterministic per-user deployment of the OpenZeppelin smart account, with a salt derived from the credential ID | | [allbridge-public/allbridge-core-soroban-contracts](https://github.com/allbridge-public/allbridge-core-soroban-contracts) | Cross-chain bridge | Storage-tier abstraction trait plus persistent sent/received message-hash flags | | [Phoenix-Protocol-Group/phoenix-contracts](https://github.com/Phoenix-Protocol-Group/phoenix-contracts) | DEX, vesting, and staking | Per-user persistent bonding data plus persistent per-reward-token history maps | | [CometDEX/comet-contracts-v1](https://github.com/CometDEX/comet-contracts-v1) | Weighted AMM | Persistent LP-token balances, temporary composite-key allowances, and a factory pool registry | | [FredericRezeau/soroban-kit](https://github.com/FredericRezeau/soroban-kit) | Macro library | Type-safe storage macros that select instance, persistent, or temporary storage and bind key/value types | **Methodology.** Network values were read with `stellar network settings --network mainnet` and cross-checked against the Mainnet constants rendered by [Stellar Laboratory](https://lab.stellar.org/network-limits) on 2026-07-20. Repository claims above were checked against each repository's current default branch. Further reading: [state archival](../../../learn/fundamentals/contract-development/storage/state-archival.mdx) · [persisting data](../../../learn/fundamentals/contract-development/storage/persisting-data.mdx). --- ## Use instance storage in a contract Under the hood, instance storage is exactly like persistent storage. The only difference is that anything stored in instance storage has an archival TTL that is tied to the contract instance itself. So, if a contract is live and available, the instance storage is guaranteed to be so, too. Instance storage is really useful for global contract data that is shared among all users of the contract (token administrator, for example). From the [token example contract](../../smart-contracts/example-contracts/tokens.mdx), the helper functions to set and retrieve the admininistrator address are basically just wrappers surrounding the one Admin ledger entry. :::caution It should be noted that _every_ piece of data stored in `instance()` storage is retrieved from the ledger _every_ time the contract is invoked. Even if the invoked function does not interact with any ledger data at all. This can lead to more expensive (computationally and financially) function invocations if the stored data grows over time. Choose judiciously which bits of data actually belong in the instance storage, and which should be kept in persistent storage. ::: ```rust pub fn has_administrator(e: &Env) -> bool { let key = DataKey::Admin; e.storage().instance().has(&key) } pub fn read_administrator(e: &Env) -> Address { let key = DataKey::Admin; e.storage().instance().get(&key).unwrap() } pub fn write_administrator(e: &Env, id: &Address) { let key = DataKey::Admin; e.storage().instance().set(&key, id); } ``` --- ## Use persistent storage in a contract Persistent storage can be very useful for ledger entrys that are not common across every user of the contract instance, but that are not suitable to be temporary (user balances, for example). In this guide, we'll assume we want to store a random number for a user, and store it in the contract's persistent storage as though it were their favorite number. ```rust #[contracttype] pub enum DataKey { Favorite(Address), } #[contract] pub struct FavoriteContract; #[contractimpl] impl FavoriteContract { // This function generates, stores, and returns a random number for the user pub fn generate_fave(env: Env, user: Address) -> u64 { let key = DataKey::Favorite(user); let fave: u64 = env.prng().gen(); env.storage().persistent().set(&key, &fave); fave } // This function retrieves and returns the random number for the user pub fn get_fave(env: Env, user: Address) -> u64 { let key = DataKey::Favorite(user); if let Some(fave) = env.storage().persistent().get(&key) { fave } else { 0 } } } ``` --- ## Use temporary storage in a contract Temporary storage is useful for a contract to store data that can quickly become irrelevant or out-dated. For example, here's how a contract might be used to store a recent price of BTC against the US Dollar. ```rust // This function updates the BTC price pub fn update_btc_price(env: Env, price: i128) { env.storage().temporary().set(&!symbol_short("BTC"), &price); } // This function reads and returns the current BTC price (zero if the storage // entry is archived) pub fn get_btc_price(env: Env) -> i128 { if let Some(price) = env.storage().temporary().get(&!symbol_short("BTC")) { price } else { 0 } } ``` --- ## Contract Testing Testing is vital to ensure that smart contracts are safe, resilient, and accurate. --- ## Code Coverage Measuring code coverage uses tools to identify lines of code that are and aren't executed by tests. Code coverage stats can give us an idea of how much of a contract is actually tested by its tests. :::tip Mutation testing is another form of coverage testing. See [Mutation Testing]. ::: In rust projects the `cargo-llvm-cov` tool can be used to generate coverage stats, HTML reports, and lcov files that IDEs will load to display the coverage in the code editor. Install `cargo-llvm-cov` before proceeding with the other commands. ``` cargo install cargo-llvm-cov ``` ## How to Get Coverage Stats Run the test subcommand that will run the tests and output the stats per file. ``` cargo llvm-cov test ``` ## How to Generate a Coverage Report with Code Run the test subcommand that will run the tests and output a set of HTML files showing which lines of code are covered. ``` cargo llvm-cov test --html --open ``` The output of the command will indicate where the HTML file has been written. Open the file in a browser. ## How to Generate an LCOV File for IDEs Run the test subcommand that will run the tests and output a single `lcov.info` file. ``` cargo llvm-cov test --lcov --output-path=lcov.info ``` Load the `lcov.info` file into your IDE using its coverage feature. In VSCode this can be done by installing the [Coverage Gutters] extension and executing the `Coverage Gutters: Watch` command. :::info Measuring code coverage in fuzz tests requires different tooling. See [Fuzzing]. ::: [Coverage Gutters]: https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters [Mutation Testing]: mutation-testing.mdx [Fuzzing]: fuzzing.mdx --- ## Differential Tests with Test Snapshots Tests are written to ensure that contracts behave today as expected, and in the future as well. Over time a contract may change and in all software development there remains the possibility of changes causing side-effects that are unexpected. Testing is one of the ways that we identify unexpected changes. However tests are limited, as they only show changes to values that the tests assert on. :::tip Test snapshots are one tool for performing differential testing. See [Differential Testing] for other ways. ::: ### Test Snapshots The Soroban Rust SDK generates test snapshots on every test involving an `Env`. Test snapshots are enabled by default. At the end of the test the `Env` writes a JSON file to the `test_snapshots` directory with a full snapshot of all the events published, and the final ledger storage state. Most tests have a single `Env` and will result in a single test snapshot. Tests that have multiple `Env`s will write multiple test snapshots, one for each `Env`. Test snapshot files are named with a incrementing number on the end to separate the test snapshots for each `Env`. ### How to Use Test Snapshots 1. Write tests using the default `Env`. For example: ```rust #![cfg(test)] use soroban_sdk::Env; use crate::{Contract, ContractClient}; #[test] fn test_abc() { let env = Env::default(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); assert_eq!(client.increment(), 1); // highlight-start // At the end of the test the Env will automatically write a test snapshot // to the following directory: test_snapshots/test_abc.1.json // highlight-end } ``` 2. Run the tests and see that the test snapshots have been written to `test_snapshots/`. 3. Commit the test snapshots to source control. 4. On future updates look out for changes to test snapshots in tests that are unexpected. For example, when changing one part of a contract if the test snapshots for other parts of the contract or unrelated end-to-end tests change, that could signal that side-effects have occurred. 5. Diff test snapshots as needed to look for hints to why an unexpected change has occurred. :::info Test snapshots files are verbose. Test snapshots are most useful when changes appear and can be diffed, such as a new event being published, or storage changing. ::: To give this a go, check out the [Getting Started] contract or any of the [examples], run the tests, and look for the test snapshots on disk. [Differential Testing]: ./differential-tests.mdx [Getting Started]: ../../smart-contracts/getting-started/README.mdx [examples]: ../../smart-contracts/example-contracts/README.mdx --- ## Differential Tests Differential testing is the testing of two things to discover differences in their behavior. The goal is to prove that the two things behave consistently, and that they do not diverge in behavior except for some expected differences. The assertions should be as broad as possible, broadly testing that all observable outcomes do not change, except for any expected changes. This strategy is effective when building something new that should behave like something that already exists. That could be a new version of a contract that has unchanged behavior from its previous version. Or it could be the same contract with an updated SDK or other dependency. Or it could be a refactor that expects no functional changes. This strategy can be used in the context of unit and integration tests, or in the context of fuzz tests as well. :::tip All contracts built with the Rust Soroban SDK have a form of differential testing built-in and enabled by default. See [Differential Testing with Test Snapshots]. ::: ## How to Write Differential Tests To experiment with writing a differential test, open a contract that you've deployed, or checkout an example from the [soroban-examples] repository and deploy it. Assuming the contract has been deployed, and changes are being made to the local copy. We need to check that unchanged behavior in the contract hasn't changed compared to what is deployed. 1. Use the [stellar contract fetch] command to fetch the contract that's already deployed. The contract already deployed will be used as a baseline that the local copy is expected to behave like. ```shell stellar contract fetch --id C... --out-file contract.wasm ``` 2. Import the contract into the tests with the `contractimport!` macro. ```rust mod deployed { soroban_sdk::contractimport!(file = "contract.wasm"); } ``` 3. Write a test that runs the same logic for the deployed contract and the local contract, comparing the result. Assuming the [increment example] is in use, the test would look something like the following. ```rust #![cfg(test)] use crate::{IncrementContract, IncrementContractClient}; use soroban_sdk::{testutils::Events as _, Env}; mod deployed { soroban_sdk::contractimport!(file = "contract.wasm"); } #[test] fn differential_test() { assert_eq!( // Baseline – the deployed contract { let env = Env::default(); let contract_id = env.register(deployed::WASM, ()); let client = IncrementContractClient::new(&env, &contract_id); ( // Return Values ( client.increment(), client.increment(), client.increment(), ), // Events env.events().all(), ) }, // Local – the changed or refactored contract { let env = Env::default(); let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); ( // Return Values ( client.increment(), client.increment(), client.increment(), ), // Events env.events().all(), ) }, ); } ``` 4. Run the test to compare the baseline and local observable outcomes. This test uses the same patterns used in [unit tests] and [integration tests]: 1. Create an environment, the `Env`. 2. Import the Wasm contract to compare with. 3. Register the local contract to be tested. 4. Invoke functions using a client. 5. Assert equality. :::tip Differential tests work best when less assumptions are made. Rather than asserting only on specific return values or on implementation details like specific state, asserting on all observable outcomes and including things like events published or return values of other read-only contract functions will help to discover unexpected changes. ::: [Getting Started]: ../../smart-contracts/getting-started [increment example]: https://github.com/stellar/soroban-examples/blob/main/increment/src/lib.rs [Differential Testing with Test Snapshots]: ./differential-tests-with-test-snapshots.mdx [stellar contract fetch]: ../../../tools/cli/stellar-cli.mdx#stellar-contract-fetch [integration tests]: ./integration-tests.mdx [unit tests]: ./unit-tests.mdx [stellar/rs-soroban-sdk#1360]: https://github.com/stellar/rs-soroban-sdk/issues/1360 --- ## Fork Testing Fork testing is another form of [integration test], where not only mainnet contracts can be used, but also their data on mainnet. A snapshot of the ledger is taken and used to create an environment for testing. The test operates as a fork of that initial state. Our ability to test a contract that relies on another contract is limited to our own knowledge of this other contract and the different states it can get into. Integration testing, where real dependencies are used removes some assumptions, but if a dependency has complex state it may also be useful to test against mainnet data as it exists at different points in time. Testing with mainnet data is one way to further close the gap. The [Soroban Rust SDK] and [Stellar CLI] come together to make possible testing with mainnet data. ## How to Write Tests with Mainnet Data The following is an example of a test that includes a dependency contract into the test, rather than mock it. The test is written to test the [increment-with-pause contract] and the [pause contract]. The contract has an `increment` function that increases a counter value by one on every invocation. The contract depends on the pause contract to control whether the increment functionality is paused. The following tests set up the `increment-with-pause` contract, using a ledger snapshot that has the dependency `pause` contract already deployed along with its contract data. 1. Use [stellar snapshot create] command to create a snapshot of the pause contract and its data. ``` stellar snapshot create --address C... --output json --out snapshot.json ``` :::info The `--ledger ` option can be added to specify a ledger to snapshot. When not specified the latest ledger that's been pushed to archives is snapshot, that is usually less than 5 minutes old. ::: 2. Use `Env::from_ledger_snapshot_file(...)` to load the snapshot into an `Env` in the test. ```rust let env = Env::from_ledger_snapshot_file("snapshot.json"); ``` 3. Write a test similar to the following that uses the `Env` preloaded with the contracts and contract data in `snapshot.json`. The test makes assertions about the behavior of the main contract in the context of the real dependencies and data that the dependencies have on the network. ```rust #[test] fn test() { // highlight-start let env = Env::from_ledger_snapshot_file("snapshot.json"); // highlight-end let contract_id = env.register( IncrementContract, IncrementContractArgs::__constructor(&pause_id), ); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.increment(), 1); } ``` Most tests, whether they're unit, mocks, or integration tests, will look very similar to the test above. The tests will do four things: 1. Create an environment, the `Env`, either with `Env::default()` or `Env::from_ledger_snapshot_file(...)`. 2. Register the contract(s) to be tested. 3. Invoke functions using the generated client. 4. Assert the outcome. :::tip Snapshots created from testnet or mainnet can also be a way to test and debug incidents with a contract on network. A snapshot can be created at a specific ledger and tests written using that snapshot to better understand why or how a series of events occurred. ::: :::tip A snapshot can be created of any contract deployed to mainnet or testnet using the `stellar snapshot create` command. It can be a fun way to experiment with and learn about deployed contracts by loading a snapshot into a test and invoking the contract. Fuzzing can even be performed on contracts using this tooling. ::: [increment-with-pause contract]: https://github.com/stellar/soroban-examples/blob/main/increment_with_pause/src/lib.rs [pause contract]: https://github.com/stellar/soroban-examples/blob/main/pause/src/lib.rs [integration test]: ./integration-tests.mdx [Making Cross-Contract Calls]: ../conventions/cross-contract.mdx [Soroban Rust SDK]: ../../../tools/sdks/contract-sdks.mdx#soroban-rust-sdk [Stellar CLI]: ../../../tools/cli/README.mdx --- ## Fuzzing Fuzzing is the process of providing random data to programs to identify unexpected behavior, such as crashes and panics. Fuzz tests can also be written as property tests that instead of seeking to identify panics and crashes, assert on some property remaining true. Fuzzing as demonstrated here and elsewhere in these docs will use principles from both property testing and fuzzing, but will only use the term fuzzing to refer to both. The following steps can be used in any Stellar contract workspace. If experimenting, try them in the [increment example]. The contract has an `increment` function that increases a counter value by one on every invocation. ## How to Write Fuzz Tests 1. Install the nightly Rust toolchain. Nightly Rust is required to run cargo-fuzz. ``` rustup install nightly ``` 2. Install `cargo-fuzz`. ```text cargo install --locked cargo-fuzz ``` 3. Initialize a fuzz project by running the following command inside your contract directory. ```text cargo fuzz init ``` 4. Open the contract's `Cargo.toml` file. Add `lib` as a `crate-type`. ```diff [lib] -crate-type = ["cdylib"] +crate-type = ["lib", "cdylib"] ``` 5. Open the generated `fuzz/Cargo.toml` file. Add the `soroban-sdk` dependency. ```diff [dependencies] libfuzzer-sys = "0.4" +soroban-sdk = { version = "*", features = ["testutils"] } ``` 6. Open the generated `fuzz/src/fuzz_target_1.rs` file. It will look like the below. ```rust #![no_main] use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { // fuzzed code goes here }); ``` 7. Fill out the `fuzz_target!` call with test setup and assertions. For example, for the [increment example]: ```rust #![no_main] use libfuzzer_sys::fuzz_target; use soroban_increment_with_fuzz_contract::{IncrementContract, IncrementContractClient}; use soroban_sdk::{ testutils::arbitrary::{arbitrary, Arbitrary}, Env, }; #[derive(Debug, Arbitrary)] pub struct Input { pub by: u64, } fuzz_target!(|input: Input| { let env = Env::default(); let id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &id); let mut last: Option = None; for _ in input.by.. { match client.try_increment() { Ok(Ok(current)) => assert!(Some(current) > last), Err(Ok(_)) => {} // Expected error Ok(Err(_)) => panic!("success with wrong type returned"), Err(Err(_)) => panic!("unrecognised error"), } } }); ``` 8. Execute the fuzz target. ```text cargo +nightly fuzz run --sanitizer=thread fuzz_target_1 ``` :::info If you're developing on MacOS you need to add the `--sanitizer=thread` flag in order to work around a [known issue](https://github.com/stellar/rs-soroban-sdk/issues/1056). ::: This test uses the same patterns used in [unit tests] and [integration tests]: 1. Create an environment, the `Env`. 2. Register the contract to be tested. 3. Invoke functions using a client. 4. Assert expectations. :::tip For a full detailed example, see the [fuzzing example]. ::: :::info There is another tool for fuzzing Rust code, `cargo-afl`. See the [Rust Fuzz book] for a tutorial for how to use it. ::: ## How to Get Code Coverage of Fuzz Tests Getting code coverage data for fuzz tests requires some different tooling than when doing the same for regular Rust tests. 1. Run the fuzz tests until it has produced a corpus, just as in step 7 above. ```text cargo +nightly fuzz run --sanitizer thread fuzz_target_1 ``` 2. Install the llvm-tools for the nightly compiler. ```text rustup component add --toolchain nightly llvm-tools-preview ``` 3. Run the fuzz coverage command that'll execute the corpus and write coverage data to the coverage directory in the `profdata` format. ```text cargo +nightly fuzz coverage --sanitizer thread fuzz_target_1 ``` 4. Run the llvm-cov command to convert the profdata file to an lcov file. ``` $(find $(rustc --print sysroot) -name llvm-cov) export \ -instr-profile=fuzz/coverage/fuzz_target_1/coverage.profdata \ -object target/$(rustc -vV | sed -n 's|host: ||p')/coverage/$(rustc -vV | sed -n 's|host: ||p')/release/fuzz_target_1 \ --ignore-filename-regex "rustc" \ -format=lcov \ > lcov.info ``` Load the `lcov.info` file into your IDE using its coverage feature. In VSCode this can be done by installing the [Coverage Gutters] extension and executing the `Coverage Gutters: Watch` command. :::tip To measure code coverage of regular Rust tests, see [Code Coverage]. ::: [increment example]: https://github.com/stellar/soroban-examples/blob/main/increment/src/lib.rs [Differential Testing with Test Snapshots]: ./differential-tests-with-test-snapshots.mdx [stellar contract fetch]: ../../../tools/cli/stellar-cli.mdx#stellar-contract-fetch [integration tests]: ./integration-tests.mdx [unit tests]: ./unit-tests.mdx [stellar/rs-soroban-sdk#1360]: https://github.com/stellar/rs-soroban-sdk/issues/1360 [fuzzing example]: ../../smart-contracts/example-contracts/fuzzing.mdx [Rust Fuzz Book]: https://rust-fuzz.github.io/book [Code Coverage]: code-coverage.mdx [Coverage Gutters]: https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters --- ## Integration Tests Integration tests are tests that include the integration between components, and so test a larger scope such as other contracts. The [Soroban Rust SDK] makes it just as easy to integration test by providing utilities for testing against real contracts fetched from mainnet, testnet, or the local file system. ## How to Write Integration Tests The following is an example of a test that includes a dependency contract into the test, rather than mock it. The test is written to test the [increment-with-pause contract] and the [pause contract]. The contract has an `increment` function that increases a counter value by one on every invocation. The contract depends on the pause contract to control whether the increment functionality is paused. The following tests set up the `increment-with-pause` contract, as well as import and register the real pause contract using its wasm file. 1. Use the [stellar contract fetch] command to fetch the dependency contract that's already deployed on testnet or mainnet. ```shell stellar contract fetch --id C... > pause.wasm ``` 2. Import the contract into the tests with the `contractimport!` macro. ```rust mod pause { soroban_sdk::contractimport!(file = "pause.wasm"); } ``` 3. Write a test similar to the following that registers not only the increment-with-pause contract, but also the pause contract that was imported. The test checks that when the pause contract is not paused that the increment contract operates as expected. When it is paused, the increment function errors. Once it's unpaused the function operates as expected. ```rust #[test] fn test() { let env = Env::default(); // highlight-start let pause_id = env.register(pause::WASM, ()); let pause_client = pause::Client::new(&env, &pause_id); // highlight-end let contract_id = env.register( IncrementContract, IncrementContractArgs::__constructor(&pause_id), ); let client = IncrementContractClient::new(&env, &contract_id); pause_client.set(&false); assert_eq!(client.increment(), 1); pause_client.set(&true); assert_eq!(client.try_increment(), Err(Ok(Error::Paused))); pause_client.set(&false); assert_eq!(client.increment(), 2); } ``` Most tests, whether they're unit, mocks, or integration tests, will look very similar to the test above. The tests will do four things: 1. Create an environment, the `Env`. 2. Register the contract(s) to be tested. 3. Invoke functions using the generated client. 4. Assert the outcome. [increment-with-pause contract]: https://github.com/stellar/soroban-examples/blob/main/increment_with_pause/src/lib.rs [pause contract]: https://github.com/stellar/soroban-examples/blob/main/pause/src/lib.rs [Integration Tests]: ./integration-tests.mdx [Making Cross-Contract Calls]: ../conventions/cross-contract.mdx [Soroban Rust SDK]: ../../../tools/sdks/contract-sdks.mdx#soroban-rust-sdk --- ## Testing with Ledger Snapshot Ledger snapshots can be used to test smart contracts with a local copy of the ledger’s entries. Learn how to create a ledger snapshot and use it for smart contract testing in a few simple steps. The examples here will cover the following use cases: - Read stored value from the ledger data - Read stored value by calling a smart contract function - Read the balance of an account ## 1. Create a default project Before getting into creating a ledger snapshot and getting into the testing, let’s first create a simple smart contract that can be used for the test examples. Get started by creating the default Hello World contract: ```bash stellar contract init ``` The smart contract has two functions: `set_value()` for storing a value, and `get_value()` for reading the value from storage. Replace the default `hello()` function with these two functions: ```rust #![no_std] use soroban_sdk::{contract, contractimpl, vec, Env, String, Vec}; #[contract] pub struct Contract; #[contractimpl] impl Contract { pub fn set_value(env: Env, key: String, value: u32) { env.storage().persistent().set(&key, &value); } pub fn get_value(env: Env, key: String) -> u32 { env.storage().persistent().get(&key).unwrap_or(0) } } ``` Since we want to write a test that reads data stored on the ledger, or more precisely, in a snapshot of the ledger, we must build, deploy, and invoke the smart contract. #### Build contract Use the [Stellar CLI](../../../tools/cli/stellar-cli.mdx) to build the contract: ```bash stellar contract build ``` #### Deploy contract Next, we deploy the contract to Testnet: ```bash stellar contract deploy \ --wasm target/wasm32v1-none/release/hello_world.wasm \ --source alice \ --network testnet ``` The deploy command will return the contract’s address (e.g., `CB5QCALXDP2N6H473AQBNIFEAPCNHWCIWOASRNGTHCSC4WNC3SOROBAN`), which will be used in the following steps. ####Invoke contract Finally, we invoke the set_value() function to store a value on the Testnet ledger: ```bash stellar contract invoke \ --id CB5QCALXDP2N6H473AQBNIFEAPCNHWCIWOASRNGTHCSC4WNC3SOROBAN \ --source alice \ --network testnet \ -- \ set_value \ --key "count" --value 123456 ``` Now we have a working, deployed smart contract function that has stored a value on the ledger. You can verify the value by invoking the `get_value()` function. ## 2. Create a ledger snapshot A snapshot of the ledger can be created using the Stellar CLI. The CLI command allows you to customize and limit the scope of the snapshot, since it’s most likely not necessary for you to create a snapshot of all ledger entries. See the [documentation](../../../tools/cli/stellar-cli.mdx#stellar-snapshot-create) for full details about how to limit the snapshot. For the examples used here, we want to limit the ledger snapshot to include entries related to: - The smart contract - The user `alice` - The native token address (XLM) From the smart contract project root, create the ledger snapshot with this command: ```bash stellar snapshot create \ --output json \ --network testnet \ --address CB5QCALXDP2N6H473AQBNIFEAPCNHWCIWOASRNGTHCSC4WNC3SOROBAN \ --address alice \ --address CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC ``` The command will store the snapshot as a JSON file named `snapshot.json`. This is a sample of the snapshot, which shows the value stored when the contract function `set_value()` was invoked: ```json { "protocol_version": 23, "sequence_number": 801343, "timestamp": 0, "network_id": "cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472", "base_reserve": 1, "min_persistent_entry_ttl": 0, "min_temp_entry_ttl": 0, "max_entry_ttl": 0, "ledger_entries": [ ... [ { "contract_data": { "contract": "CB5QCALXDP2N6H473AQBNIFEAPCNHWCIWOASRNGTHCSC4WNC3SOROBAN", "key": { "string": "count" }, "durability": "persistent" } }, [ { "last_modified_ledger_seq": 801330, "data": { "contract_data": { "ext": "v0", "contract": "CB5QCALXDP2N6H473AQBNIFEAPCNHWCIWOASRNGTHCSC4WNC3SOROBAN", "key": { "string": "count" }, "durability": "persistent", "val": { "u32": 123456 } } }, "ext": "v0" }, 4294967295 ] ], ] ... } ``` We can now start to write a test script that loads the snapshot into the test environment. If the smart contract function `set_value()` is invoked again, run the `stellar snapshot create` command again to ensure the snapshot reflects the ledger. ## 3. Read stored values and balances To illustrate how smart contract tests can utilize the ledger snapshot data, we will read the stored value in two different ways and get the balance of the user `alice`. ### Loading snapshot data As a starting point, we use the test file `test.rs` included in the default smart contract project we created and modified. Since the `hello()` function was removed and replaced with the functions to store and get a value, the test for `hello()` can also be removed. First, the ledger snapshot data is loaded into the environment: ```rust #[test] fn test() { let env = Env::from_ledger_snapshot_file( "../../snapshot.json", ); } ``` ### Read stored value from the ledger In this first example, we read the stored value directly from the ledger using `env.storage()`. The method `env.as_contract()` allows us to execute code in the context of a given contract ID. This means we can execute code as if we were inside the contract. First, we define the contract ID for the test, which is the contract ID returned when the smart contract was deployed. Then `env.as_contract()` is used to get the value from storage, which means from the ledger. When the contract function `set_value()` was invoked, we used `count` as the key, so the same key is used to get the value. ```rust #[test] fn test() { let env = Env::from_ledger_snapshot_file( "../../snapshot.json", ); let contract_id = Address::from_str( &env, "CB5QCALXDP2N6H473AQBNIFEAPCNHWCIWOASRNGTHCSC4WNC3SOROBAN" ); env.as_contract(&contract_id, || { let val: u32 = env.storage().persistent().get( &String::from_str(&env, "count") ).unwrap_or(0); assert_eq!(val, 123456); }); } ``` Now run the test: ```bash cargo test ``` The expected value is `123456`, and the value is checked by `assert_eq!()`. If the test doesn’t pass, the test will panic. ### Read stored value with contract function The more typical way of reading a stored value in tests is to call the contract function that reads the value. The test for this approach is the same as if the default environment were loaded, except we don’t need to register the contract in the test; we use a real contract ID, like in the previous example, because in the ledger snapshot, the value is tied to the specific contract ID. ```rust #[test] fn test() { let env = Env::from_ledger_snapshot_file( "../../snapshot.json", ); let contract_id = Address::from_str( &env, "CB5QCALXDP2N6H473AQBNIFEAPCNHWCIWOASRNGTHCSC4WNC3SOROBAN" ); let client = ContractClient::new(&env, &contract_id); let value: u32 = client.get_value(&String::from_str(&env, "count")); assert_eq!(value, 123456); } ``` This example shows there are only minor differences between testing contract functions with the default environment and the snapshot environment. ### Read account balance In this last example, we want to read the balance of an account. It can be the balance of a user account or of a smart contract - any account that can hold a balance. In this example, we check the balance of the user account `alice`. In this example, we assume that `alice` already has a balance in XLM, and since the examples are based on Testnet, the `alice` account was funded by FriendBot if this [guide](../../../tools/cli/cookbook/stellar-keys.mdx) was followed while creating the account. The address of `alice` can be looked up by running this command: ```bash stellar keys address alice ``` Tokens, such as XLM, are wrapped in a contract interface called the [Stellar Asset Contract (SAC)](../../../tokens/stellar-asset-contract.mdx). This interface provides a convenient way to query an account’s balance. The first step of getting the account balance is to create a contract client for XLM tokens. The SAC address for XLM is a reserved address and will be the same for all projects. USDC and other assets will have another SAC address, but they do not change. We use the SAC address to define a client using `TokenClient`. Then we define the address of the account we want to look up the balance of, and call `client.balance()` with the account address as the argument. ```rust #[test] fn test() { let env = Env::from_ledger_snapshot_file( "../../snapshot.json", ); let client = TokenClient::new( &env, &Address::from_string(&String::from_str( &env, "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" )) ); let account = Address::from_string(&String::from_str( &env, "GDKOYSOKU4TNHGGR763NOL6VNY52WKJL3XI33TOUKDNF4AZN7SOROBAN" )); assert_eq!(client.balance(&account), 99602342515); } ``` The account can be a user, like in this case, but it can also be a contract holding assets - like a smart contract wallet. --- ## Mocking Mocks are used in tests to exclude functionality that a test doesn't want to test. Mocks are used when it's difficult to test against an external component. :::tip The [Soroban Rust SDK] makes it just as easy to test against a real contract as it does to test against a mock of a contract. In some ecosystems integration tests are avoided. Not in the Stellar ecosystem. See [Integration Tests]. ::: ## How to Write Tests with Mocks The following is an example of a test that uses a mock, written to test the [increment-with-pause contract]. The contract has an `increment` function that increases a counter value by one on every invocation. The contract depends on another contract that controls whether the increment functionality is paused. The following tests set up the `increment-with-pause` contract, as well as a mock pause contract, and invokes the increment contract's function several times under different conditions the pause contract is expected to be in. The following test checks that when the pause contract is not paused, the increment contract functions. ```rust #![cfg(test)] use crate::{Error, IncrementContract, IncrementContractArgs, IncrementContractClient, Pause}; use soroban_sdk::{contract, contractimpl, Env}; mod notpaused { use super::*; // highlight-start #[contract] pub struct Mock; #[contractimpl] impl Pause for Mock { fn paused(_env: Env) -> bool { false } } // highlight-end } #[test] fn test_notpaused() { let env = Env::default(); // highlight-start let pause_id = env.register(notpaused::Mock, ()); // highlight-end let contract_id = env.register( IncrementContract, IncrementContractArgs::__constructor(&pause_id), ); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.increment(), 1); assert_eq!(client.increment(), 2); assert_eq!(client.increment(), 3); } ``` The following test checks that when the pause contract is paused, the increment contract function rejects attempts to increment. ```rust #![cfg(test)] use crate::{Error, IncrementContract, IncrementContractArgs, IncrementContractClient, Pause}; use soroban_sdk::{contract, contractimpl, Env}; mod paused { use super::*; // highlight-start #[contract] pub struct Mock; #[contractimpl] impl Pause for Mock { fn paused(_env: Env) -> bool { true } } // highlight-end } #[test] fn test_paused() { let env = Env::default(); // highlight-start let pause_id = env.register(paused::Mock, ()); // highlight-end let contract_id = env.register( IncrementContract, IncrementContractArgs::__constructor(&pause_id), ); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.try_increment(), Err(Ok(Error::Paused))); } ``` Most tests, whether you're writing unit, mocks, or integration tests, will look very similar to these tests. They'll do four things: 1. Create an environment, the `Env`. 2. Register the contract(s) to be tested. 3. Invoke functions using the generated client. 4. Assert the outcome. :::warning Mocking introduces assumptions about the behavior of another contract. Even if the contract publishes an interface that says it'll return a bool (true/false), contracts can return any type. ```rust impl Pause { fn paused(env: Env) -> ? { ? } } ``` This is one reason why it's helpful to test and fuzz using real dependencies and to code defensively assuming any external contract call could cause your contract to fail. See [Integration Tests] for how to test with real dependencies. The [Soroban Rust SDK] handles contract calls defensively so that any unexpected error or unexpected type returned from the called contract will cause execution to stop. The SDK also provides methods to make these calls and to intercept error situations. See [Making Cross-Contract Calls] for more details. ::: [increment-with-pause contract]: https://github.com/stellar/soroban-examples/blob/main/increment_with_pause/src/lib.rs [Integration Tests]: ./integration-tests.mdx [Making Cross-Contract Calls]: ../conventions/cross-contract.mdx [Soroban Rust SDK]: ../../../tools/sdks/contract-sdks.mdx#soroban-rust-sdk --- ## Mutation Testing Mutation testing is making changes to a program, either manually or automatically, to identify changes that can be made that don't get caught by tests. Mutation testing is similar to measuring [code coverage], sharing the same goal to identify code not covered by tests. But where code coverage focuses on checking if a line of code is executed during a test, mutation testing will actually check that a test fails when the line is changed. A line of code can look like it is covered by tests, but the outcomes and side-effects may not be asserted on in the tests. ## How to do Mutation Testing The `cargo-mutants` tool can be used to automatically and iteratively modify the Rust code, and rerun the tests after each mutation, to identify code not tested. 1. Install `cargo-mutants`: ```shell cargo install --locked cargo-mutants ``` 2. Run the `cargo mutants` command inside your contract's crate directory. ```shell $ cargo mutants Found 4 mutants to test ok Unmutated baseline in 19.0s build + 0.6s test INFO Auto-set test timeout to 20s MISSED src/lib.rs:14:9: replace IncrementContract::increment -> u32 with 1 in 0.4s build + 0.4s test 4 mutants tested in 23s: 1 missed, 3 caught ``` Code that is identified as not covered by a test will be outputted as a `MISSED` line in the output. Diffs of each change that was attempted can be found in the `mutants.out/diff` directory. [code coverage]: code-coverage.mdx --- ## Test Authorization Tests can assert on the auths that are expected to occur. The following example sets up a test environment, registers an increment contract, and checks after the increment invocation what auths were required. ```rust #[test] fn test() { let env = Env::default(); env.mock_all_auths(); let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); let user_1 = Address::random(&env); assert_eq!(client.increment(&user_1, &5), 5); // highlight-start // Verify that the user indeed had to authorize a call of `increment` with // the expected arguments: assert_eq!( // Get the auths that were seen in the last invocation. env.auths(), std::vec![( // Address for which authorization check is performed user_1.clone(), // Invocation tree that needs to be authorized AuthorizedInvocation { // Function that is authorized. Can be a contract function or // a host function that requires authorization. function: AuthorizedFunction::Contract(( // Address of the called contract contract_id.clone(), // Name of the called function symbol_short!("increment"), // Arguments used to call `increment` (converted to the // env-managed vector via `into_val`) (user_1.clone(), 5_u32).into_val(&env), )), // The contract doesn't call any other contracts that require // authorization, sub_invocations: std::vec![] } )] ); // highlight-end } ``` :::tip For the full example the above snippet is extracted from, see the [auth example contract](../../smart-contracts/example-contracts/auth.mdx). ::: ## Authorization in constructors If a contract's constructor calls `require_auth()` (or `require_auth_for_args()`), both `env.register(...)` and `env.register_at(...)` switch the environment to recording-auth mode for the duration of the constructor invocation, inheriting the mode from the current auth manager. Once the constructor returns, the previous auth manager is restored. This means constructor auth checks succeed for contracts registered with either function, without requiring `mock_all_auths()` to be called first. To have auth in a constructor execute as it will in production the contract must be deployed in the test using the standard deployment functions after being built to Wasm. :::caution This behaviour was not consistent in some versions of the `soroban-sdk` due to an issue, but is consistent as of v27.0.2. ::: --- ## Test Events Tests can assert on events that are expected to be published. The following example sets up a test environment, registers an increment contract, and checks after the increment invocations which events were published. ```rust #[test] fn test() { let env = Env::default(); let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.increment(), 1); assert_eq!(client.increment(), 2); assert_eq!(client.increment(), 3); // highlight-start assert_eq!( // Get all events published since the Env was created. env.events().all(), // Compare the events with the expected events. vec![ &env, ( contract_id.clone(), (symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env), 1u32.into_val(&env) ), ( contract_id.clone(), (symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env), 2u32.into_val(&env) ), ( contract_id, (symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env), 3u32.into_val(&env) ), ] ); // highlight-end } ``` :::tip For the full example the above snippet is extracted from, see the [events example contract](../../smart-contracts/example-contracts/events.mdx). ::: --- ## Unit Tests Unit tests are small tests that test one piece of functionality within a contract. ## How to Write Unit Tests The following is an example of a unit test, written to test the [increment contract](https://github.com/stellar/soroban-examples/blob/main/increment/src/lib.rs). The contract has an `increment` function, that increases a counter value by one on every invocation. The following test invokes that contract's function several times, and checks that the value increases by one. ```rust #![cfg(test)] use soroban_sdk::Env; use crate::{IncrementContract, IncrementContractClient}; #[test] fn test() { let env = Env::default(); let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.increment(), 1); assert_eq!(client.increment(), 2); assert_eq!(client.increment(), 3); } ``` Ref: https://github.com/stellar/soroban-examples/blob/main/increment/src/test.rs In the test there's a full environment setup, used, and torn down in the test, and it happens fast. The Rust test harness runs all the tests for a contract in parallel and each will have its own isolated contract environment. Most tests, even integration tests and fuzz tests, will look very similar to this unit test. They'll do four things: 1. Create an environment, the `Env`. ```rust let env = Env::default(); ``` 2. Register the contract(s) to be tested. Include as a second parameter any constructor parameters. ```rust let contract_id = env.register(IncrementContract, ()); ``` 3. Invoke functions using the generated client. ```rust let client = IncrementContractClient::new(&env, &contract_id); client.increment() ``` 4. Assert the outcome. ```rust assert_eq!(client.increment(), 1); ``` :::tip Tests are written in Rust, alongside the contract. Same tools. Same APIs. Same SDK. No context switching! Use your favorite Rust tools and libraries that you'd use for any Rust test. ::: :::info The `Env` created at the beginning of the test is not a simulation of the Soroban Environment. It's the same Soroban Environment that mainnet uses to execute contracts. There are only some minor differences: test utilities are enabled, and the storage backend is different. ::: It's a simple test, but it's a complete test. There's a full environment setup, used, and torn down in the test, and it happens fast. The Rust test harness runs all the tests for a contract in parallel and each will have its own isolated contract environment. Most tests, even integration tests and fuzz tests, will look very similar to this unit test. They'll do four things: 1. Create an environment, the `Env`. 2. Register the contract(s) to be tested. 3. Invoke functions using a client. 4. Assert the outcome. --- ## Stellar Asset Contract (SAC) Tokens The Stellar Asset Contract (SAC) is a standard introduced on the Stellar blockchain to streamline and enhance token issuance and management. Learn about using tokens on Stellar. --- ## Set a custom admin account for a Stellar Asset Contract (SAC) The [Stellar Asset Contract (SAC)](../../../tokens/stellar-asset-contract.mdx) includes functionality to set a custom administration account on the contract itself. This can allow for flexible arrangements of asset administration, which can be configured to suit many distinct use-cases. For example, you can set a custom SAC admin, and then lock the issuer account, allowing for token administration to be _exclusively_ performed through the SAC contract. Or, you could set a custom SAC admin and still keep the issuer account unlocked, allowing for a hybrid approach to token administration. :::info It should be understood that an asset's issuer account can have a distinct set of signatures and weights, and these are unrelated to the SAC admin address. ::: ## Considerations To effectively utilize the SAC admin functionality, you should be aware of a few things first: - When a SAC is initially enabled for an issued asset, the `Admin` address on the contract defaults to the issuer account. - A SAC `Admin` address can be either a regular Stellar account (`G...`), or it can be a smart contract (`C...`) address. The use-cases requiring a smart contract to act as a SAC `Admin` tend to be more common. This opens up the possibility of programmatically minting and taking other administrative actions for an asset by way of a smart contract invocation. - Changing a SAC `Admin` address requires authorization from the asset's _current_ `Admin` address, which will be the issuer account the first time the action is performed. Depending on the use-case, it can be beneficial for asset issuers to mint an entire supply of tokens up front, and then lock down the issuer account. This would keep the total token supply capped forever. You can learn more about this practice on the [asset design considerations](../../../tokens/control-asset-access.mdx#limiting-the-supply-of-an-asset) page. However, since the issuer account and a SAC `Admin` address act independently, there are some interesting points to make that might seem counterintuitive at first glance. - If an asset's issuer account is locked while the SAC `Admin` address has not been changed, it will _never_ be possible to change the SAC `Admin` address. This is because the issuer account can no longer authorize the first `set_admin` invocation. - If an asset's issuer account is locked after the SAC `Admin` address has been changed, tokens will still be mint-able and/or clawback-able (if enabled) from the SAC as long as it's authorized by the _current_ `Admin` address. :::danger When changing a SAC `Admin` address, the new admin address provided is not validated at that time. This means you can lock down administration from the SAC **forever**. Consider your choice of SAC `Admin` address carefully and thoroughly. ::: ## Example The following example will create a new Stellar asset, `STAR:GCS5NEHKJALCSVJAKIORXXVS554QQV5FNDLBK33CCAH6UIRYPXYZFC34`. Then, we will create a simple contract to regulate a hypothetical airdrop for the token. This contract will be configured as the SAC `Admin` address for the `STAR` asset, and it will be able to perform any administrative functions we want. :::note We will not demonstrate setting a new `G...` account as a SAC admin in this guide. Any use-case that needs asset administration delegated to another `G...` account will likely be better served by taking advantage of the existing [multisig capabilities](../../../learn/fundamentals/transactions/signatures-multisig.mdx) of Stellar accounts. ::: ### Enable the built-in SAC contract First, we'll enable the SAC contract using the Stellar CLI: ```shell stellar contract asset deploy \ --source-account starIssuer --network testnet \ --asset STAR:GCS5NEHKJALCSVJAKIORXXVS554QQV5FNDLBK33CCAH6UIRYPXYZFC34 # CBVYF2KJ72BRPLVPCUL3PGWDO5RK2XP4AJDHKX7GDDBJW42L2C6VT3SF ``` This gives us the SAC address of `CBVYF2KJ72BRPLVPCUL3PGWDO5RK2XP4AJDHKX7GDDBJW42L2C6VT3SF`. :::note We're using the asset's issuer account here to enable the SAC on the Testnet network, but this action can be performed using _any_ account. ::: ### Check the initial `Admin` address Now that we have the SAC deployed to the network, we can invoke the `admin` function to find out what its current `Admin` address is: ```shell stellar contract invoke \ --source-account starIssuer --network testnet \ --id CBVYF2KJ72BRPLVPCUL3PGWDO5RK2XP4AJDHKX7GDDBJW42L2C6VT3SF \ -- \ admin # GCS5NEHKJALCSVJAKIORXXVS554QQV5FNDLBK33CCAH6UIRYPXYZFC34 ``` Unsurprisingly, it's set to the issuer's `GCS5...` account. Let's change that. ### Set the `Admin` address to some contract `C...` address For this part of the example, we'll use a simple "airdrop" contract as the `Admin` address. It has a very simple implementation, and only one main function, `claim_airdrop`. Whenever someone invokes that function, 12.3456789 `STAR` will be minted to their account. ```rust #[contractimpl] impl StarDropContract { pub fn __constructor(env: Env, sac_address: Address) { env.storage().instance().set(&symbol_short!("SAC_ADDR"), &sac_address); } pub fn claim_airdrop(env: Env, receiver: Address) { if env.storage().persistent().has(&receiver) { panic!("receiver has already claimed") } receiver.require_auth(); let amount: i128 = 123456789; env.storage().persistent().set(&receiver, &amount); let sac_address: Address = env.storage().instance().get(&symbol_short!("SAC_ADDR")).unwrap(); let token_client = token::StellarAssetClient::new(&env, &sac_address); token_client.trust(&receiver); token_client.mint(&receiver, &amount); } } ``` Notice the contract calls the SAC's [`trust` function](../../../tokens/stellar-asset-contract.mdx#creating-trustlines-from-a-contract) before minting. As of Yardstick, Protocol 26 ([CAP-73](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0073.md)), this creates the receiver's `STAR` trustline as part of the same invocation, so a `G...` receiver doesn't need to set up a trustline in a separate transaction beforehand. The call is a no-op if the receiver is a contract address or already has the trustline, and it requires the receiver's authorization (which they're already providing by invoking `claim_airdrop`). This contract gets deployed, and the address might be (for example) `CDF4VYXJE2KPLIZHQAX4XQRZ3QAMVQJRTOCS3B3GEKONHMYSYK6QID7B`. We can now set the SAC `Admin` address to this contract: ```sh stellar contract invoke \ --source-account starIssuer --network testnet \ --id CBVYF2KJ72BRPLVPCUL3PGWDO5RK2XP4AJDHKX7GDDBJW42L2C6VT3SF \ -- \ set_admin --new_admin CDF4VYXJE2KPLIZHQAX4XQRZ3QAMVQJRTOCS3B3GEKONHMYSYK6QID7B ``` Double-checking our work with the `admin` function of the SAC, we'll see that our airdrop contract is now the `Admin` address: ```shell stellar contract invoke \ --source-account starIssuer --network testnet \ --id CBVYF2KJ72BRPLVPCUL3PGWDO5RK2XP4AJDHKX7GDDBJW42L2C6VT3SF \ -- \ admin # CDF4VYXJE2KPLIZHQAX4XQRZ3QAMVQJRTOCS3B3GEKONHMYSYK6QID7B ``` Now, since the airdrop contract is the administrator of the SAC, we can invoke its `claim_airdrop` function from any account we like. The contract's `trust` call takes care of creating the asset's trustline for the receiving account, so the receiver only needs enough XLM to cover the new trustline's [base reserve](../../../learn/fundamentals/lumens.mdx#base-reserves). ```sh stellar contract invoke \ --source-account randomStarHolder --network testnet \ --id CDF4VYXJE2KPLIZHQAX4XQRZ3QAMVQJRTOCS3B3GEKONHMYSYK6QID7B \ -- \ claim_airdrop --receiver randomStarHolder ``` Now, this `randomStarHolder` account will have a balance of 12.3456789 `STAR`, and the token minting was executed by the airdrop contract itself. ### Making a `payment` operation from the issuer account Since we haven't locked down the signer(s) of our issuer account, it's still entirely possible for the `GCS5...` issuer account to create and submit to the network any `payment` operations it wants. This means tokens could still be minted with a `payment` operation, or burned with a `clawback` operation, or trustline flags modified, etc. all using the original asset issuer's account. The `Admin` address of the SAC and the signing permissions of the issuer account operate and function independently of one another. ## In the wild A fun example of a project utilizing a custom administrator address on a SAC can be found with the [`KALE` Project](https://github.com/kalepail/KALE-sc). The `KALE` issuer is `GBDVX4VELCDSQ54KQJYTNHXAHFLBCA77ZY2USQBM4CSHTTV7DME7KALE`, but you can see the `Admin` address for the SAC is set to another contract: ```sh stellar contract invoke \ --source-account S...ECRETKEY --network mainnet \ --id CB23WRDQWGSP6YPMY4UV5C4OW5CBTXKYN3XEATG7KJEZCXMJBYEHOUOV \ -- \ admin # CDL74RF5BLYR2YBLCCI7F5FB6TPSCLKEJUBSD2RSVWZ4YHF3VMFAIGWA ``` This [`CDL7...` contract](https://stellar.expert/explorer/public/contract/CDL74RF5BLYR2YBLCCI7F5FB6TPSCLKEJUBSD2RSVWZ4YHF3VMFAIGWA) is the "homestead" contract created to facilitate the minting/burning/mining of the `KALE` asset. --- ## Deploy a Stellar Asset Contract (SAC) from within a contract {`Deploying a Stellar Asset Contract (SAC) from within a contract`} ## Overview In this guide, you'll learn how to deploy a [Stellar Asset Contract (SAC)](../../../tokens/stellar-asset-contract.mdx) from within another smart contract using the Soroban Rust SDK. The Soroban Rust SDK provides tools and utilities for working with Stellar smart contracts, allowing you to deploy and interact with SACs directly from your contract logic. ## Prerequisites: Before you begin, make sure you have the following: - Basic understanding of [Rust programming language](https://www.rust-lang.org). To brush up on Rust, check out [Rustlings](https://github.com/rust-lang/rustlings) or [The Rust book](https://doc.rust-lang.org/book). - [Soroban Rust SDK](../../../tools/sdks/contract-sdks.mdx#soroban-rust-sdk) installed and configured in your development environment. - Basic understanding of the Soroban Rust SDK and familiarity with Soroban's core concepts and Rust programming. ## 1. Define the SacDeployer contract The SacDeployer contract will be responsible for deploying the Stellar Asset Contract. Here is the code for the SacDeployer contract: ```rust title="lib.rs" use soroban_sdk::{contract, contractimpl, Env, Address, Bytes}; #[contract] pub struct SacDeployer; #[contractimpl] impl SacDeployer { pub fn deploy_sac(env: Env, serialized_asset: Bytes) -> Address { // Create the Deployer with Asset let deployer = env.deployer().with_stellar_asset(serialized_asset); let _ = deployer.deployed_address(); // Deploy the Stellar Asset Contract let sac_address = deployer.deploy(); sac_address } } ``` ### Explanation - `SacDeployer` contract: this contract defines the `deploy_sac` function to handle the deployment of the SAC. - `deploy_sac` function: - `env.deployer().with_stellar_asset(serialized_asset)`: creates a deployer configured to deploy a Stellar Asset Contract using the provided serialized asset. - `deployer.deploy()`: Deploys the SAC and returns the address of the deployed contract. ## 2. Testing the deployment You need to test the deployment to ensure everything works as expected. The following code demonstrates how to test the SacDeployer contract using the Soroban Rust SDK's test utilities. ```rust #[test] fn test() { use soroban_sdk::{ xdr::{Asset, Limits, WriteXdr}, Bytes, Env, }; let env = Env::default(); let contract_id = env.register(SacDeployer, ()); let client = SacDeployerClient::new(&env, &contract_id); let serialized_asset = Bytes::from_slice(&env, &Asset::Native.to_xdr(Limits::none()).unwrap()); let sac_address = client.deploy_sac(&serialized_asset); assert_eq!(sac_address, env.deployer().with_stellar_asset(serialized_asset).deployed_address()); } ``` ### Explanation - `env.register(SacDeployer, ())`: registers the `SacDeployer` contract in the test environment and returns its contract ID. - `SacDeployerClient::new(&env, &contract_id)`: creates a client used to invoke the deployed `SacDeployer` contract's functions. - `serialized_asset`: the Stellar `Asset` XDR serialized to bytes. This example serializes the native XLM asset via [`soroban_sdk::xdr::Asset`](https://docs.rs/soroban-sdk/latest/soroban_sdk/xdr/enum.Asset.html); for an issued asset, use `Asset::CreditAlphanum4`/`Asset::CreditAlphanum12` instead. - `client.deploy_sac(&serialized_asset)`: invokes the contract, which deploys the SAC and returns its address. - The assertion confirms the returned address matches the deterministic address the SDK computes for that same serialized asset. ### Conclusion By following this guide, you’ve successfully deployed a Stellar Asset Contract from within another contract using the Soroban Rust SDK. This approach enables smart contracts to handle SAC deployments dynamically, providing flexibility for various use cases in the Stellar ecosystem. For further details, refer to the Soroban SDK documentation and explore more advanced features and configurations. --- ## Integrate Stellar Assets Contracts When interacting with assets in a smart contract, the [Stellar Asset Contract](../../../tokens/stellar-asset-contract.mdx) is not different from any other token that implements the Stellar [SEP-41 Token Interface]. ## Contract Code The Rust SDK contains a pre-generated client for any contract that implements the token interface: ```rust use soroban_sdk::{contract, contractimpl} use soroban_sdk::token; #[contract] pub struct MyContract; #[contractimpl] impl MyContract { pub fn token_fn(e: Env, id: Address) { // Create a client instance for the provided token identifier. If the id // value corresponds to an SAC contract, then SAC implementation is used. let client = token::TokenClient::new(&e, &id); // Call token functions part of the Stellar SEP-41 token interface client.transfer(...); } } ``` The `asset` parameter is not the address of the issuer of an asset, it corresponds to the deployed contract address for this asset. ```bash stellar contract id asset \ --source-account G... \ --network testnet \ --asset [asset:issuer] ``` E.g. for USDC, it would be `--asset USDC:G...` For the native asset, XLM, `--asset native`. See the [deploy SAC] guide for more details. [deploy SAC]: ../../../tools/cli/cookbook/deploy-stellar-asset-contract.mdx :::info[Clients] A client created by [`token::TokenClient`] implements the functions defined by any contract that implements the [SEP-41 Token Interface]. But with [CAP-46-6 smart contract standardized asset], the Stellar Asset Contract exposes additional functions such as `mint`. To access the additional functions, another client needs to be used: [`token::StellarAssetClient`]. This client only implements the functions from CAP-46-6, which are not part of the SEP-41 interface. ```rust let client = token::StellarAssetClient::new(&env, &id); // Call token functions which are not part of the SEP-41 token interface // but part of the CAP-46-6 Smart Contract Standardized Asset client.mint(...); ``` ::: ## Testing Soroban Rust SDK provides an easy way to instantiate a Stellar Asset Contract tokens using `register_stellar_asset_contract_v2`. This function can be seen as the deployment of a generic token. It also allows you to manipulate flags on the issuer account like `AUTH_REVOCABLE` and `AUTH_REQUIRED`. In the following example, we are following the best practices outlined in the [Issuing and Distribution Accounts section](../../../tokens/control-asset-access.mdx#issuing-and-distribution-accounts): ```rust #![cfg(test)] use soroban_sdk::testutils::Address as _; use soroban_sdk::{token, Address, Env}; use token::{StellarAssetClient, TokenClient}; #[test] fn test() { let e = Env::default(); e.mock_all_auths(); let issuer = Address::generate(&e); let distributor = Address::generate(&e); let sac = e.register_stellar_asset_contract_v2(issuer.clone()); let token_address = sac.address(); // client for SEP-41 functions let token = TokenClient::new(&e, &token_address); // client for Stellar Asset Contract functions let token_sac = StellarAssetClient::new(&e, &token_address); // note that you need to account for the difference between the minimal // unit and the unit itself when working with amounts. // E.g. to mint 1 TOKEN, we need to use 1*1e7 in the mint function. let genesis_amount: i128 = 1_000_000_000 * 10_000_000; token_sac.mint(&distributor, &genesis_amount); assert_eq!(token.balance(&distributor), genesis_amount); // Make issuer AuthRequired and AuthRevocable sac.issuer().set_flag(IssuerFlags::RevocableFlag); sac.issuer().set_flag(IssuerFlags::RequiredFlag); } ``` ## Examples See the full examples that utilize the token contract in various ways for more details: - [Timelock](../../smart-contracts/example-contracts/timelock.mdx) and [single offer](../../smart-contracts/example-contracts/single-offer-sale.mdx) move token via `transfer` to and from the contract - [Atomic swap](../../smart-contracts/example-contracts/atomic-swap.mdx) uses `transfer` to transfer token on behalf of the user [sep-41 token interface]: ../../../tokens/token-interface.mdx [cap-46-6 smart contract standardized asset]: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-06.md [`token::tokenclient`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/struct.TokenClient.html [`token::stellarassetclient`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/struct.StellarAssetClient.html --- ## Transactions Stellar transactions are comprised of operations. These guides will walk you through various Stellar operations, non-contract transactions, and smart contract transactions. Read more about operations and transactions in the [Operations and Transactions section](../../../learn/fundamentals/transactions/README.mdx). --- ## Channel accounts Channel accounts provide a method for submitting transactions to the network at a high rate. An account’s transactions always need to be submitted to the network in increments of one sequence number (unless minimum sequence number preconditions are set). This can cause problems if you are submitting transactions at a high rate, as they can potentially reach Stellar Core out of order and will then bounce with a bad sequence error. To avoid this, you can create separate channel accounts that can be used as the source account for the transaction and use the account holding the assets as the base account or the source account for the individual operations in the transaction. In this scenario, the assets will come out of the base account, and the sequence number and fees will be consumed by the channel account. Channels take advantage of the fact that the source account of a transaction can be different than the source account of the operations inside the transaction. With this setup, you can make as many channels as you need to maintain your desired transaction rate. You will, of course, have to sign the transaction with both the base account key and the channel account key. For example: ```js // channelAccounts[] is an array of accountIDs, one for each channel // channelKeys[] is an array of secret keys, one for each channel // channelIndex is the channel you want to send this transaction over // create payment from baseAccount to customerAddress var transaction = new StellarSdk.TransactionBuilder( channelAccounts[channelIndex], { fee: StellarSdk.BASE_FEE, networkPassphrase: StellarSdk.Networks.TESTNET, }, ) .addOperation( StellarSdk.Operation.payment({ source: baseAccount.address(), destination: customerAddress, asset: StellarSdk.Asset.native(), amount: amountToSend, }), ) // Wait a maximum of three minutes for the transaction .setTimeout(180) .build(); transaction.sign(baseAccountKey); // base account must sign to approve the payment transaction.sign(channelKeys[channelIndex]); // channel must sign to approve it being the source of the transaction ``` ```python # channelAccounts[] is an array of accountIDs, one for each channel # channelKeys[] is an array of secret keys, one for each channel # channelIndex is the channel you want to send this transaction over transaction = ( TransactionBuilder( source_account=channelAccounts[channelIndex], network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, base_fee=base_fee, ) .append_payment_op( source=baseAccount.public_key, destination=customerAddress, asset=Asset.native(), amount=amountToSend, ) .set_timeout(180) # Wait a maximum of three minutes for the transaction .build() ) transaction.sign(baseAccountKey) # base account must sign to approve the payment transaction.sign(channelKeys[channelIndex]) # channel must sign to approve it being the source of the transaction ``` --- ## Claimable balances Claimable balances were introduced in [CAP-23](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0023.md) and are used to split a payment into two parts. - Part 1: sending account creates a payment, or ClaimableBalanceEntry, using the Create Claimable Balance operation - Part 2: destination account(s), or claimant(s), accepts the ClaimableBalanceEntry using the Claim Claimable Balance operation Claimable balances allow an account to send a payment to another account that is not necessarily prepared to receive the payment. They can be used when you send a non-native asset to an account that has not yet established a trustline, which can be useful for anchors onboarding new users. A trustline must be established by the claimant to the asset before it can claim the claimable balance, otherwise, the claim will result in an `op_no_trust` error. It is important to note that if a claimable balance isn’t claimed, it sits on the ledger forever, taking up space and ultimately making the network less efficient. **For this reason, it is a good idea to put one of your own accounts as a claimant for a claimable balance.** Then you can accept your own claimable balance if needed, freeing up space on the network. Each ClaimableBalanceEntry is a ledger entry, and each claimant in that entry increases the source account’s minimum balance by one base reserve. Once a ClaimableBalanceEntry has been claimed, it is deleted. ## Operations ### Create Claimable Balance For basic parameters, see the Create Claimable Balance entry in our [List of Operations section](../../../learn/fundamentals/transactions/list-of-operations.mdx#create-claimable-balance). #### Additional parameters `Claim_Predicate_` Claimant — an object that holds both the destination account that can claim the ClaimableBalanceEntry and a ClaimPredicate that must evaluate to true for the claim to succeed. A ClaimPredicate is a recursive data structure that can be used to construct complex conditionals using different ClaimPredicateTypes. Below are some examples with the `Claim_Predicate_` prefix removed for readability. Note that the SDKs expect the Unix timestamps to be expressed in seconds. - Can claim at any time - `UNCONDITIONAL` - Can claim if the close time of the ledger, including the claim is before X seconds + the ledger close time in which the ClaimableBalanceEntry was created - `BEFORE_RELATIVE_TIME(X)` - Can claim if the close time of the ledger including the claim is before X (Unix timestamp) - `BEFORE_ABSOLUTE_TIME(X)` - Can claim if the close time of the ledger, including the claim is at or after X seconds + the ledger close time in which the ClaimableBalanceEntry was created - `NOT(BEFORE_RELATIVE_TIME(X))` - Can claim if the close time of the ledger, including the claim is at or after X (Unix timestamp) - `NOT(BEFORE_ABSOLUTE_TIME(X))` - Can claim between X and Y Unix timestamps (given X < Y) - `AND(NOT(BEFORE_ABSOLUTE_TIME(X))`, `BEFORE_ABSOLUTE_TIME(Y))` - Can claim outside X and Y Unix timestamps (given X < Y) - `OR(BEFORE_ABSOLUTE_TIME(X)`, `NOT(BEFORE_ABSOLUTE_TIME(Y))` `ClaimableBalanceID` ClaimableBalanceID is a union with one possible type (`CLAIMABLE_BALANCE_ID_TYPE_V0`). It contains an SHA-256 hash of the OperationID for Claimable Balances. A successful Create Claimable Balance operation will return a Balance ID, which is required when claiming the ClaimableBalanceEntry with the Claim Claimable Balance operation. ### Claim Claimable Balance For basic parameters, see the Claim Claimable Balance entry in our [List of Operations section](../../../learn/fundamentals/transactions/list-of-operations#claim-claimable-balance). This operation will load the ClaimableBalanceEntry that corresponds to the Balance ID and then search for the source account of this operation in the list of claimants on the entry. If a match on the claimant is found, and the ClaimPredicate evaluates to true, then the ClaimableBalanceEntry can be claimed. The balance on the entry will be moved to the source account if there are no limit or trustline issues (for non-native assets), meaning the claimant must establish a trustline to the asset before claiming it. ### Clawback Claimable Balance This operation claws back a claimable balance, returning the asset to the issuer account, burning it. You must claw back the entire claimable balance, not just part of it. Once a claimable balance has been claimed, use the regular clawback operation to claw it back. Clawback claimable balances require the claimable balance ID. Learn more about clawbacks in our [Clawback Guide](./clawbacks.mdx). ## Example The below code demonstrates via both the JavaScript and Go SDKs how an account (Account A) creates a ClaimableBalanceEntry with two claimants: Account A (itself) and Account B (another recipient). Each of these accounts can only claim the balance under unique conditions. Account B has a full minute to claim the balance before Account A can reclaim the balance back for itself. **Note:** there is no recovery mechanism for a claimable balance in general — if none of the predicates can be fulfilled, the balance cannot be recovered. The reclaim example below acts as a safety net for this situation.
```go func fundAccount(rpcClient *client.Client, address string) error { ctx := context.Background() // Use GetNetwork method from client networkResp, err := rpcClient.GetNetwork(ctx) if err != nil { return err } if networkResp.FriendbotURL != "" { friendbotURL := networkResp.FriendbotURL + "?addr=" + url.QueryEscape(address) resp, err := http.Post(friendbotURL, "application/x-www-form-urlencoded", nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("friendbot failed with status: %d", resp.StatusCode) } return nil } return fmt.Errorf("friendbot not configured for network - %s", networkResp.Passphrase) } func panicIf(err error) { if err != nil { log.Fatal(err) } } ```
```js /** * Creates a claimable balance on Stellar testnet * A claimable balance allows splitting a payment into two parts: * 1. Sender creates the claimable balance * 2. Recipient(s) can claim it later */ async function createClaimableBalance() { // Connect to Stellar testnet RPC server const server = new StellarSdk.rpc.Server( "https://soroban-testnet.stellar.org", ); const A = StellarSdk.Keypair.random(); const B = StellarSdk.Keypair.random(); console.log( `Account A... public key: ${A.publicKey()}, secret: ${A.secret()}`, ); console.log( `Account B... public key: ${B.publicKey()}, secret: ${B.secret()}`, ); try { // Fund the source account using testnet's built-in airdrop await server.requestAirdrop(A.publicKey()); // Load the funded account to get current sequence number const aAccount = await server.getAccount(A.publicKey()); console.log(`Account sequence: ${aAccount.sequenceNumber()}`); // Create a claimable balance with our two above-described conditions. let soon = Math.ceil(Date.now() / 1000 + 60); // .now() is in ms let bCanClaim = StellarSdk.Claimant.predicateBeforeRelativeTime("60"); let aCanReclaim = StellarSdk.Claimant.predicateNot( StellarSdk.Claimant.predicateBeforeAbsoluteTime(soon.toString()), ); // Create claimable balance operation const claimableBalanceOp = StellarSdk.Operation.createClaimableBalance({ claimants: [ new StellarSdk.Claimant(B.publicKey(), bCanClaim), new StellarSdk.Claimant(A.publicKey(), aCanReclaim), ], asset: StellarSdk.Asset.native(), amount: "420", }); // Build the transaction console.log(`Building transaction...`); const transaction = new StellarSdk.TransactionBuilder(aAccount, { fee: StellarSdk.BASE_FEE, networkPassphrase: StellarSdk.Networks.TESTNET, }) .addOperation(claimableBalanceOp) .setTimeout(180) .build(); /* Claimable BalanceIds are predictable and can be derived from the Sha256 hash of the operation that creates them. */ const predictableBalanceId = transaction.getClaimableBalanceId(0); // Sign the transaction with source account transaction.sign(A); // Submit transaction to the network console.log(`Submitting transaction...`); const response = await server.sendTransaction(transaction); // Poll for transaction completion (RPC is asynchronous) console.log(`Polling for result...`); const finalResponse = await server.pollTransaction(response.hash); if (finalResponse.status === "SUCCESS") { // Extract claimable balance ID from transaction result const txResult = finalResponse.resultXdr; const results = txResult.result().results(); const operationResult = results[0].value().createClaimableBalanceResult(); const balanceId = operationResult.balanceId().toXDR("hex"); console.log(`Balance ID (from txResult): ${balanceId}`); console.log( `Predictable Balance ID (obtained before txSubmission): ${predictableBalanceId}`, ); if (balanceId === predictableBalanceId) { console.log(`Balance ID from txResult matches the predictable ID`); } else { console.log( ` Balance ID from txResult does NOT match the predictable ID`, ); } } else { console.log(`Transaction failed: ${finalResponse.status}`); } } catch (error) { console.error(`Error: ${error.message}`); } } // Run the function createClaimableBalance(); ``` ```go package main "context" "fmt" "log" "net/http" "net/url" "time" client "github.com/stellar/go-stellar-sdk/clients/rpcclient" protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" "github.com/stellar/go-stellar-sdk/keypair" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/txnbuild" "github.com/stellar/go-stellar-sdk/xdr" ) func main() { // Create RPC client rpcClient := client.NewClient("https://soroban-testnet.stellar.org", nil) defer rpcClient.Close() // Generate random keypairs A := keypair.MustRandom() B := keypair.MustRandom() fmt.Printf("Account A: public key: %s, secret key: %s\n", A.Address(), A.Seed()) fmt.Printf("Account B: public key: %s\n", B.Address()) // Fund account using GetNetwork + friendbot fmt.Println("\nFunding account...") panicIf(fundAccount(rpcClient, A.Address())) fmt.Println("Account funded") // Wait for funding time.Sleep(3 * time.Second) // Use LoadAccount method from the client ctx := context.Background() sourceAccount, err := rpcClient.LoadAccount(ctx, A.Address()) panicIf(err) // Create a claimable balance with our two above-described conditions. soon := time.Now().Add(time.Second * 60) bCanClaim := txnbuild.BeforeRelativeTimePredicate(60) aCanReclaim := txnbuild.NotPredicate( txnbuild.BeforeAbsoluteTimePredicate(soon.Unix()), ) // Create claimable balance operation claimableBalanceOp := txnbuild.CreateClaimableBalance{ Destinations: []txnbuild.Claimant{ txnbuild.NewClaimant(B.Address(), &bCanClaim), txnbuild.NewClaimant(A.Address(), &aCanReclaim), }, Asset: txnbuild.NativeAsset{}, Amount: "1", } // Build transaction tx, err := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: sourceAccount, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewInfiniteTimeout()}, Operations: []txnbuild.Operation{&claimableBalanceOp}, }, ) panicIf(err) // Sign transaction tx, err = tx.Sign(network.TestNetworkPassphrase, A) panicIf(err) // Get transaction XDR txXDR, err := tx.Base64() panicIf(err) // Submit using RPC client's SendTransaction method fmt.Println("Submitting transaction...") sendResp, err := rpcClient.SendTransaction(ctx, protocol.SendTransactionRequest{ Transaction: txXDR, }) panicIf(err) if sendResp.Status != "PENDING" { log.Fatalf("Transaction not pending: %s", sendResp.Status) } fmt.Printf("Transaction submitted: %s\n", sendResp.Hash) // Poll using RPC client's GetTransaction method fmt.Println("Polling for result...") for i := 0; i < 10; i++ { resp, err := rpcClient.GetTransaction(ctx, protocol.GetTransactionRequest{ Hash: sendResp.Hash, }) if err != nil { log.Printf("Error getting transaction: %v", err) time.Sleep(1 * time.Second) continue } if resp.Status != protocol.TransactionStatusNotFound { if resp.Status == protocol.TransactionStatusSuccess { // Extract balance ID balanceID, err := extractBalanceID(&resp) if err != nil { log.Printf("Error extracting balance ID: %v", err) } else { fmt.Println("\nSUCCESS: Claimable balance created") fmt.Printf("Balance ID: %s\n", balanceID) } } else { fmt.Printf("Transaction failed: %s\n", resp.Status) } return } time.Sleep(time.Duration(i+1) * time.Second) } fmt.Println("Transaction polling timeout") } func extractBalanceID(resp *protocol.GetTransactionResponse) (string, error) { if resp.ResultXDR == "" { return "", fmt.Errorf("no result XDR") } var txResult xdr.TransactionResult err := xdr.SafeUnmarshalBase64(resp.ResultXDR, &txResult) if err != nil { return "", err } if results, ok := txResult.OperationResults(); ok && len(results) > 0 { operationResult := results[0].MustTr().CreateClaimableBalanceResult return xdr.MarshalHex(operationResult.BalanceId) } return "", fmt.Errorf("no operation results") } ``` At this point, the `ClaimableBalanceEntry` exists in the ledger, but we’ll need its Balance ID to claim it. You can call the RPC's [`getLedgerEntries`](../../../data/apis/rpc/api-reference/methods/getLedgerEntries.mdx) endpoint to do this. ```js // Replace with your actual Claimable Balance ID // Format: 72 hex characters (includes ClaimableBalanceId type + hash) const BALANCE_ID = "00000000db1108ff108a807150d02b8672d9a8c0e808bff918cdbe5c7605e63a7f565df5"; /** * Fetches and displays claimable balance details using Stellar RPC */ async function fetchClaimableBalance(balanceId) { const server = new StellarSdk.rpc.Server( "https://soroban-testnet.stellar.org", ); try { console.log(`Looking up balance ID: ${balanceId}`); // Parse the claimable balance ID from hex XDR const claimableBalanceId = StellarSdk.xdr.ClaimableBalanceId.fromXDR( balanceId, "hex", ); // Create ledger key for the claimable balance entry const ledgerKey = StellarSdk.xdr.LedgerKey.claimableBalance( new StellarSdk.xdr.LedgerKeyClaimableBalance({ balanceId: claimableBalanceId, }), ); console.log(`Fetching from RPC server...`); // Use SDK's getLedgerEntries method with XDR object array const response = await server.getLedgerEntries(ledgerKey); if (response.entries && response.entries.length > 0) { const claimableBalance = response.entries[0].val.claimableBalance(); const asset = StellarSdk.Asset.fromOperation(claimableBalance.asset()); console.log(`Found claimable balance`); console.log(`Amount: ${claimableBalance.amount().toString()}`); console.log(`Asset: ${asset.toString()} `); // Show claimant details console.log(`\nClaimants:`); claimableBalance.claimants().forEach((claimant, index) => { const destination = claimant.v0().destination().ed25519(); console.log( ` ${index + 1}. ${StellarSdk.StrKey.encodeEd25519PublicKey( destination, )}`, ); }); } else { console.log(`Claimable balance not found`); } } catch (error) { console.error(`Error: ${error.message}`); } } fetchClaimableBalance(BALANCE_ID); ``` ```go package main "context" "fmt" client "github.com/stellar/go-stellar-sdk/clients/rpcclient" protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" "github.com/stellar/go-stellar-sdk/xdr" ) // Replace with your claimable balance ID const BALANCE_ID = "00000000a4c91c4561f2d8b30dad9cf6475221b3003a3b4e12fc0cf78a13251c0e7ff665" func main() { // Create RPC client rpcClient := client.NewClient("https://soroban-testnet.stellar.org", nil) defer rpcClient.Close() fmt.Printf("Looking up balance ID: %s\n", BALANCE_ID) // Parse claimable balance ID from hex var claimableBalanceID xdr.ClaimableBalanceId err := xdr.SafeUnmarshalHex(BALANCE_ID, &claimableBalanceID) panicIf(err) // Create ledger key for claimable balance ledgerKey := xdr.LedgerKey{ Type: xdr.LedgerEntryTypeClaimableBalance, ClaimableBalance: &xdr.LedgerKeyClaimableBalance{ BalanceId: claimableBalanceID, }, } // Convert ledger key to base64 for RPC call ledgerKeyB64, err := xdr.MarshalBase64(ledgerKey) panicIf(err) fmt.Println("Fetching from RPC server...") // Use GetLedgerEntries method from client ctx := context.Background() resp, err := rpcClient.GetLedgerEntries(ctx, protocol.GetLedgerEntriesRequest{ Keys: []string{ledgerKeyB64}, }) panicIf(err) if len(resp.Entries) > 0 { entry := resp.Entries[0] fmt.Println("Found claimable balance") // Parse the ledger entry XDR var ledgerEntryData xdr.LedgerEntryData err = xdr.SafeUnmarshalBase64(entry.DataXDR, &ledgerEntryData) panicIf(err) claimableBalance := ledgerEntryData.ClaimableBalance // Display details fmt.Printf("Amount: %d\n", int64(claimableBalance.Amount)) fmt.Printf("Asset: %s\n", claimableBalance.Asset.String()) // Show claimants fmt.Println("\nClaimants:") for i, claimant := range claimableBalance.Claimants { address := claimant.V0.Destination.Address() fmt.Printf(" %d. %s\n", i+1, address) } } else { fmt.Println("Claimable balance not found") } } ``` With the Claimable Balance ID acquired, either Account B or A can actually submit a claim, depending on which predicate is fulfilled. We’ll assume here that a minute has passed, so Account A just reclaims the balance entry. ```js // Replace with your claimable balance ID const BALANCE_ID = "0000000067a94da6c5d487fa09fc93c558ca91f6338413d3152d2a17771353f7c4111e11"; // Replace with the secret key of one of the claimants const CLAIMANT_SECRET = "SDJLAUDIHMDO6PAIVVVYH5IFIE5QMZOOBHO37NLF43335ULECK6EURVJ"; /** * Claims a claimable balance */ async function claimClaimableBalance(balanceId, claimantSecret) { const server = new StellarSdk.rpc.Server( "https://soroban-testnet.stellar.org", ); try { console.log(`Claiming balance ID: ${balanceId}`); // Create keypair from claimant's secret key const claimantKeypair = StellarSdk.Keypair.fromSecret(claimantSecret); // Load the claiming account const claimantAccount = await server.getAccount( claimantKeypair.publicKey(), ); // Convert balance ID to proper format for the operation const claimableBalanceId = StellarSdk.xdr.ClaimableBalanceId.fromXDR( balanceId, "hex", ); const balanceIdHex = claimableBalanceId.toXDR("hex"); // Create claim operation const claimOperation = StellarSdk.Operation.claimClaimableBalance({ balanceId: balanceIdHex, }); // Build and sign transaction const transaction = new StellarSdk.TransactionBuilder(claimantAccount, { fee: StellarSdk.BASE_FEE, networkPassphrase: StellarSdk.Networks.TESTNET, }) .addOperation(claimOperation) .setTimeout(180) .build(); transaction.sign(claimantKeypair); // Submit and poll for completion const response = await server.sendTransaction(transaction); const finalResponse = await server.pollTransaction(response.hash); if (finalResponse.status === "SUCCESS") { console.log(`Claimable balance claimed successfully`); console.log(`Transaction hash: ${response.hash}`); } else { console.log(`Transaction failed: ${finalResponse.status}`); } } catch (error) { console.error(`Error: ${error.message}`); } } claimClaimableBalance(BALANCE_ID, CLAIMANT_SECRET); ``` ```go package main "context" "fmt" "log" "time" client "github.com/stellar/go-stellar-sdk/clients/rpcclient" protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" "github.com/stellar/go-stellar-sdk/keypair" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/txnbuild" ) // Replace with your claimable balance ID and claimant secret const BALANCE_ID = "00000000a4c91c4561f2d8b30dad9cf6475221b3003a3b4e12fc0cf78a13251c0e7ff665" const CLAIMANT_SECRET = "SBMODTOLLH2LGF4AJU5XOGHRQCGKEVJUZSAUAGJL7KGKC7XLJ3SG3F7N" func main() { // Create RPC client rpcClient := client.NewClient("https://soroban-testnet.stellar.org", nil) defer rpcClient.Close() // Create keypair from claimant secret keypairAccA, err := keypair.ParseFull(CLAIMANT_SECRET) panicIf(err) fmt.Printf("Claiming account: %s\n", keypairAccA.Address()) fmt.Printf("Balance ID: %s\n", BALANCE_ID) ctx := context.Background() // Load the claimant account using client's LoadAccount method accountA, err := rpcClient.LoadAccount(ctx, keypairAccA.Address()) panicIf(err) // Create claim claimable balance operation claimOp := txnbuild.ClaimClaimableBalance{ BalanceID: BALANCE_ID, } // Build transaction fmt.Println("Building claim transaction...") tx, err := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: accountA, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewInfiniteTimeout()}, Operations: []txnbuild.Operation{&claimOp}, }, ) panicIf(err) // Sign transaction tx, err = tx.Sign(network.TestNetworkPassphrase, keypairAccA) panicIf(err) // Get transaction XDR txXDR, err := tx.Base64() panicIf(err) // Submit using RPC client's SendTransaction method fmt.Println("Submitting claim transaction...") sendResp, err := rpcClient.SendTransaction(ctx, protocol.SendTransactionRequest{ Transaction: txXDR, }) panicIf(err) if sendResp.Status != "PENDING" { log.Fatalf("Transaction not pending: %s", sendResp.Status) } fmt.Printf("Transaction submitted: %s\n", sendResp.Hash) // Poll for completion using RPC client's GetTransaction method fmt.Println("Polling for result...") for i := 0; i < 10; i++ { resp, err := rpcClient.GetTransaction(ctx, protocol.GetTransactionRequest{ Hash: sendResp.Hash, }) if err != nil { log.Printf("Error getting transaction: %v", err) time.Sleep(1 * time.Second) continue } if resp.Status != protocol.TransactionStatusNotFound { if resp.Status == protocol.TransactionStatusSuccess { fmt.Println("\nSUCCESS: Claimable balance claimed") fmt.Printf("Transaction hash: %s\n", sendResp.Hash) fmt.Printf("Claimed by: %s\n", keypairAccA.Address()) } else { fmt.Printf("Transaction failed: %s\n", resp.Status) if resp.ResultXDR != "" { fmt.Printf("Result XDR: %s\n", resp.ResultXDR) } } return } time.Sleep(time.Duration(i+1) * time.Second) } fmt.Println("Transaction polling timeout") } ``` And that’s it! Since we opted for the reclaim path, Account A should have the same balance as what it started with (minus fees), and Account B should be unchanged. --- ## Clawbacks Clawbacks were introduced in [CAP-35: Asset Clawback](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0035.md) and allow an asset issuer to burn a specific amount of a clawback-enabled asset from a trustline or claimable balance, effectively destroying it and removing it from a recipient's balance. They were designed to allow asset issuers to meet securities regulations, which in many jurisdictions require asset issuers (or designated transfer agents) to have the ability to revoke assets in the event of a mistaken or fraudulent transaction or other regulatory action regarding a specific person or asset. Clawbacks are useful for: - Recovering assets that have been fraudulently obtained - Responding to regulatory actions - Enabling identity-proofed persons to recover an enabled asset in the event of loss of key custody or theft ## Operations ### Set Options The issuer sets up their account to enable clawbacks using the `AUTH_CLAWBACK_ENABLED` flag. This causes every subsequent trustline established to any assets issued by that account to have the `TRUSTLINE_CLAWBACK_ENABLED_FLAG` set automatically. If an issuing account wants to set the `AUTH_CLAWBACK_ENABLED_FLAG`, it must have the `AUTH_REVOCABLE_FLAG` set. This allows an asset issuer to claw back balances locked up in offers by first revoking authorization from a trustline, which pulls all offers that involve that trustline. The issuer can then perform the clawback. ### Clawback The issuing account uses this operation to claw back some or all of an asset. Once an account holds a particular asset for which clawbacks have been enabled, the issuing account can claw it back, burning it. You need to provide the asset, a quantity, and the account from which you're clawing back the asset. For more details, refer the [Clawback operation](../../../learn/fundamentals/transactions/list-of-operations.mdx#clawback). ### Clawback Claimable Balance This operation claws back a claimable balance, returning the asset to the issuer account, burning it. You must claw back the entire claimable balance, not just part of it. Once a claimable balance has been claimed, use the regular clawback operation to claw it back. Clawback claimable balances require the claimable balance ID. For more details, refer the [Clawback Claimable Balance operation](../../../learn/fundamentals/transactions/list-of-operations.mdx#clawback-claimable-balance). ### Set Trust Line Flag The issuing account uses this operation to remove clawback capabilities on a specific trustline by removing the `TRUSTLINE_CLAWBACK_ENABLED_FLAG` via the [**SetTrustLineFlags**](../../../learn/fundamentals/transactions/list-of-operations.mdx#set-trustline-flags) operation. You can only clear a flag, not set it. So clearing a clawback flag on a trustline is irreversible. This is done so that you don't retroactively change the rules on your asset holders. If you'd like to enable clawbacks again, holders must reissue their trustlines. ## Examples Here we'll cover the following approaches to clawing back an asset. **Example 1:** Issuing account (Account A) creates a clawback-enabled asset and sends it to Account B. Account B sends that asset to Account C. Account A will then clawback the asset from C. **Example 2:** Account B creates a claimable balance for Account C, and Account A claws back the claimable balance. **Example 3:** Account A issues a clawback-enabled asset to Account B. A claws back some of the asset from B, then removes the clawback enabled flag from the trustline and can no longer clawback the asset. ### Preamble: Creating + Funding Accounts and Issuing a Clawback-able Asset First, we'll set up an account to enable clawbacks and issue an asset accordingly. Properly issuing an asset (with separate issuing and distribution accounts) is a little more involved, but we'll use a simpler method here. Also, note that we first need to enable clawbacks and then establish trustlines since you cannot retroactively enable clawback on existing trustlines. The following code snippet contains helper functions that will be used in the following examples. ```js let server = new sdk.rpc.Server("https://soroban-testnet.stellar.org"); const A = sdk.Keypair.random(); const B = sdk.Keypair.random(); const C = sdk.Keypair.random(); console.log("=== ACCOUNT SETUP ==="); console.log(`Account A (Issuer): ${A.publicKey()}`); console.log(`Account B (Trustor): ${B.publicKey()}`); console.log(`Account C (Trustor): ${C.publicKey()}`); console.log(); const ASSET = new sdk.Asset("CLAW", A.publicKey()); // Helper function to format account ID with label function formatAccount(accountId) { const shortId = accountId.substring(0, 8); if (accountId === A.publicKey()) { return `${shortId} (Account A)`; } else if (accountId === B.publicKey()) { return `${shortId} (Account B)`; } else if (accountId === C.publicKey()) { return `${shortId} (Account C)`; } return shortId; } // Helper function to safely scale XDR Int64 asset amounts function scaleAsset(x) { return Number((x * 10n) / 10000000n) / 10; // one decimal place } // Helper function to fetch claimable balance details using SDK's built-in method async function fetchClaimableBalance( balanceId, description = "Claimable Balance", ) { try { console.log(`\n--- Checking ${description} ---`); console.log(`Looking up balance ID: ${balanceId}`); // Use SDK's built-in getClaimableBalance method const claimableBalance = await server.getClaimableBalance(balanceId); const asset = sdk.Asset.fromOperation(claimableBalance.asset()); const amount = scaleAsset(claimableBalance.amount().toBigInt()).toFixed(1); console.log(`✅ Found claimable balance`); console.log(` Amount: ${amount} ${asset.code}`); console.log( ` Number of claimants: ${claimableBalance.claimants().length}`, ); // Show claimant details claimableBalance.claimants().forEach((claimant, index) => { const destination = claimant.v0().destination().ed25519(); const claimantAddress = sdk.StrKey.encodeEd25519PublicKey(destination); console.log( ` Claimant ${index + 1}: ${formatAccount(claimantAddress)}`, ); }); return true; // Balance exists } catch (error) { console.log(`❌ Claimable balance not found (${error.message})`); return false; // Balance doesn't exist } } // Fund accounts first function fundAccounts() { console.log("=== FUNDING ACCOUNTS WITH XLM ==="); return Promise.all([ server.requestAirdrop(A.publicKey()), server.requestAirdrop(B.publicKey()), server.requestAirdrop(C.publicKey()), ]).then(() => { console.log("All accounts funded with XLM via airdrop"); // Wait for funding to complete return new Promise((resolve) => setTimeout(resolve, 3000)); }); } // Enables AuthClawbackEnabledFlag on an account. function enableClawback(account, keys) { console.log( `Enabling clawback flags on account ${formatAccount(account.accountId())}`, ); return submitAndPollTransaction( buildTx(account, keys, [ sdk.Operation.setOptions({ setFlags: sdk.AuthClawbackEnabledFlag | sdk.AuthRevocableFlag, }), ]), "Enable Clawback Flags", ); } // Establishes a trustline for `recipient` for the CLAW Asset const establishTrustline = function (recipient, key) { console.log( `${formatAccount(recipient.accountId())} establishing trustline for ${ ASSET.code }`, ); return submitAndPollTransaction( buildTx(recipient, key, [ sdk.Operation.changeTrust({ asset: ASSET, limit: "5000", // arbitrary }), ]), `Establish Trustline (${formatAccount(recipient.accountId())})`, ); }; // Retrieves latest account info for all accounts. function getAccounts() { return Promise.all([ server.getAccount(A.publicKey()), server.getAccount(B.publicKey()), server.getAccount(C.publicKey()), ]); } // Show XLM balances (after funding) function showXLMBalances(accounts) { console.log("\n=== XLM BALANCES ==="); return Promise.all( accounts.map((acc) => { return getXLMBalance(acc.accountId()).then((balance) => { console.log(`${formatAccount(acc.accountId())}: ${balance} XLM`); }); }), ); } // Get XLM balance using account ledger entry function getXLMBalance(accountId) { return server .getAccountEntry(accountId) .then((accountEntry) => { return scaleAsset(accountEntry.balance().toBigInt()).toFixed(1); }) .catch(() => "0"); } // Show CLAW balances function showCLAWBalances(accounts) { console.log("\n=== CLAW BALANCES ==="); return Promise.all( accounts.map((acc) => { return getBalance(acc.accountId()).then((balance) => { console.log(`${formatAccount(acc.accountId())}: ${balance} CLAW`); }); }), ); } // Get CLAW balance using getTrustline function getBalance(accountId) { return server .getTrustline(accountId, ASSET) .then((trustlineEntry) => { return scaleAsset(trustlineEntry.balance().toBigInt()).toFixed(1); }) .catch(() => "0"); } // Helps simplify creating & signing a transaction. function buildTx(source, signer, ops) { var tx = new sdk.TransactionBuilder(source, { fee: sdk.BASE_FEE, networkPassphrase: sdk.Networks.TESTNET, }); ops.forEach((op) => tx.addOperation(op)); tx = tx.setTimeout(30).build(); tx.sign(signer); return tx; } // Helper function to submit transaction and poll for completion using RPC function submitAndPollTransaction(transaction, description = "Transaction") { return server.sendTransaction(transaction).then((submitResponse) => { if (submitResponse.status !== "PENDING") { throw new Error( `Transaction submission failed: ${submitResponse.status}`, ); } console.log(`${description} submitted: ${submitResponse.hash}`); return server.pollTransaction(submitResponse.hash).then((finalResponse) => { if (finalResponse.status === "SUCCESS") { console.log(`${description} completed successfully`); } else { console.log(`${description} failed: ${finalResponse.status}`); } return { hash: submitResponse.hash, status: finalResponse.status, resultXdr: finalResponse.resultXdr, }; }); }); } // Makes payment from `fromAccount` to `toAccount` of `amount` function makePayment(toAccount, fromAccount, fromKey, amount) { console.log( `\nPayment: ${formatAccount(fromAccount.accountId())} → ${formatAccount( toAccount.accountId(), )} (${amount} CLAW)`, ); return submitAndPollTransaction( buildTx(fromAccount, fromKey, [ sdk.Operation.payment({ destination: toAccount.accountId(), asset: ASSET, amount: amount, }), ]), `Payment of ${amount} CLAW`, ); } // Creates a claimable balance from `fromAccount` to `toAccount` of `amount` function createClaimable(fromAccount, fromKey, toAccount, amount) { console.log( `\nCreating claimable balance: ${formatAccount( fromAccount.accountId(), )} → ${formatAccount(toAccount.accountId())} (${amount} CLAW)`, ); return submitAndPollTransaction( buildTx(fromAccount, fromKey, [ sdk.Operation.createClaimableBalance({ asset: ASSET, amount: amount, claimants: [new sdk.Claimant(toAccount.accountId())], }), ]), `Create Claimable Balance of ${amount} CLAW`, ); } // Parse the ClaimableBalanceId from the transaction result XDR function getBalanceId(txResponse) { const txResult = txResponse.resultXdr; const operationResult = txResult.result().results()[0]; let creationResult = operationResult.value().createClaimableBalanceResult(); return creationResult.balanceId().toXDR("hex"); } // Clawback the claimable balance using its ID function clawbackClaimable(issuerAccount, issuerKey, balanceId) { console.log( `\nClawback claimable balance: ${formatAccount( issuerAccount.accountId(), )} clawing back balance ${balanceId}`, ); return submitAndPollTransaction( buildTx(issuerAccount, issuerKey, [ sdk.Operation.clawbackClaimableBalance({ balanceId }), ]), `Clawback Claimable Balance`, ); } // Clawback `amount` of CLAW from `fromAccount` by `byAccount` function doClawback(byAccount, byKey, fromAccount, amount) { console.log( `\nClawback: ${formatAccount( byAccount.accountId(), )} clawing back ${amount} CLAW from ${formatAccount( fromAccount.accountId(), )}`, ); return submitAndPollTransaction( buildTx(byAccount, byKey, [ sdk.Operation.clawback({ from: fromAccount.accountId(), asset: ASSET, amount: amount, }), ]), `Clawback of ${amount} CLAW`, ); } // Disable clawback for a trustline by the issuer function disableClawback(issuerAccount, issuerKeys, forTrustor) { console.log( `\nDisabling clawback for ${formatAccount( forTrustor.accountId(), )} on asset ${ASSET.code}`, ); return submitAndPollTransaction( buildTx(issuerAccount, issuerKeys, [ sdk.Operation.setTrustLineFlags({ trustor: forTrustor.accountId(), asset: ASSET, flags: { clawbackEnabled: false, }, }), ]), "Disable Clawback on Trustline", ); } // Enables clawback on A, and establishes trustlines for the CLAW asset for accounts B and C. function preamble() { console.log("\n=== SETTING UP CLAWBACK AND TRUSTLINES ==="); return getAccounts().then(function (accounts) { let [accountA, accountB, accountC] = accounts; return enableClawback(accountA, A) .then(() => { console.log("Clawback enabled successfully"); // Get fresh accounts after enabling clawback return getAccounts(); }) .then((refreshedAccounts) => { let [newAccountA, newAccountB, newAccountC] = refreshedAccounts; return Promise.all([ establishTrustline(newAccountB, B), establishTrustline(newAccountC, C), ]); }) .then(() => { console.log("All trustlines established successfully"); }); }); } ``` ### Example 1: Payments This example will highlight how the asset issuer holds control over their asset regardless of how it gets distributed to the world. In this scenario: - Account A will pay Account B with 1000 tokens of its custom asset. - Account B will then pay Account C 500 tokens in turn. - Finally, Account A will claw back half of Account C's balance, burning 250 CLAW tokens. ```js function examplePaymentAndThenClawback() { console.log("\n=== PAYMENT AND CLAWBACK EXAMPLE ==="); return getAccounts() .then(function (accounts) { let [accountA, accountB, accountC] = accounts; // A issues 1000 CLAW to B return makePayment(accountB, accountA, A, "1000") .then(() => { console.log("\n--- After A → B payment ---"); return getAccounts(); }) .then((refreshedAccounts) => { [accountA, accountB, accountC] = refreshedAccounts; return showCLAWBalances([accountA, accountB, accountC]); }) .then(() => { // B sends 500 CLAW to C return makePayment(accountC, accountB, B, "500"); }) .then(() => { console.log("\n--- After B → C payment ---"); return getAccounts(); }) .then((refreshedAccounts2) => { [accountA, accountB, accountC] = refreshedAccounts2; return showCLAWBalances([accountA, accountB, accountC]); }) .then(() => { // A claws back 250 CLAW from C return doClawback(accountA, A, accountC, "250"); }); }) .then(() => getAccounts()); } // Run the example with proper promise chaining function runExample1() { fundAccounts() .then(() => getAccounts()) .then(showXLMBalances) .then(preamble) .then(examplePaymentAndThenClawback) .then((finalAccounts) => { console.log("\n--- FINAL BALANCES ---"); return showCLAWBalances(finalAccounts); }) .then(() => { console.log("\n=== CLAWBACK DEMO COMPLETED ==="); }) .catch((error) => { console.error("Error in example:", error.message); }); } ``` When you invoke `runExample1()`, you should see output similar to: ```text === ACCOUNT SETUP === Account A (Issuer): GDYIV7XB5M6OS4S2P3DAGIEKRXNKSYZ4LSS5VBWMFGNAO42WGBOQY2E5 Account B (Trustor): GA7JEMMG46H6CDXR757ZUZ6HCEXE64RKC4M4DYX5DME2XP2P3LXQFVIM Account C (Trustor): GCLJYLVE43A73KV62YVC2H7ZK4CDSGMK7IYV2NC4WHQOFRQDFTBG6ZB7 === FUNDING ACCOUNTS WITH XLM === All accounts funded with XLM via airdrop === XLM BALANCES === GDYIV7XB (Account A): 10000.0 XLM GCLJYLVE (Account C): 10000.0 XLM GA7JEMMG (Account B): 10000.0 XLM === SETTING UP CLAWBACK AND TRUSTLINES === Enabling clawback flags on account GDYIV7XB (Account A) Enable Clawback Flags submitted: aa48d5bbf1e3c5b3b3ee6b21f4ab4dfbf52632e0d70b1f6e2a09ee33943093d5 Enable Clawback Flags completed successfully Clawback enabled successfully GA7JEMMG (Account B) establishing trustline for CLAW GCLJYLVE (Account C) establishing trustline for CLAW Establish Trustline (GCLJYLVE (Account C)) submitted: effb33b48f126ddd54237b8942d48fdfb562d7dc07500e9db07a1b223a5ef50e Establish Trustline (GA7JEMMG (Account B)) submitted: e05fe02b4da8dff22285da2d927545288160ec8f68c1359cefbabdfd4f0020df Establish Trustline (GA7JEMMG (Account B)) completed successfully Establish Trustline (GCLJYLVE (Account C)) completed successfully All trustlines established successfully === PAYMENT AND CLAWBACK EXAMPLE === Payment: GDYIV7XB (Account A) → GA7JEMMG (Account B) (1000 CLAW) Payment of 1000 CLAW submitted: be5cfda0f1625762b3b3b704affa356ff04e5ab388b7b63ede1fa5ca9873c96a Payment of 1000 CLAW completed successfully --- After A → B payment --- === CLAW BALANCES === GDYIV7XB (Account A): 0 CLAW GA7JEMMG (Account B): 1000.0 CLAW GCLJYLVE (Account C): 0 CLAW Payment: GA7JEMMG (Account B) → GCLJYLVE (Account C) (500 CLAW) Payment of 500 CLAW submitted: 8a0d19c8e56487255ffe24f5453ffe78177acc6f39bad204bab2849415032555 Payment of 500 CLAW completed successfully --- After B → C payment --- === CLAW BALANCES === GDYIV7XB (Account A): 0 CLAW GA7JEMMG (Account B): 500.0 CLAW GCLJYLVE (Account C): 500.0 CLAW Clawback: GDYIV7XB (Account A) clawing back 250 CLAW from GCLJYLVE (Account C) Clawback of 250 CLAW submitted: e63eed593a20fb5571e8189ff549cd1360849749c06294f00596fae30da0f23d Clawback of 250 CLAW completed successfully --- FINAL BALANCES --- === CLAW BALANCES === GCLJYLVE (Account C): 250.0 CLAW GA7JEMMG (Account B): 500.0 CLAW GDYIV7XB (Account A): 0 CLAW === CLAWBACK DEMO COMPLETED === ```
Clawback Flow Chart ![example1](/assets/clawback/example1.png)
Notice that Account A (the issuer) holds none of the asset despite clawing back 250 from Account C. This should drive home the fact that clawed-back assets are burned, not transferred. It may be strange that A never holds any tokens of its custom asset, but that's exactly how issuing works: you create value where there used to be none. Sending an asset to its issuing account is equivalent to burning it, and auditing the total amount of an asset in existence is one of the benefits of properly distributing an asset via a distribution account, which we avoid doing here for example brevity. ### Example 2: Claimable Balances Direct payments aren't the only way to transfer assets between accounts: claimable balances also do this. Since they are a separate payment mechanism, they need a separate clawback mechanism. In this scenario: - Account A will pay Account B with 1000 tokens of its custom asset. - Account B creates a Claimable Balance for Account C for 300 tokens. (You can query the Claimable Balance via RPC's `getLedgerEntries` endpoint.) - Account A then claws back the Claimable Balance from Account C. - The Claimable Balance entry is deleted and no longer queryable. ```js function exampleClaimableBalanceClawback() { console.log("\n=== CLAIMABLE BALANCE CLAWBACK EXAMPLE ==="); let balanceId; return getAccounts() .then(function (accounts) { let [accountA, accountB, accountC] = accounts; console.log("\n--- Initial CLAW balances ---"); return showCLAWBalances([accountA, accountB, accountC]) .then(() => { // A pays 1000 CLAW to B return makePayment(accountB, accountA, A, "1000"); }) .then(() => { console.log("\n--- After A → B payment ---"); return getAccounts(); }) .then((refreshedAccounts) => { [accountA, accountB, accountC] = refreshedAccounts; return showCLAWBalances([accountA, accountB, accountC]); }) .then(() => { // B creates claimable balance for C return createClaimable(accountB, B, accountC, "300"); }) .then((txResp) => { balanceId = getBalanceId(txResp); console.log(`Claimable balance created with ID: ${balanceId}`); console.log("\n--- After claimable balance creation ---"); return getAccounts() .then((refreshedAccounts2) => { [accountA, accountB, accountC] = refreshedAccounts2; return showCLAWBalances([accountA, accountB, accountC]); }) .then(() => { // Check that the claimable balance exists return fetchClaimableBalance( balanceId, "claimable balance after creation", ); }) .then(() => { // A claws back the claimable balance return clawbackClaimable(accountA, A, balanceId); }) .then(() => { // Check that the claimable balance no longer exists return fetchClaimableBalance( balanceId, "claimable balance after clawback", ); }); }); }) .then(() => getAccounts()); } // Run the example with proper promise chaining function runExample2() { fundAccounts() .then(() => getAccounts()) .then(showXLMBalances) .then(preamble) .then(exampleClaimableBalanceClawback) .then((finalAccounts) => { console.log("\n--- FINAL BALANCES ---"); return showCLAWBalances(finalAccounts); }) .then(() => { console.log("\n=== CLAIMABLE BALANCE CLAWBACK DEMO COMPLETED ==="); }) .catch((error) => { console.error("Error in example:", error.message); }); } ``` When you invoke `runExample2()`, you should see output similar to: ``` === ACCOUNT SETUP === Account A (Issuer): GBOK4XIKNCKVWKRG27EEMYX2H7H5GP6ZCLYJTORHJVV3ZDJZJXTUPKTJ Account B (Trustor): GBI5XUOWLBL44DWJXURGQJX46TUSNEPQ553LZUGLFCVQ6KRRPEEFBPKE Account C (Trustor): GANXLMMUIG7G5NT6GQZNE3OPCRCROYJLTR6PGRZHVURUGNURSF3H3ZEZ === FUNDING ACCOUNTS WITH XLM === All accounts funded with XLM via airdrop === XLM BALANCES === GBI5XUOW (Account B): 10000.0 XLM GANXLMMU (Account C): 10000.0 XLM GBOK4XIK (Account A): 10000.0 XLM === SETTING UP CLAWBACK AND TRUSTLINES === Enabling clawback flags on account GBOK4XIK (Account A) Enable Clawback Flags submitted: 4812d2be7e8652dbeb29fd4d9387c71725e09e5f1d3500b3e913eebc9bec03b1 Enable Clawback Flags completed successfully Clawback enabled successfully GBI5XUOW (Account B) establishing trustline for CLAW GANXLMMU (Account C) establishing trustline for CLAW Establish Trustline (GANXLMMU (Account C)) submitted: eb2c17135109408d751cdbab9235c8a833922a66c6576fe76a7930a2ef9e7042 Establish Trustline (GBI5XUOW (Account B)) submitted: 1ebb5ed64cb1c5c9beb121d56c31b802fab885ef06b771b3d8617371482e115d Establish Trustline (GANXLMMU (Account C)) completed successfully Establish Trustline (GBI5XUOW (Account B)) completed successfully All trustlines established successfully === CLAIMABLE BALANCE CLAWBACK EXAMPLE === --- Initial CLAW balances --- === CLAW BALANCES === GANXLMMU (Account C): 0 CLAW GBI5XUOW (Account B): 0 CLAW GBOK4XIK (Account A): 0 CLAW Payment: GBOK4XIK (Account A) → GBI5XUOW (Account B) (1000 CLAW) Payment of 1000 CLAW submitted: ec852daea2fea4e5b6a1d56530618f30642d9207767f30558d577b4f98e59850 Payment of 1000 CLAW completed successfully --- After A → B payment --- === CLAW BALANCES === GANXLMMU (Account C): 0.0 CLAW GBI5XUOW (Account B): 1000.0 CLAW GBOK4XIK (Account A): 0 CLAW Creating claimable balance: GBI5XUOW (Account B) → GANXLMMU (Account C) (300 CLAW) Create Claimable Balance of 300 CLAW submitted: 743066775839ef5112fd2b8a26730700a986655c557abcb98b91c6fdbd12abde Create Claimable Balance of 300 CLAW completed successfully Claimable balance created with ID: 0000000091b5fe84a029c79d409ac88d34b7047a6cc9f95b2c2f965843db122ef70fac2c --- After claimable balance creation --- === CLAW BALANCES === GBI5XUOW (Account B): 700.0 CLAW GBOK4XIK (Account A): 0 CLAW GANXLMMU (Account C): 0 CLAW --- Checking claimable balance after creation --- Looking up balance ID: 0000000091b5fe84a029c79d409ac88d34b7047a6cc9f95b2c2f965843db122ef70fac2c ✅ Found claimable balance Amount: 300.0 CLAW Number of claimants: 1 Claimant 1: GANXLMMU (Account C) Clawback claimable balance: GBOK4XIK (Account A) clawing back balance 0000000091b5fe84a029c79d409ac88d34b7047a6cc9f95b2c2f965843db122ef70fac2c Clawback Claimable Balance submitted: a82a0ed067d34361da622a56a3b6d59a7c4a351414a69da4dc9df8a5728e7758 Clawback Claimable Balance completed successfully --- Checking claimable balance after clawback --- Looking up balance ID: 0000000091b5fe84a029c79d409ac88d34b7047a6cc9f95b2c2f965843db122ef70fac2c ❌ Claimable balance not found (Claimable balance 0000000091b5fe84a029c79d409ac88d34b7047a6cc9f95b2c2f965843db122ef70fac2c not found) --- FINAL BALANCES --- === CLAW BALANCES === GBI5XUOW (Account B): 700.0 CLAW GBOK4XIK (Account A): 0 CLAW GANXLMMU (Account C): 0.0 CLAW === CLAIMABLE BALANCE CLAWBACK DEMO COMPLETED === ``` ### Example 3: Selectively Enabling Clawback When you enable the `AUTH_CLAWBACK_ENABLED_FLAG` on your account, it will make all future trustlines have clawback enabled for any of your issued assets. This may not always be desirable as you may want certain assets to behave as they did before. Though you could work around this by reissuing assets from a “dedicated clawback” account, you can also simply disable clawbacks for certain trustlines by clearing the `TRUST_LINE_CLAWBACK_ENABLED_FLAG` on a trustline. In this scenario: - Account A issues an asset and sends 1000 tokens to a distribution account (Account B). - Account A claws back 500 tokens from Account B. - Account A then clears the trustline so that it (the issuer) can no longer clawback the asset. - Account A then attempts to clawback 250 tokens from Account B and fails. _Please note that Account C is not relevant in this example._ ```js function exampleSelectiveClawbackThenDisableClawback() { console.log("\n=== SELECTIVE CLAWBACK EXAMPLE ==="); return getAccounts() .then((accounts) => { let [accountA, accountB] = accounts; console.log("\n--- Initial CLAW balances ---"); return showCLAWBalances([accountA, accountB]) .then(() => { // A pays 1000 CLAW to B return makePayment(accountB, accountA, A, "1000"); }) .then(() => { console.log("\n--- After A → B payment ---"); return getAccounts(); }) .then((refreshedAccounts) => { [accountA, accountB] = refreshedAccounts; return showCLAWBalances([accountA, accountB]); }) .then(() => { // A claws back 500 CLAW from B (should work) return doClawback(accountA, A, accountB, "500"); }) .then(() => { console.log("\n--- After first clawback ---"); return getAccounts(); }) .then((refreshedAccounts2) => { [accountA, accountB] = refreshedAccounts2; return showCLAWBalances([accountA, accountB]); }) .then(() => { // A disables clawback for B's trustline return disableClawback(accountA, A, accountB); }) .then(() => { // Try to clawback again (should fail) return doClawback(accountA, A, accountB, "250"); }) .catch((err) => { console.log("Error:", err.message); }); }) .then(() => getAccounts()); } // Run the example with proper promise chaining function runExample3() { fundAccounts() .then(() => getAccounts()) .then((accounts) => showXLMBalances([accounts[0], accounts[1]])) // Only show A and B .then(preamble) // This sets up clawback and trustlines for A, B, C .then(exampleSelectiveClawbackThenDisableClawback) .then((finalAccounts) => { console.log("\n--- FINAL BALANCES ---"); return showCLAWBalances([finalAccounts[0], finalAccounts[1]]); // Only show A and B }) .then(() => { console.log("\n=== SELECTIVE CLAWBACK DEMO COMPLETED ==="); }) .catch((error) => { console.error("Error in example:", error.message); }); } ``` When you invoke `runExample3()`, you should see output similar to: ```text === ACCOUNT SETUP === Account A (Issuer): GCTYN2SAMM2SHM5LOCHS2P2I24MEHVYKHKCSO5XPR2H2GQCK6PFCQRK6 Account B (Trustor): GAWDYTIKQI7J3YSCSLWGAJPN6J62WQKDA3XCAC55DDRH5KCTEZ5IGGWP Account C (Trustor): GC5KQ7H5G5E65OVZGKBVCXBJKTEHWBBLQ56L7DK4ATUHUH5VJP4YHQVC === FUNDING ACCOUNTS WITH XLM === All accounts funded with XLM via airdrop === XLM BALANCES === GCTYN2SA (Account A): 10000.0 XLM GAWDYTIK (Account B): 10000.0 XLM === SETTING UP CLAWBACK AND TRUSTLINES === Enabling clawback flags on account GCTYN2SA (Account A) Enable Clawback Flags submitted: 52e7e15d70e46dc3b551e787ec3be11692d2cc8e0b2fda070cebfa048c67cedd Enable Clawback Flags completed successfully Clawback enabled successfully GAWDYTIK (Account B) establishing trustline for CLAW GC5KQ7H5 (Account C) establishing trustline for CLAW Establish Trustline (GAWDYTIK (Account B)) submitted: 3cf49bf597085ede004d566d151b9a917b31d1dbbfa81d71a64b615356702da7 Establish Trustline (GC5KQ7H5 (Account C)) submitted: 9250e62be27e3111924d36c0169f3b08e12565f8f842bf32a8a7a0efb098a080 Establish Trustline (GAWDYTIK (Account B)) completed successfully Establish Trustline (GC5KQ7H5 (Account C)) completed successfully All trustlines established successfully === SELECTIVE CLAWBACK EXAMPLE === --- Initial CLAW balances --- === CLAW BALANCES === GCTYN2SA (Account A): 0 CLAW GAWDYTIK (Account B): 0 CLAW Payment: GCTYN2SA (Account A) → GAWDYTIK (Account B) (1000 CLAW) Payment of 1000 CLAW submitted: 9974f7a8f85dbe437a18364066959718ceeeaa3ae9a84ccd0e91da5f4e6bfaeb Payment of 1000 CLAW completed successfully --- After A → B payment --- === CLAW BALANCES === GAWDYTIK (Account B): 1000.0 CLAW GCTYN2SA (Account A): 0 CLAW Clawback: GCTYN2SA (Account A) clawing back 500 CLAW from GAWDYTIK (Account B) Clawback of 500 CLAW submitted: b1729001fba89198f67bcff2de7fb47ea97c358e902688d88b76c6ff3947cec2 Clawback of 500 CLAW completed successfully --- After first clawback --- === CLAW BALANCES === GAWDYTIK (Account B): 500.0 CLAW GCTYN2SA (Account A): 0 CLAW Disabling clawback for GAWDYTIK (Account B) on asset CLAW Disable Clawback on Trustline submitted: a1fe18c12628806bac90936f563978d1f9a3333a1a77482d55d3b7af99e679a7 Disable Clawback on Trustline completed successfully Clawback: GCTYN2SA (Account A) clawing back 250 CLAW from GAWDYTIK (Account B) Clawback of 250 CLAW submitted: eab31d237832b513ca911cd2b4a4466a7b7b502f274dcc926b9910223fc2043f Clawback of 250 CLAW failed: FAILED --- FINAL BALANCES --- === CLAW BALANCES === GAWDYTIK (Account B): 500.0 CLAW GCTYN2SA (Account A): 0 CLAW === SELECTIVE CLAWBACK DEMO COMPLETED === ``` --- ## Create an account # Create an Account _Before we get started with working with Stellar in code, consider going through the following examples using the [Stellar Lab](https://lab.stellar.org). The lab allows you to create accounts, fund accounts on the Stellar test network, build transactions, run any operation, and inspect responses from Horizon via the Endpoint Explorer._ [Accounts](../../../learn/fundamentals/stellar-data-structures/accounts.mdx) are a fundamental building block of Stellar: they hold all your balances, allow you to send and receive payments, and let you place offers to buy and sell assets. Since pretty much everything on Stellar is in some way tied to an account, the first thing you generally need to do when you start developing is create one. This beginner-level tutorial walks through the three building blocks you'll need: [generating keys](#create-a-keypair), [funding an account](#create-an-account), and [fetching balances](#fetch-balances). ## Create a Keypair Stellar uses public key cryptography to secure every transaction: each Stellar account has a keypair consisting of a **public key** and a **secret key**. The public key is always safe to share — other people need it to identify your account and verify that you authorized a transaction. It's like an email address. The secret key, however, is private information that proves you own — and gives you access to — your account. It's like a password, and you should never share it with anyone. Before creating an account, you need to generate your own keypair; we'll learn how in the [full example](#full-example), below. ## Create an Account A valid keypair alone does not make an account. To prevent unused entries from bloating the ledger, Stellar requires every account to hold a [minimum balance](../../../learn/fundamentals/lumens.mdx#minimum-balance) of two base reserves before it actually exists (at the current base reserve of 0.5 XLM that works out to 1 XLM, but validators can change the base reserve — see the linked section for the current value). This is the ordinary, self-funded path shown throughout this tutorial. Alternatively, an account can be created with a `startingBalance` of `0` when another account sponsors its reserves ([CAP-33](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0033.md) sponsored reserves): the sponsor carries the two base reserves on the new account's behalf, so the account exists without holding the minimum balance itself. See the [Sponsored Reserves guide](sponsored-reserves.mdx) for how to set this up. On the test network you can ask Friendbot — a friendly funding service — to create and fund the account for you. In the SDK examples, below, you'll see that we "discover" the Friendbot endpoint via the RPC's `getNetwork` call, then issue a funding request. On the public network, things are a little different. You'd acquire lumens from an exchange or have someone create or sponsor an account on your behalf. This is the purpose of the [`CreateAccount` operation](../../../learn/fundamentals/transactions/list-of-operations.mdx#create-account). We'll see in the examples, later, that given a funded account, you can then create a child account on the network with a starting balance. ## Fetch Balances Once your accounts exist you can query their state. On Stellar, assets can be held in a handful of different forms: - your **native** balance is how much XLM you're holding, which is associated directly to your account - you can also establish **trustlines** to various assets to hold your balance; these can be acquired both by simple payment operations and smart contract interactions - finally, you can hold **custom smart contract tokens** which are non-standard assets You'll learn about these in later tutorials. ## Full Example Let's combine these four concepts into a single cohesive example. Keep in mind that you will always see your USDC balance as zero, since we did not actually receive any of it, but it will be useful in the future when you hold more than just the native token. For each of these, be sure to follow the installation and setup instructions corresponding to each SDK from their [documentation](../../../tools/sdks/client-sdks.mdx). ```js Keypair, BASE_FEE, Networks, Operation, Asset, humanizeEvents, TransactionBuilder, } from "@stellar/stellar-sdk"; // See https://developers.stellar.org/docs/data/apis/rpc/providers const server = new Server("https://soroban-testnet.stellar.org"); const testnetUsdc = new Asset( "USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", ); async function main() { // // Generating keypairs // const parent = Keypair.random(); console.log("Secret:", parent.secret()); console.log("Public:", parent.publicKey()); // // Account creation via friendbot (does not work on mainnet) // const friendbotResponse = await server.requestAirdrop(parent.publicKey()); console.log("SUCCESS! You have a new account:\n", friendbotResponse); const parentAccount = await server.getAccount(parent.publicKey()); const childAccount = Keypair.random(); // // Account creation via the CreateAccount operation // const createAccountTx = new TransactionBuilder(parentAccount, { fee: BASE_FEE, networkPassphrase: Networks.TESTNET, }) .addOperation( Operation.createAccount({ destination: childAccount.publicKey(), startingBalance: "5", }), ) .addOperation(Operation.changeTrust({ asset: testnetUsdc })) .setTimeout(180) .build(); createAccountTx.sign(parent); const sendTxResponse = await server.sendTransaction(createAccountTx); if (sendTxResponse.status !== "PENDING") { console.log(`There was an error: ${JSON.stringify(sendTxResponse)}`); throw sendTxResponse; } const txResponse = await server.pollTransaction(sendTxResponse.hash); if (txResponse.status !== "SUCCESS") { console.log( `Transaction status: ${txResponse.status}, events: ${humanizeEvents( txResponse.diagnosticEvents, )}`, ); } console.log("Created the new account", childAccount.publicKey()); // // Fetching native and USDC balances // const accountEntry = await server.getAccountEntry(parent.publicKey()); console.log("Balance for account: " + parent.publicKey()); console.log("XLM:", accountEntry.balance().toString()); const trustlineEntry = await server.getTrustline( parent.publicKey(), testnetUsdc, ); console.log("USDC:", trustlineEntry.balance().toString()); } main().catch((err) => console.error(err)); ``` ```python from stellar_sdk import * from stellar_sdk.soroban_rpc import GetTransactionStatus, SendTransactionStatus from stellar_sdk.xdr import * RPC_URL = "https://soroban-testnet.stellar.org" TESTNET_USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" SERVER = SorobanServer(RPC_URL) def main(): # # Keypair generation # parent = Keypair.random() print(f"Secret: {parent.secret}") print(f"Public: {parent.public_key}") # # Account creation via friendbot # friendbot_url = get_friendbot_url() print(f"Using {friendbot_url} for friendbot funding") body = request_funds(friendbot_url, parent.public_key) print(f"SUCCESS! You have a new account:\n{body}") # # Account creation via CreateAccount operation # child = Keypair.random() parent_account = SERVER.load_account(parent.public_key) usdc_asset = Asset("USDC", TESTNET_USDC_ISSUER) tx = ( TransactionBuilder( source_account=parent_account, network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, base_fee=TransactionBuilder.BASE_FEE, ) .add_time_bounds(0, 0) .append_operation( CreateAccount(destination=child.public_key, starting_balance="5") ) .build() ) tx.sign(parent) ledger_seq = submit_and_confirm(tx) print(f"Transaction confirmed in ledger {ledger_seq}") print(f"Created the new account {child.public_key}") # # Fetching native and USDC balances # native_balance = load_native_balance(parent.public_key) print(f"Balance for account {parent.public_key}") print(f"XLM: {native_balance}") trustline_balance = get_trustline(parent.public_key, usdc_asset) print(f"USDC trustline balance (raw): {trustline_balance}") def request_funds(friendbot_url: str, account_id: str) -> str: request_url = f"{friendbot_url}?addr={account_id}" resp = requests.get(request_url, timeout=30) check(resp.ok, f"friendbot returned {resp.status_code}: {resp.text}") return resp.text def get_friendbot_url() -> str: response = SERVER.get_network() if response.friendbot_url: return response.friendbot_url.strip() raise RuntimeError("Friendbot URL not provided by network") def load_native_balance(account_id: str) -> int: key = LedgerKey( LedgerEntryType.ACCOUNT, account=LedgerKeyAccount( account_id=Keypair.from_public_key(account_id).xdr_account_id() ), ) response = SERVER.get_ledger_entries([key]) check(response.entries, f"account {account_id} not found") ledger_entry = LedgerEntryData.from_xdr(response.entries[0].xdr) account = ledger_entry.account check(account is not None, "ledger entry missing account data") return account.balance.int64 def get_trustline(account_id: str, asset: Asset) -> int: key = LedgerKey( LedgerEntryType.TRUSTLINE, trust_line=LedgerKeyTrustLine( account_id=Keypair.from_public_key(account_id).xdr_account_id(), asset=asset.to_trust_line_xdr_object(), ), ) response = SERVER.get_ledger_entries([key]) if not response.entries: return 0 ledger_entry = LedgerEntryData.from_xdr(response.entries[0].xdr) trust_line = ledger_entry.trust_line if trust_line is None: return 0 return trust_line.balance.int64 def submit_and_confirm(transaction): response = SERVER.send_transaction(transaction) check( response.status == SendTransactionStatus.PENDING, f"Transaction submission failed: {response.status}", ) tx_resp = SERVER.poll_transaction(response.hash) if tx_resp.status == GetTransactionStatus.SUCCESS: return tx_resp.ledger or tx_resp.latest_ledger raise RuntimeError(f"Transaction failed: {tx_resp.diagnostic_events_xdr}") def check(condition: bool, message: str) -> None: if not condition: raise RuntimeError(message) if __name__ == "__main__": main() ``` ```go package main "context" "fmt" "io" "log" "net/http" "net/url" "strings" "time" "github.com/stellar/go-stellar-sdk/amount" "github.com/stellar/go-stellar-sdk/keypair" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/txnbuild" "github.com/stellar/go-stellar-sdk/xdr" client "github.com/stellar/go-stellar-sdk/clients/rpcclient" protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" ) const ( rpcURL = "https://soroban-testnet.stellar.org" testnetUSDCIssuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" pollAttempts = 10 pollInitialDelay = time.Second ) func main() { // // Generating keypairs // parent := keypair.MustRandom() log.Printf("Secret: %s", parent.Seed()) log.Printf("Public: %s", parent.Address()) // // Funding via friendbot // ctx := context.Background() cli := client.NewClient(rpcURL, nil) defer cli.Close() networkInfo, err := cli.GetNetwork(ctx) check(err) friendbotURL := strings.TrimSpace(networkInfo.FriendbotURL) if friendbotURL == "" { log.Fatal("friendbot URL not provided by network") } log.Printf("Using %s for friendbot funding", friendbotURL) body := requestAirdrop(friendbotURL, parent.Address()) log.Printf("SUCCESS! You have a new account:\n%s", body) // // Funding a new account via CreateAccount operation // child := keypair.MustRandom() parentAccount, err := cli.LoadAccount(ctx, parent.Address()) check(err) tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ SourceAccount: parentAccount, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Preconditions: txnbuild.Preconditions{ TimeBounds: txnbuild.NewInfiniteTimeout(), }, Operations: []txnbuild.Operation{ &txnbuild.CreateAccount{ Destination: child.Address(), Amount: "5", }, }, }) check(err) tx, err = tx.Sign(network.TestNetworkPassphrase, parent) check(err) resp := submitAndAwait(ctx, cli, tx) log.Printf("Transaction confirmed in ledger %d", resp.Ledger) log.Printf("Created the new account %s", child.Address()) // // Fetch native and USDC balances for account // accountEntry := getAccountEntry(ctx, cli, parent.Address()) log.Printf("Balance for account %s", parent.Address()) log.Printf("XLM: %s", amount.String(accountEntry.Balance)) usdcAsset := txnbuild.CreditAsset{Code: "USDC", Issuer: testnetUSDCIssuer} trustlineEntry := getTrustlineEntry(ctx, cli, parent.Address(), usdcAsset) log.Printf("USDC trustline balance (raw): %d", trustlineEntry.Balance) } func requestAirdrop(friendbotURL, address string) string { requestURL := fmt.Sprintf("%s?addr=%s", friendbotURL, url.QueryEscape(address)) resp, err := http.Get(requestURL) check(err) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) check(err) if resp.StatusCode >= 400 { log.Fatalf("friendbot returned %s: %s", resp.Status, string(body)) } return string(body) } func submitAndAwait(ctx context.Context, cli *client.Client, tx *txnbuild.Transaction) protocol.GetTransactionResponse { txnB64, err := tx.Base64() check(err) sendResp, err := cli.SendTransaction(ctx, protocol.SendTransactionRequest{Transaction: txnB64}) check(err) if sendResp.Status != "PENDING" { log.Fatalf("transaction submission failed with status %s", sendResp.Status) } return pollTransaction(ctx, cli, sendResp.Hash) } func pollTransaction(ctx context.Context, cli *client.Client, hash string) protocol.GetTransactionResponse { delay := pollInitialDelay for range pollAttempts { resp, err := cli.GetTransaction(ctx, protocol.GetTransactionRequest{Hash: hash}) check(err) switch resp.Status { case protocol.TransactionStatusSuccess: return resp case protocol.TransactionStatusFailed: log.Fatalf("transaction failed: %s", strings.Join(resp.DiagnosticEventsXDR, "\n")) case protocol.TransactionStatusNotFound: // keep polling default: // unexpected status, continue polling in case it's transient } select { case <-ctx.Done(): log.Fatalf("context cancelled while polling: %v", ctx.Err()) case <-time.After(delay): } delay += pollInitialDelay } log.Fatalf("transaction %s not found after polling", hash) return protocol.GetTransactionResponse{} } func getAccountEntry(ctx context.Context, cli *client.Client, address string) *xdr.AccountEntry { ledgerKey := xdr.LedgerKey{ Type: xdr.LedgerEntryTypeAccount, Account: &xdr.LedgerKeyAccount{ AccountId: xdr.MustAddress(address), }, } keyB64, err := xdr.MarshalBase64(ledgerKey) check(err) resp, err := cli.GetLedgerEntries(ctx, protocol.GetLedgerEntriesRequest{Keys: []string{keyB64}}) check(err) if len(resp.Entries) == 0 { log.Fatalf("account %s not found", address) } var ledgerData xdr.LedgerEntryData check(xdr.SafeUnmarshalBase64(resp.Entries[0].DataXDR, &ledgerData)) if ledgerData.Account == nil { log.Fatalf("ledger entry for %s missing account data", address) } return ledgerData.Account } func getTrustlineEntry(ctx context.Context, cli *client.Client, account string, asset txnbuild.CreditAsset) xdr.TrustLineEntry { trustlineAsset, err := asset.MustToTrustLineAsset().ToXDR() check(err) ledgerKey := xdr.LedgerKey{ Type: xdr.LedgerEntryTypeTrustline, TrustLine: &xdr.LedgerKeyTrustLine{ AccountId: xdr.MustAddress(account), Asset: trustlineAsset, }, } keyB64, err := xdr.MarshalBase64(ledgerKey) check(err) resp, err := cli.GetLedgerEntries(ctx, protocol.GetLedgerEntriesRequest{Keys: []string{keyB64}}) check(err) if len(resp.Entries) == 0 { check(fmt.Errorf("trustline for %s:%s not found", asset.Code, asset.Issuer)) } var ledgerData xdr.LedgerEntryData check(xdr.SafeUnmarshalBase64(resp.Entries[0].DataXDR, &ledgerData)) return ledgerData.MustTrustLine() } func check(err error) { if err != nil { log.Fatal(err) } } ``` ```java // File: main.java // Dependencies (Maven coordinates): // network.lightsail:stellar-sdk:3.1.0 // com.fasterxml.jackson.core:jackson-databind:2.17.2 public final class main { private static final String RPC_URL = "https://soroban-testnet.stellar.org"; private static final String TESTNET_USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; private static final HttpClient HTTP = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(20)) .build(); private static final SorobanServer SERVER = new SorobanServer(RPC_URL); public static void main(String[] args) throws Exception { // // Generate keypairs // KeyPair parent = KeyPair.random(); log("Secret: %s", new String(parent.getSecretSeed())); log("Public: %s", parent.getAccountId()); // // Fund a new account via friendbot // GetNetworkResponse networkInfo = SERVER.getNetwork(); String friendbotUrl = networkInfo != null ? networkInfo.getFriendbotUrl() : null; check(friendbotUrl != null && !friendbotUrl.isBlank(), "Friendbot URL not provided by network"); log("Using %s for friendbot funding", friendbotUrl); String friendbotResponse = requestFunds(friendbotUrl, parent.getAccountId()); log("SUCCESS! You have a new account:\n%s", friendbotResponse); // // Fund a new account via CreateAccount operation // KeyPair child = KeyPair.random(); Account parentAccount = SERVER.loadAccount(parent.getAccountId()); Transaction transaction = new TransactionBuilder(parentAccount, Network.TESTNET) .setBaseFee(Transaction.MIN_BASE_FEE) .setTimeout(TransactionBuilder.TIMEOUT_INFINITE) .addOperation(CreateAccountOperation.builder().destination(child.getAccountId()).startingBalance(new BigDecimal("5")).build()) .build(); transaction.sign(parent); long ledgerSeq = submitAndConfirm(transaction); log("Transaction confirmed in ledger %d", ledgerSeq); log("Created the new account %s", child.getAccountId()); // // Fetch native and USDC balances for account // long nativeBalance = loadNativeBalance(parent.getAccountId()); log("Balance for account %s", parent.getAccountId()); log("XLM: %d", nativeBalance); Asset usdcAsset = Asset.createNonNativeAsset("USDC", TESTNET_USDC_ISSUER); long trustlineBalance = getTrustlineBalance(parent.getAccountId(), usdcAsset); log("USDC trustline balance (raw): %d", trustlineBalance); } private static String requestFunds(String friendbotUrl, String accountId) throws IOException, InterruptedException { String requestUrl = friendbotUrl + "?addr=" + accountId; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(requestUrl)) .timeout(Duration.ofSeconds(30)) .GET() .build(); HttpResponse response = HTTP.send(request, HttpResponse.BodyHandlers.ofString()); check(response.statusCode() / 100 == 2, "Friendbot returned " + response.statusCode() + ": " + response.body()); return response.body(); } private static long loadNativeBalance(String accountId) throws IOException { LedgerKey key = new LedgerKey(); key.setDiscriminant(LedgerEntryType.ACCOUNT); LedgerKeyAccount accountKey = new LedgerKeyAccount(); accountKey.setAccountID(KeyPair.fromAccountId(accountId).getXdrAccountId()); key.setAccount(accountKey); GetLedgerEntriesResponse response = SERVER.getLedgerEntries(Collections.singletonList(key)); check(!response.getEntries().isEmpty(), "Account " + accountId + " not found"); LedgerEntryData data = LedgerEntryData.fromXdr(response.getEntries().get(0).getXdr()); check(data.getAccount() != null, "Ledger entry missing account data"); return data.getAccount().getBalance().getInt64(); } private static long getTrustlineBalance(String accountId, org.stellar.sdk.Asset asset) throws IOException { LedgerKey key = new LedgerKey(); key.setDiscriminant(LedgerEntryType.TRUSTLINE); LedgerKeyTrustLine trustLineKey = new LedgerKeyTrustLine(); trustLineKey.setAccountID(KeyPair.fromAccountId(accountId).getXdrAccountId()); trustLineKey.setAsset(asset.toXdr()); key.setTrustLine(trustLineKey); GetLedgerEntriesResponse response = SERVER.getLedgerEntries(Collections.singletonList(key)); if (response.getEntries().isEmpty()) { return 0L; } LedgerEntryData data = LedgerEntryData.fromXdr(response.getEntries().get(0).getXdr()); TrustLineEntry trustLine = data.getTrustLine(); if (trustLine == null) { return 0L; } return trustLine.getBalance().getInt64(); } private static long submitAndConfirm(Transaction transaction) throws IOException, InterruptedException { SendTransactionResponse sendResp = SERVER.sendTransaction(transaction); check(sendResp.getStatus() == SendTransactionStatus.PENDING, "Transaction submission failed: " + sendResp.getStatus()); GetTransactionResponse txResp = SERVER.pollTransaction(sendResp.getHash()); check(txResp.getStatus() == GetTransactionResponse.Status.SUCCESS, "Transaction failed: " + txResp.getStatus()); Long ledger = txResp.getLedger() != null ? txResp.getLedger() : txResp.getLatestLedger(); check(ledger != null, "Transaction completed but ledger sequence unavailable"); return ledger; } private static void check(boolean condition, String message) { if (!condition) { throw new IllegalStateException(message); } } private static void log(String format, Object... args) { System.out.println(String.format(format, args)); } } ``` ```rust use std::{cell::RefCell, rc::Rc, time::Duration}; use soroban_client::{ account::{Account, AccountBehavior}, asset::{Asset, AssetBehavior}, keypair::{Keypair, KeypairBehavior}, network::{NetworkPassphrase, Networks}, operation::{self, Operation}, transaction::{TransactionBehavior, TransactionBuilder, TransactionBuilderBehavior}, xdr, Options, Server, }; const TESTNET_USDC_ISSUER: &str = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; #[tokio::main] pub async fn main() -> Result<(), Box> { let server_url = "https://soroban-testnet.stellar.org"; let server = Server::new(server_url, Options::default()).expect("Cannot create server"); // // Generate keypairs // let parent = Keypair::random()?; println!("Secret: {}", parent.secret_key()?); println!("Public: {}", parent.public_key()); // // Fund a new account via friendbot // let account_data = server.request_airdrop(&parent.public_key()).await?; println!("SUCCESS! You have a new account:\n{account_data:?}"); let child = Keypair::random()?; let mut parent_account = Account::new(&parent.public_key(), &account_data.sequence_number()).unwrap(); // // Fund a new account via CreateAccount operation // let mut builder = TransactionBuilder::new(&mut parent_account, Networks::testnet(), None); builder.fee(1000u32); builder.add_operation( Operation::new() .create_account(&child.public_key(), operation::ONE * 5) .unwrap(), ); let mut tx = builder.build(); tx.sign(&[parent]); let response = server.send_transaction(tx).await?; let hash = response.hash; println!("Tx hash: {}", hash); // // Polling for transaction completion // server .wait_transaction(&hash, Duration::from_secs(15)) .map_err(|_| "Failed to create account") .await?; // // Fetch native and USDC balances for account // let account_id = child.xdr_account_id(); let ledger_key = xdr::LedgerKey::Account(xdr::LedgerKeyAccount { account_id }); let response = server.get_ledger_entries(vec![ledger_key]).await?; if let xdr::LedgerEntryData::Account(account) = response.entries.unwrap()[0].to_data() { // Convert the balance from stroops let balance = account.balance / operation::ONE; println!("XLM: {balance}"); } let account_id = child.xdr_account_id(); let asset = Asset::new("USDC", Some(TESTNET_USDC_ISSUER))?.into(); let ledger_key = xdr::LedgerKey::Trustline(xdr::LedgerKeyTrustLine { account_id, asset }); let response = server.get_ledger_entries(vec![ledger_key]).await?; let entries = response.entries.unwrap_or_default(); let usdc_balance = match entries.first().map(|e| e.to_data()) { Some(xdr::LedgerEntryData::Trustline(t)) => t.balance, _ => 0, }; println!("USDC trustline balance (raw): {}", usdc_balance); Ok(()) } ``` Now that you’ve got an account and check its asset balances, you can [start sending and receiving payments](send-and-receive-payments.mdx), or, if you're ready to hunker down, you can skip ahead and [build a wallet](../../apps/wallet/overview.mdx) or [issue a Stellar asset](../../../tokens/anatomy-of-an-asset.mdx). --- ## Fee-bump transactions Fee-bump transactions were introduced in [CAP-15](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0015.md) and enable an account to pay the transaction fees for an existing transaction without having to re-sign the transaction or manage sequence numbers. A fee-bump transaction is made of two parts: 1. An inner transaction envelope with its signature(s) 2. An outer transaction envelope with the fee-bump transaction and fee account signature ![Fee-bump transaction structure](/assets/guides/fee-bump-transaction.png) ## Common use cases You may want to consider using fee bumps when: - You’re building a service where you want to cover user fees - You want to increase the fee on an existing transaction so it has a better chance of making it to the ledger during surge pricing - You need to adjust the fee on a pre-authorized transaction so it can make it to the ledger if minimum network fees have increased :::note Use the [Stellar Wallet Sponsorship Calculator](../../../tools/developer-tools/wallets.mdx#stellar-wallet-sponsorship-calculator) to estimate the XLM requirements for wallets looking to use sponsored reserves and fee-bump transactions to cover account creation, transaction fees, trustlines, and more. ::: ## Attributes ### Existing transaction envelope (inner transaction) Before creating a fee-bump transaction, you must first have a transaction wrapped with its signatures in a transaction envelope. We’ll call this transaction the inner transaction. ### Fee account The account that will pay the fee for the fee-bump transaction. This account will incur the fee instead of the source account specified in the inner transaction. The sequence number is still taken from the source account, however. ### Fee The maximum per-operation fee you’re willing to pay for the fee-bump transaction. The fee-bump transaction is one operation. Therefore, the total number of operations is equal to the number of operations in the inner transaction plus one. Read more about transaction fees in our [Fees section](../../../learn/fundamentals/fees-resource-limits-metering.mdx). ### Replace-by-fee You can apply a fee-bump transaction to increase a fee originating from your own account. However, if you submit two distinct transactions with the same source account and sequence number with the second transaction being a fee-bump transaction, the second transaction will replace the first transaction in the queue if and only if the fee bid of the second transaction is 10x the fee bid of the first transaction. This number may seem random, it is a deliberate design decision to limit DOS attacks without introducing too much complexity to the protocol. ### Fee-bump transaction envelope When a fee-bump transaction is ready to be signed, it’s wrapped in a transaction envelope. This envelope contains the fee-bump transaction and the signature of the specified fee account. ## Validity of a fee-bump transaction A fee-bump transaction goes through a series of checks in its lifecycle to determine validity. The following conditions must be met: - **Fee account** — the fee account for the fee-bump transaction must exist on the ledger. - **Fee** - The fee must be greater than or equal to the network minimum fee for the number of operations in the inner transaction, plus one for the fee bump. - The fee must also be greater than or equal to the fee specified in the inner transaction. - If the fee-bump transaction is taking advantage of the replace-by-fee, the fee must be 10x higher than the first transaction - **Fee account signature** — the fee-bump transaction envelope must contain a valid signature for the fee account. Additionally, the weight of that signature must meet the low threshold for the fee account, and the appropriate network passphrase must be part of the transaction hash signed by the fee account. - **Fee account balance** — the fee account must have a sufficient XLM balance to cover the fee - **Inner transaction** — the inner transaction must be valid, which means it must meet the requirements described in the [Validity of a Transaction section](../../../learn/fundamentals/transactions/operations-and-transactions.mdx#transaction-and-operation-validity). If validation of the inner transaction is successful, then the result is `FEE_BUMP_INNER_SUCCESS`, and the validation results from the validation of the inner transaction appear in the inner result. If the inner transaction is invalid, the result is `FEE_BUMP_INNER_FAILED`, and the fee-bump transaction is invalid because the inner transaction is invalid. ## Application The sole purpose of a fee-bump transaction is to get an inner transaction included in a transaction set. Since the fee-bump transaction has no side effects other than paying a fee — and at the time the fee is paid the outer transaction must have been valid (otherwise nodes would not have voted for it) — there is no reason to check the validity of the fee-bump transaction at apply time. Therefore, the sequence number of the inner transaction is always consumed at apply time. The inner transaction, however, will still have its validity checked at apply time. Every fee-bump transaction result contains a complete inner transaction result. This inner-transaction result is exactly what would have been produced had there been no fee-bump transaction, except that the inner fee will always be 0. ## Example: implementing a fee-bump transaction This example shows how to create and submit a fee-bump transaction on the Stellar network. Replace secret key values, `SECREY_KEY_1` and `SECREY_KEY_2` of keypairs of your choosing. This is not a production example, and you should take care to never expose your secret keys on the web. ```js // Define the network passphrase (use 'Testnet' for testing and 'Public Global Stellar Network ; September 2015' for production) const networkPassphrase = StellarSdk.Networks.TESTNET; // Create keypairs for the source account and the fee account const sourceKeypair = StellarSdk.Keypair.fromSecret("SECREY_KEY_1"); const feeKeypair = StellarSdk.Keypair.fromSecret("SECREY_KEY_2"); // Load the source account (this requires network interaction) const server = new StellarSdk.rpc.Server("https://soroban-testnet.stellar.org"); const sourceAccount = await server.getAccount(sourceKeypair.publicKey()); // Construct the inner transaction, just a example tx, to transfer 10 XLM to a destination account const innerTransaction = new StellarSdk.TransactionBuilder(sourceAccount, { fee: StellarSdk.BASE_FEE, networkPassphrase, }) .addOperation( StellarSdk.Operation.payment({ destination: "GDWH3P3MNTCMOY42CA7RVEACUUAUPZ73XDYKPYUL3TWOFRF37FD6OVM6", asset: StellarSdk.Asset.native(), amount: "10", }), ) .setTimeout(30) .build(); // Sign the inner transaction with the source account innerTransaction.sign(sourceKeypair); // Build the fee-bump transaction const feeBumpTransaction = StellarSdk.TransactionBuilder.buildFeeBumpTransaction( feeKeypair, StellarSdk.BASE_FEE * 2, innerTransaction, networkPassphrase, ); // Sign the fee-bump transaction with the fee account feeBumpTransaction.sign(feeKeypair); // Submit the fee-bump transaction to the Stellar network server .sendTransaction(feeBumpTransaction) .then((response) => { if (response.status !== "PENDING") { throw response; } return server.pollTransaction(response.hash, { sleepStrategy: StellarSdk.rpc.LinearSleepStrategy, attempts: 5, }); }) .then((response) => { console.log("transaction response:", response); }) .catch((error) => { console.error("Something went wrong!", error); }); ``` --- ## Install and deploy a smart contract with code Install and deploy a smart contract with code This guide will walk you through the process of installing and deploying a smart contract using [js-stellar-sdk](https://github.com/stellar/js-stellar-sdk). We will cover the setup of a sample Rust contract, creating a Node.js project to manage the deployment, and finally, installing the Wasm of the contract and deploying it to the network ## Prerequisites Before you begin, ensure you have the following installed: 1. [Rust](https://www.rust-lang.org) and Cargo (for compiling smart contracts) 2. [Node.js](https://nodejs.org/en) and npm (for running JavaScript deployment scripts) 3. [Stellar CLI](../../smart-contracts/getting-started/setup.mdx#install-the-stellar-cli) ## Initialize a sample Rust Contract ```bash stellar contract init hello-world cd hello-world stellar contract build ``` This sequence of commands creates a new directory for your project, initializes a new Soroban smart contract within that directory, and builds the contract. The build generates .wasm file in the path `hello-world/target/wasm32v1-none/release/hello_world.wasm` ## Create a Node Project 1. Create a new directory for your Node.js project and navigate into it: ```bash mkdir deploy-contract cd deploy-contract ``` 2. Initialize a new Node.js project and install necessary dependencies: ```bash npm init -y npm install @stellar/stellar-sdk ``` ## Set Up Deployment Scripts ### Imports Import necessary modules in your JavaScript file (save it as `deploy.mjs` to enable ES module imports): ```javascript ``` ### Installing the Wasm on the network Create a function to upload the compiled Wasm file: ```javascript async function uploadWasm(filePath) { const bytecode = fs.readFileSync(filePath); const account = await server.getAccount(sourceKeypair.publicKey()); const operation = StellarSDK.Operation.uploadContractWasm({ wasm: bytecode }); return await buildAndSendTransaction(account, operation); } ``` This function reads the compiled Wasm file, retrieves account details from the network, and installs the bytecode using `uploadContractWasm` Stellar operation, which when wrapped in a transaction is sent to the network. ### Deploy contract Deploy the contract by referencing the Wasm hash: ```javascript async function deployContract(response) { const account = await server.getAccount(sourceKeypair.publicKey()); const operation = StellarSDK.Operation.createCustomContract({ wasmHash: response.returnValue.bytes(), address: StellarSDK.Address.fromString(sourceKeypair.publicKey()), salt: response.hash, }); const responseDeploy = await buildAndSendTransaction(account, operation); const contractAddress = StellarSDK.StrKey.encodeContract( StellarSDK.Address.fromScAddress( responseDeploy.returnValue.address(), ).toBuffer(), ); console.log(contractAddress); } ``` This function uses the Wasm hash to deploy the contract using the `createCustomContract` stellar operation, which when wrapped in a transaction is sent to the network, generating a contract address. ### Building, Signing and Sending the Transaction Handle the building, signing, and sending of transactions: ```javascript async function buildAndSendTransaction(account, operations) { const transaction = new StellarSDK.TransactionBuilder(account, { fee: StellarSDK.BASE_FEE, networkPassphrase: StellarSDK.Networks.TESTNET, }) .addOperation(operations) .setTimeout(30) .build(); const tx = await server.prepareTransaction(transaction); tx.sign(sourceKeypair); console.log("Submitting transaction..."); let response = await server.sendTransaction(tx); const hash = response.hash; console.log(`Transaction hash: ${hash}`); console.log("Awaiting confirmation..."); while (true) { response = await server.getTransaction(hash); if (response.status !== "NOT_FOUND") { break; } await new Promise((resolve) => setTimeout(resolve, 1000)); } if (response.status === "SUCCESS") { console.log("Transaction successful."); return response; } else { console.log("Transaction failed."); throw new Error("Transaction failed"); } } ``` This function constructs a transaction, signs it, and submits it to the network, handling any necessary retries for transaction confirmation. ## Running the Script Execute the deployment script: ```javascript const server = new StellarSDK.rpc.Server( "https://soroban-testnet.stellar.org:443", ); const sourceKeypair = StellarSDK.Keypair.fromSecret("Your_Secret_Key"); const wasmFilePath = "../hello-world/target/wasm32v1-none/release/hello_world.wasm"; // Adjust this path as necessary try { let uploadResponse = await uploadWasm(wasmFilePath); await deployContract(uploadResponse); } catch (error) { console.error(error); } ``` Replace "Your_Secret_Key" with your actual secret key. This script initiates the upload of the Wasm file and deploys the contract, resulting in a contract address where the contract is deployed. This is just demo code, so ensure that you handle secrets and private keys securely in production environments and never expose them in your code repositories. This guide should provide you with a clear path to installing and deploying your smart contracts using Javascript code. ## Complete Script Here are all the snippets stacked together in a single file for convenience: ```javascript async function uploadWasm(filePath) { const bytecode = fs.readFileSync(filePath); const account = await server.getAccount(sourceKeypair.publicKey()); const operation = StellarSDK.Operation.uploadContractWasm({ wasm: bytecode }); return await buildAndSendTransaction(account, operation); } async function deployContract(response) { const account = await server.getAccount(sourceKeypair.publicKey()); const operation = StellarSDK.Operation.createCustomContract({ wasmHash: response.returnValue.bytes(), address: StellarSDK.Address.fromString(sourceKeypair.publicKey()), salt: response.hash, }); const responseDeploy = await buildAndSendTransaction(account, operation); const contractAddress = StellarSDK.StrKey.encodeContract( StellarSDK.Address.fromScAddress( responseDeploy.returnValue.address(), ).toBuffer(), ); console.log(contractAddress); } async function buildAndSendTransaction(account, operations) { const transaction = new StellarSDK.TransactionBuilder(account, { fee: StellarSDK.BASE_FEE, networkPassphrase: StellarSDK.Networks.TESTNET, }) .addOperation(operations) .setTimeout(30) .build(); const tx = await server.prepareTransaction(transaction); tx.sign(sourceKeypair); console.log("Submitting transaction..."); let response = await server.sendTransaction(tx); const hash = response.hash; console.log(`Transaction hash: ${hash}`); console.log("Awaiting confirmation..."); while (true) { response = await server.getTransaction(hash); if (response.status !== "NOT_FOUND") { break; } await new Promise((resolve) => setTimeout(resolve, 1000)); } if (response.status === "SUCCESS") { console.log("Transaction successful."); return response; } else { console.log("Transaction failed."); throw new Error("Transaction failed"); } } const server = new StellarSDK.rpc.Server( "https://soroban-testnet.stellar.org:443", ); const sourceKeypair = StellarSDK.Keypair.fromSecret("Your_Secret_Key"); const wasmFilePath = "../hello-world/target/wasm32v1-none/release/hello_world.wasm"; // Adjust this path as necessary try { let uploadResponse = await uploadWasm(wasmFilePath); await deployContract(uploadResponse); } catch (error) { console.error(error); } ``` --- ## Invoke a contract function in a transaction using SDKs This is a simple example using the Stellar SDK to create, simulate, and then assemble a Stellar transaction which invokes an `increment` function of the [auth example contract](../../smart-contracts/example-contracts/auth.mdx). :::tip Please go to [the project homepage](https://github.com/stellar/js-stellar-sdk) of JavaScript SDK to learn how to install it. ::: First, upload the bytes of [the example contract](../../smart-contracts/example-contracts/auth.mdx) onto the blockchain using [Stellar CLI](../../../tools/cli/stellar-cli.mdx). This is called "install" because, from the perspective of the blockchain itself, this contract has been installed. ```bash stellar contract build stellar contract upload --wasm target/wasm32v1-none/release/test_auth_contract.wasm --network testnet ``` This will return a hash; save that, you'll use it soon. For this example, we will use `bc7d436bab44815c03956b344dc814fac3ef60e9aca34c3a0dfe358fcef7527f`. No contract has yet been deployed with this hash. In Soroban, you can have many Smart Contracts which all reference the same Wasm hash, defining their behavior. We'll do that from the following JavaScript code itself. ```ts // As mentioned, we are using Testnet for this example const rpcUrl = "https://soroban-testnet.stellar.org" const networkPassphrase = "Test SDF Network ; September 2015" const wasmHash = "bc7d436bab44815c03956b344dc814fac3ef60e9aca34c3a0dfe358fcef7527f" /** * Generate a random keypair and fund it */ async function generateFundedKeypair() { const keypair = Keypair.random(); const server = new Server(rpcUrl); await server.requestAirdrop(keypair.publicKey()); return keypair } (async () => { // The source account will be used to sign and send the transaction. const sourceKeypair = await generateFundedKeypair() // If you are using a browser, you can pass in `signTransaction` from your // Wallet extension such as Freighter. If you're using Node, you can use // `signTransaction` from `basicNodeSigner`. const { signTransaction } = basicNodeSigner(sourceKeypair, networkPassphrase) // This constructs and simulates a deploy transaction. Once we sign and send // this below, it will create a brand new smart contract instance that // references the wasm we uploaded with the CLI. const deployTx = await Client.deploy( null, // if the contract has a `__constructor` function, its arguments go here { networkPassphrase, rpcUrl, wasmHash, publicKey: sourceKeypair.publicKey(), signTransaction, } ) // Like other `Client` methods, `deploy` returns an `AssembledTransaction`, // which wraps logic for signing, sending, and awaiting completion of the // transaction. Once that all completes, the `result` of this transaction // will contain the final `Client` instance, which we can use to invoke // methods on the new contract. Here we are using JS destructuring to get the // `result` key from the object returned by `signAndSend`, and put it in a // local variable called `client`. const { result: client } = await deployTx.signAndSend() ... ``` :::tip[Client from existing Contract] If you don't need to deploy a contract, and instead already know a deployed contract's ID, you can instantiate a Client for it directly. This uses similar arguments to the ones to `Client.deploy` above, with the addition of `contractId`: ```diff -const deployTx = await Client.deploy( - null, - { +const client = await Client.from({ + contractId: "C123abc…", networkPassphrase, rpcUrl, wasmHash, publicKey: sourceKeypair.publicKey(), signTransaction, }) ``` ::: Now that we instantiated a `client`, we can use it to call methods on the contract. Picking up where we left off: ```ts ... // This will construct and simulate an `increment` transaction. Since the // `auth` contract requires that this transaction be signed, we will need to // call `signAndSend` on it, like we did with `deployTx` above. const incrementTx = await client.increment({ user: sourceKeypair.publicKey(), // who needs to sign value: 1, // how much to increment by }) // For calls that don't need to be signed, you can get the `result` of their // simulation right away, on a call like `client.increment()` above. const { result } = await incrementTx.signAndSend() // Now you can do whatever you need to with the `result`, which in this case // contains the new value of the incrementor/counter. console.log("New incremented value:", result) })(); ``` :::tip Please go to [the project homepage](https://github.com/StellarCN/py-stellar-base) of Python SDK to learn how to install it. In addition, Python provides a lot of example code, which you can find [here](https://github.com/StellarCN/py-stellar-base/tree/main/examples). ::: ```py from stellar_sdk import Keypair, Network, SorobanServer, TransactionBuilder, scval from stellar_sdk import xdr as stellar_xdr from stellar_sdk.exceptions import PrepareTransactionException from stellar_sdk.soroban_rpc import GetTransactionStatus, SendTransactionStatus secret = "SAAPYAPTTRZMCUZFPG3G66V4ZMHTK4TWA6NS7U4F7Z3IMUD52EK4DDEV" rpc_server_url = "https://soroban-testnet.stellar.org:443" network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE # Here we will use a deployed instance of the `increment` example contract. contract_id = "CDOGJBJMRVVXW5K3UJ5BGVJ5RSQXQB4UFVQYVFOIARC2UYXSEPF4YAVR" # The source account will be used to sign and send the transaction. source_keypair = Keypair.from_secret(secret) # Configure SorobanClient to use the `stellar-rpc` instance of your choosing. soroban_server = SorobanServer(rpc_server_url) # Transactions require a valid sequence number (which varies from one account to another). # We fetch this sequence number from the RPC server. source = soroban_server.load_account(source_keypair.public_key) tx = ( # The transaction begins as pretty standard. The source account, minimum fee, # and network passphrase are provided. TransactionBuilder(source, network_passphrase, base_fee=100) # This transaction will be valid for the next 30 seconds .set_timeout(30) # The invocation of the `increment` function of our contract is added to the transaction. .append_invoke_contract_function_op( contract_id=contract_id, function_name="increment", parameters=[scval.to_address(source_keypair.public_key), scval.to_uint32(5)], ).build() ) print(f"builtTransaction: {tx.to_xdr()}") try: # We use the RPC server to "prepare" the transaction. This simulating the # transaction, discovering the soroban data, and updating the # transaction to include that soroban data. If you know the soroban data ahead of # time, you could manually use `set_soroban_data` and skip this step. tx = soroban_server.prepare_transaction(tx) except PrepareTransactionException as e: print(f"Prepare Transaction Failed: {e.simulate_transaction_response}") raise e # Sign the transaction with the source account's keypair. tx.sign(source_keypair) # Let's see the base64-encoded XDR of the transaction we just built. print(f"Signed prepared transaction XDR: {tx.to_xdr()}") # Submit the transaction to the Stellar-RPC server. The RPC server will # then submit the transaction into the network for us. Then we will have to # wait, polling `get_transaction` until the transaction completes. send_transaction_data = soroban_server.send_transaction(tx) print(f"Sent transaction: {send_transaction_data}") if send_transaction_data.status != SendTransactionStatus.PENDING: print(send_transaction_data.error_result_xdr) raise Exception("Send transaction failed") while True: print("Waiting for transaction to be confirmed...") # Poll `get_transaction` until the status is not "NOT_FOUND" get_transaction_data = soroban_server.get_transaction(send_transaction_data.hash) if get_transaction_data.status != GetTransactionStatus.NOT_FOUND: break time.sleep(3) print(f"getTransaction response: {get_transaction_data}") if get_transaction_data.status == GetTransactionStatus.SUCCESS: # The transaction was successful, so we can extract the `result_meta_xdr` transaction_meta = stellar_xdr.TransactionMeta.from_xdr( get_transaction_data.result_meta_xdr ) result = transaction_meta.v3.soroban_meta.return_value # Find the return value from the contract and return it print(f"Transaction result: {scval.from_uint32(result)}") else: # The transaction failed, so we can extract the `result_xdr` print(f"Transaction failed: {get_transaction_data.result_xdr}") raise Exception("Transaction failed") ``` :::tip Please go to [the project homepage](https://github.com/lightsail-network/java-stellar-sdk) of Java SDK to learn how to install it. ::: ```java public class Example { public static void main(String[] args) throws SorobanRpcException, IOException, InterruptedException { // The source account will be used to sign and send the transaction. KeyPair sourceKeypair = KeyPair.fromSecretSeed("SAAPYAPTTRZMCUZFPG3G66V4ZMHTK4TWA6NS7U4F7Z3IMUD52EK4DDEV"); // Configure SorobanClient to use the `stellar-rpc` instance of your choosing. SorobanServer sorobanServer = new SorobanServer("https://soroban-testnet.stellar.org"); // Here we will use a deployed instance of the `increment` example contract. String contractAddress = "CDOGJBJMRVVXW5K3UJ5BGVJ5RSQXQB4UFVQYVFOIARC2UYXSEPF4YAVR"; // Transactions require a valid sequence number (which varies from one account to // another). We fetch this sequence number from the RPC server. TransactionBuilderAccount sourceAccount = null; try { sourceAccount = sorobanServer.getAccount(sourceKeypair.getAccountId()); } catch (AccountNotFoundException e) { throw new RuntimeException("Account not found, please activate it first"); } // The invocation of the `increment` function. InvokeHostFunctionOperation operation = InvokeHostFunctionOperation.invokeContractFunctionOperationBuilder(contractAddress, "increment", List.of( Scv.toAddress(sourceAccount.getAccountId()), Scv.toUint32(5) ) ).build(); // Create a transaction with the source account and the operation we want to invoke. Transaction transaction = new TransactionBuilder(sourceAccount, Network.TESTNET) .addOperation(operation) // The invocation of the `increment` function of our contract is added to the transaction. .setTimeout(30) // This transaction will be valid for the next 30 seconds .setBaseFee(100) // The base fee is 100 stroops (0.00001 XLM) .build(); // We use the RPC server to "prepare" the transaction. This simulating the // transaction, discovering the soroban data, and updating the // transaction to include that soroban data. If you know the soroban data ahead of // time, you could manually use `setSorobanData` and skip this step. try { transaction = sorobanServer.prepareTransaction(transaction); } catch (PrepareTransactionException e) { // You should handle the error here System.out.println("Prepare Transaction Failed: " + e.getMessage()); throw new RuntimeException(e); } // Sign the transaction with the source account's keypair. transaction.sign(sourceKeypair); // Let's see the base64-encoded XDR of the transaction we just built. System.out.println("Signed prepared transaction XDR: " + transaction.toEnvelopeXdrBase64()); // Submit the transaction to the Stellar-RPC server. The RPC server will then // submit the transaction into the network for us. Then we will have to wait, // polling `getTransaction` until the transaction completes. SendTransactionResponse response = sorobanServer.sendTransaction(transaction); if (!SendTransactionResponse.SendTransactionStatus.PENDING.equals(response.getStatus())) { throw new RuntimeException("Sending transaction failed"); } // Poll `getTransaction` until the status is not "NOT_FOUND" GetTransactionResponse getTransactionResponse; while (true) { System.out.println("Waiting for transaction confirmation..."); // See if the transaction is complete getTransactionResponse = sorobanServer.getTransaction(response.getHash()); if (!GetTransactionResponse.GetTransactionStatus.NOT_FOUND.equals(getTransactionResponse.getStatus())) { break; } // Wait one second Thread.sleep(1000); } System.out.println("Get transaction response: " + getTransactionResponse); if (GetTransactionResponse.GetTransactionStatus.SUCCESS.equals(getTransactionResponse.getStatus())) { // The transaction was successful, so we can extract the `resultMetaXdr` TransactionMeta transactionMeta = TransactionMeta.fromXdrBase64(getTransactionResponse.getResultMetaXdr()); SCVal result = transactionMeta.getV3().getSorobanMeta().getReturnValue(); long parsedResult = Scv.fromUint32(result); System.out.println("Transaction result: " + parsedResult); } else { // The transaction failed, so we can extract the `resultXdr` System.out.println("Transaction failed: " + getTransactionResponse.getResultXdr()); } } } ``` :::tip Please go to [the project homepage](https://github.com/rahul-soshte/rs-soroban-client) of Rust Soroban Client Library to learn how to install it. Other examples can be found in the [`examples/`](https://github.com/rahul-soshte/rs-soroban-client/tree/main/examples) folder of the repository. ::: First, deploy [the example contract](../../smart-contracts/example-contracts/auth.mdx) onto the blockchain using [Stellar CLI](../../../tools/cli/stellar-cli.mdx). ```bash stellar contract build stellar contract deploy --wasm ./target/wasm32v1-none/release/soroban_auth_contract.wasm --network testnet ``` This will return a contract ID, in the following example we will use `CBU3OHKZ2BHOHK5VMG3HBWIW3PBQHZLNMHNJUGM23W5NBFA75JMMWAVT`. ```rust use std::time::Duration; use soroban_client::{ account::{Account, AccountBehavior}, address::{Address, AddressTrait}, contract::{ContractBehavior, Contracts}, keypair::{Keypair, KeypairBehavior}, network::{NetworkPassphrase, Networks}, soroban_rpc::TransactionStatus, transaction::{TransactionBehavior, TransactionBuilder, TransactionBuilderBehavior}, Options, Server, }; #[tokio::main] pub async fn main() -> Result<(), Box> { let server_url = "https://soroban-testnet.stellar.org"; let server = Server::new(server_url, Options::default())?; let source_keypair = Keypair::random()?; let source_public_key = &source_keypair.public_key(); let signers = [source_keypair]; // Get account information from server let account_data = server.request_airdrop(source_public_key).await?; let mut source_account = Account::new(source_public_key, &account_data.sequence_number())?; // Calling the increment method of the contract let contract_addr = "CBU3OHKZ2BHOHK5VMG3HBWIW3PBQHZLNMHNJUGM23W5NBFA75JMMWAVT"; let contract = Contracts::new(contract_addr).unwrap(); let tx = TransactionBuilder::new(&mut source_account, Networks::testnet(), None) .fee(1000u32) .add_operation(contract.call( "increment", Some(vec![ Address::account(signers[0].raw_public_key())?.to_sc_val()?, 3u32.into(), ]), )) .build(); // Preparing the transaction, this will call `server.simulate_transaction` and // `assemble_transaction` to enhance the transaction with the soroban data and auths let ptxr = server.prepare_transaction(&tx).await; let mut ptx = match ptxr { Ok(p) => p, Err(e) => { // Manage errors here return Err(e.into()); } }; // Sign the transaction with the source account ptx.sign(&signers); println!("> Calling increment on contract {contract_addr}",); let response = server.send_transaction(ptx).await?; let hash = &response.hash; println!(">> Tx hash: {hash}"); let counter: u32 = match server.wait_transaction(hash, Duration::from_secs(15)).await { Ok(tx_result) if tx_result.status == TransactionStatus::Success => { // On success we can extract the returned value let (_meta, ret_val) = tx_result.to_result_meta().expect("No result meta"); ret_val .expect("None returned value") .try_into() .expect("Return value is not u32") } _ => { return Err("Failed to create contract".into()); } }; println!(">> Counter: {counter}",); println!(); Ok(()) } ``` --- ## Path payments In a path payment, the asset received differs from the asset sent. Rather than the operation transferring assets directly from one account to another, path payments cross through the SDEX and/or liquidity pools before arriving at the destination account. For the path payment to succeed, there has to be a DEX offer or liquidity pool exchange path in existence. It can sometimes take several hops of conversion to succeed. For example: Account A sells XLM → [buy XLM / sell ETH → buy ETH / sell BTC → buy BTC / sell USDC] → Account B receives USDC It is possible for path payments to fail if there are no viable exchange paths. For more information on the Stellar Decentralized Exchange and Liquidity Pools, see the [Liquidity on Stellar section](../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx). ## Operations Path payments use the Path Payment Strict Send or Path Payment Strict Receive operations. ### Path Payment Strict Send Allows a user to specify the amount of the asset to send. The amount received will vary based on offers in the order books and/or liquidity pools. ### Path Payment Strict Receive Allows a user to specify the amount of the asset received. The amount sent will vary based on the offers in the order books/liquidity pools. ## Path payments - more info - Path payments don’t allow intermediate offers to be from the source account as this would yield a worse exchange rate. You’ll need to either split the path payment into two smaller path payments or ensure that the source account’s offers are not at the top of the order book. - Balances are settled at the very end of the operation. - This is especially important when (`Destination, Destination Asset) == (Source, Send Asset`) as this provides a functionality equivalent to getting a no-interest loan for the duration of the operation. - `Destination min` is a protective measure, it allows you to specify a lower bound for an acceptable conversion. If offers in the order books are not favorable enough for the operation to deliver that amount, the operation will fail. ## Example First, ensure the receiver has a trustline established for the asset they will receive. In this example, we will use USDC as the asset received. The sender will send XLM, which will be converted to USDC through the path payment operation. ```js Horizon, Asset, Keypair, TransactionBuilder, Networks, BASE_FEE, Operation, Memo, } from "@stellar/stellar-sdk"; const USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; // USDC issuer on Stellar Testnet const USDC_ASSET = new Asset("USDC", USDC_ISSUER); // USDC asset on Stellar Testnet const RECEIVER_SECRET = "S..."; // Receiver's secret key const SENDER_SECRET = "S..."; // Sender's secret key const horizonServer = new Horizon.Server("https://horizon-testnet.stellar.org"); // Create a USDC trustline for the receiver const receiverKP = Keypair.fromSecret(RECEIVER_SECRET); let account = await horizonServer.loadAccount(receiverKP.publicKey()); let transaction = new TransactionBuilder(account, { fee: BASE_FEE * 100, networkPassphrase: Networks.TESTNET, }) .addOperation( Operation.changeTrust({ asset: USDC_ASSET, limit: "10", }), ) .addMemo(Memo.text("Trusting USDC")) .setTimeout(30) .build(); transaction.sign(receiverKP); const resp = await SERVER.submitTransaction(transaction); console.log("resp", resp); ``` Now let's send a path payment from the sender to the receiver, converting XLM to USDC. ```js // Use path payment to send XLM from the receiver to the sender, who receives USDC let senderKP = Keypair.fromSecret(SENDER_SECRET); let account = await horizonServer.loadAccount(senderKP.publicKey()); let transaction = new TransactionBuilder(account, { fee: BASE_FEE * 100, networkPassphrase: Networks.TESTNET, }) .addOperation( Operation.pathPaymentStrictReceive({ sendAsset: Asset.native(), // Sending XLM sendMax: "10", // Maximum amount of XLM to send destAsset: USDC_ASSET, // Receiving USDC destAmount: "1", // Amount of USDC to receive destination: receiverKP.publicKey(), // Receiver's public key }), ) .addMemo(Memo.text("XLM to USDC")) .setTimeout(30) .build(); transaction.sign(senderKP); const resp = await horizonServer.submitTransaction(transaction); console.log("resp", resp); ``` --- ## Pooled accounts: muxed accounts and memos When building an application or service on Stellar, one of the first things you have to decide is how to handle user accounts. You can create a Stellar account for each user, but most custodial services, including cryptocurrency exchanges, choose to use a single pooled Stellar account to handle transactions on behalf of their users. In these cases, the muxed account feature can map transactions to individual accounts via an internal customer database. :::note We used memos in the past for this purpose, however, using muxed accounts is better in the long term. At this time, there isn't support for muxed accounts by all wallets, exchanges, and anchors, so you may want to support both memos and muxed accounts, at least for a while. ::: ## Pooled accounts A pooled account allows a single Stellar account ID to be shared across many users. Generally, services that use pooled accounts track their customers in a separate, internal database and use the muxed accounts feature to map an incoming and outgoing payment to the corresponding internal customer. The benefits of using a pooled account are lower costs – no base reserves are needed for each account – and lower key complexity – you only need to manage one account keypair. However, with a single pooled account, it is now your responsibility to manage all individual customer balances and payments. You can no longer rely on the Stellar ledger to accumulate value, handle errors and atomicity, or manage transactions on an account-by-account basis. ## Muxed accounts Muxed accounts are embedded into the protocol for convenience and standardization. They distinguish individual accounts that all exist under a single, traditional Stellar account. They combine the familiar `GABC…` address with a 64-bit integer ID. Muxed accounts do not exist on the ledger, but their shared underlying `GABC…` account does. Muxed accounts are defined in [CAP-27](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0027.md), introduced in Protocol 13, and their string representation is described in [SEP-23](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md). It is safe for all wallets to implement sending to muxed accounts. If you wish to receive deposits to muxed accounts please keep in mind that they are not yet supported by all wallets and exchanges. ### Address format Muxed accounts have their own address format that starts with an M prefix. For example, from a traditional Stellar account address: `GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ`, we can create new muxed accounts with different IDs. The IDs are embedded into the address itself- when you parse the muxed account addresses, you get the G address from above plus another number. - `MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUQ` has the ID 0, while - `MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAABUTGI4` has the ID 420. Both of these addresses will act on the underlying `GA7Q…` address when used with one of the supported operations. ### Supported operations Not all operations can be used with muxed accounts. Here is what you can use them for: - The source account of any operation or transaction; - The fee source of a fee-bump transaction; - The destination of all three types of payments: - `Payment`, - `PathPaymentStrictSend`, and - `PathPaymentStrictReceive`; - The destination of an AccountMerge; and - The target of a `Clawback` operation (i.e. the from field). We will demonstrate some of these in the examples section. There’s no validation on IDs and as far as the Stellar network is concerned, all supported operations operate exactly as if you did not use a muxed account. ## Examples The following examples demonstrate how muxed accounts work in practice, showing the three main payment scenarios: standard account-to-account, muxed-to-unmuxed, and muxed-to-muxed payments. **_Note:_** _The complete code implementation for all examples follows after this section. You can run individual examples using their respective functions or execute all three sequentially using the main function._ Muxed accounts (M...) are a client-side abstraction that don't actually exist on the Stellar ledger — only the underlying G account lives on-chain. This means operations like loading account data must always use the base G account, while payment operations can still provide either M or G address. The code diverges only slightly to handle this distinction between the logical muxed account and the actual account. **Transaction Fees:** All transactions incur a fee (typically 100 stroops or 0.0000100 XLM). Since the custodian account is the source of all transactions in these examples, fees are always debited from the custodian's balance. You'll see this reflected in the before/after balance comparisons, where the custodian account decreases by both the payment amount and the transaction fee. ### Example 1 - Basic Payment (G → G) This example establishes baseline behavior with a standard payment between two regular Stellar accounts. Both accounts exist directly on the ledger, demonstrating traditional Stellar payment flow without muxed account abstractions. The custodian account balance decreases by 10 XLM plus transaction fees, while the outsider account increases by exactly 10 XLM. ### Example 2 - Muxed to Unmuxed Payment (M → G) This demonstrates a payment from a muxed account (representing a custodial customer) to a regular Stellar account. The underlying custodian account executes the payment, but the transaction includes the muxed address to identify which customer initiated it. The transaction is signed with custodian keys since muxed accounts have no secret keys. The custodian account balance decreases by 10 XLM plus fees. ### Example 3 - Muxed to Muxed Payment (M → M) When two muxed accounts share the same underlying account, payments between them are essentially the account sending to itself. The payment amount (+10 XLM and -10 XLM) cancels out, but transaction fees are still charged since the operation is recorded on the ledger. The account balance decreases only by the transaction fee. You may want to detect these transactions in your application to avoid unnecessary fees. Note that payments between muxed accounts with different underlying accounts would behave like normal G-to-G payments. --- ## Code Implementation _The following code demonstrates all three examples using Stellar RPC. You can run all examples sequentially with `runAllMuxedExamples()` or execute individual examples using their respective functions._ ```js const server = new sdk.rpc.Server("https://soroban-testnet.stellar.org"); const custodian = sdk.Keypair.random(); const outsider = sdk.Keypair.random(); let custodianAcc, outsiderAcc, customers; // Setup function to fund accounts and create muxed customers async function preamble() { console.log("=== FUNDING ACCOUNTS WITH XLM ==="); await Promise.all([ server.requestAirdrop(custodian.publicKey()), server.requestAirdrop(outsider.publicKey()), ]); console.log("All accounts funded with XLM via airdrop"); [custodianAcc, outsiderAcc] = await Promise.all([ server.getAccount(custodian.publicKey()), server.getAccount(outsider.publicKey()), ]); customers = ["1", "22", "333", "4444"].map( (id) => new sdk.MuxedAccount(custodianAcc, id), ); console.log("\n=== ACCOUNT SETUP ==="); console.log("Custodian:\n ", custodian.publicKey()); console.log("Outsider:\n ", outsider.publicKey()); console.log("Customers:"); customers.forEach((customer) => { console.log( " " + customer.id().padStart(4, " ") + ":", customer.accountId(), ); }); } // Example 1: Basic payment between two G accounts async function runUnmuxedExample() { console.log("=== BASIC PAYMENT EXAMPLE (G → G) ==="); console.log("=== INITIAL BALANCES ==="); await showBalance(custodianAcc); await showBalance(outsiderAcc); await makePayment( custodianAcc, outsiderAcc, "Basic Payment from G to G address", ); console.log("\n=== FINAL BALANCES ==="); const finalCustodianAcc = await server.getAccount(custodian.publicKey()); const finalOutsiderAcc = await server.getAccount(outsider.publicKey()); await showBalance(finalCustodianAcc); await showBalance(finalOutsiderAcc); console.log("\n=== BASIC PAYMENT EXAMPLE COMPLETED ==="); } // Example 2: Payment from M account to G account async function runMuxedToUnmuxedExample() { console.log("=== MUXED TO UNMUXED PAYMENT EXAMPLE (M → G) ==="); console.log("=== INITIAL BALANCES ==="); await showBalance(custodianAcc); await showBalance(outsiderAcc); const src = customers[0]; console.log( `Sending 10 XLM from Customer ${src.id()} to ${formatAccount( outsiderAcc.accountId(), )}.`, ); await makePayment(src, outsiderAcc, "Payment from M to G address"); console.log("\n=== FINAL BALANCES ==="); const finalCustodianAcc = await server.getAccount(custodian.publicKey()); const finalOutsiderAcc = await server.getAccount(outsider.publicKey()); await showBalance(finalCustodianAcc); await showBalance(finalOutsiderAcc); console.log("\n=== MUXED TO UNMUXED EXAMPLE COMPLETED ==="); } // Example 3: Payment between two M accounts async function runMuxedToMuxedExample() { console.log("=== MUXED TO MUXED PAYMENT EXAMPLE (M → M) ==="); console.log("=== INITIAL BALANCES ==="); await showBalance(custodianAcc); const src = customers[1]; // Customer 22 const dest = customers[2]; // Customer 333 console.log( `Sending 10 XLM from Customer ${src.id()} to Customer ${dest.id()}.`, ); await makePayment(src, dest, "Payment from M to M address"); console.log("\n=== FINAL BALANCES ==="); const finalCustodianAcc = await server.getAccount(custodian.publicKey()); await showBalance(finalCustodianAcc); console.log("\n=== MUXED TO MUXED EXAMPLE COMPLETED ==="); } // Main function that runs preamble once and then all three examples async function runAllMuxedExamples() { try { // Run setup/funding only once await preamble(); // Show initial state console.log("=== OVERALL INITIAL BALANCES ==="); await showBalance(custodianAcc); await showBalance(outsiderAcc); console.log("\n" + "=".repeat(60) + "\n"); // Run all three examples sequentially await runUnmuxedExample(); console.log("\n" + "=".repeat(60) + "\n"); await runMuxedToUnmuxedExample(); console.log("\n" + "=".repeat(60) + "\n"); await runMuxedToMuxedExample(); console.log("\n=== ALL MUXED ACCOUNT EXAMPLES COMPLETED ==="); } catch (error) { console.error("Error in examples:", error.message); } } // Helper function to format account ID with label function formatAccount(accountId) { const shortId = accountId.substring(0, 8); if (accountId === custodian.publicKey()) { return `${shortId} (Custodian)`; } else if (accountId === outsider.publicKey()) { return `${shortId} (Outsider)`; } // Check if it's a muxed account by finding the matching customer const matchingCustomer = customers?.find( (customer) => customer.accountId() === accountId, ); if (matchingCustomer) { return `${accountId.substring(0, 8)}...${accountId.slice( -6, )} (Customer ${matchingCustomer.id()})`; } return shortId; } function scaleAsset(x) { return Number(x) / 10000000; // Preserves decimal precision } // Helper function to get XLM balance using RPC function getXLMBalance(accountId) { return server .getAccountEntry(accountId) .then((accountEntry) => { return scaleAsset(accountEntry.balance().toBigInt()).toFixed(7); }) .catch(() => "0"); } // Helper function to submit transaction and poll for completion using RPC function submitAndPollTransaction(transaction, description = "Transaction") { return server.sendTransaction(transaction).then((submitResponse) => { if (submitResponse.status !== "PENDING") { throw new Error( `Transaction submission failed: ${submitResponse.status}`, ); } console.log(`${description} submitted: ${submitResponse.hash}`); return server.pollTransaction(submitResponse.hash).then((finalResponse) => { if (finalResponse.status === "SUCCESS") { console.log(`${description} completed successfully`); return { hash: submitResponse.hash, status: finalResponse.status, resultXdr: finalResponse.resultXdr, }; } else { throw new Error(`${description} failed: ${finalResponse.status}`); } }); }); } function buildTx(source, signer, ops) { var tx = new sdk.TransactionBuilder(source, { fee: sdk.BASE_FEE, networkPassphrase: sdk.Networks.TESTNET, }); ops.forEach((op) => tx.addOperation(op)); tx = tx.setTimeout(30).build(); tx.sign(signer); return tx; } // Helper function to load account, handling muxed accounts function loadAccount(account) { if (sdk.StrKey.isValidMed25519PublicKey(account.accountId())) { return loadAccount(account.baseAccount()); } else { return server.getAccount(account.accountId()); } } // Helper function to display balance of an account async function showBalance(acc) { const balance = await getXLMBalance(acc.accountId()); console.log(`${formatAccount(acc.accountId())}: ${balance} XLM`); } // Function to make a payment from source to destination account async function makePayment(source, dest, description = "Payment") { console.log( `\nPayment: ${formatAccount(source.accountId())} → ${formatAccount( dest.accountId(), )} (10 XLM)`, ); const accountBeforePayment = await loadAccount(source); console.log("Before payment:"); await showBalance(accountBeforePayment); let payment = sdk.Operation.payment({ source: source.accountId(), destination: dest.accountId(), asset: sdk.Asset.native(), amount: "10", }); let tx = buildTx(accountBeforePayment, custodian, [payment]); await submitAndPollTransaction(tx, description); const accountAfterPayment = await loadAccount(source); console.log("After payment:"); await showBalance(accountAfterPayment); } // Run the main function runAllMuxedExamples(); ``` ### Running the Examples - **All Examples**: `runAllMuxedExamples()` - Runs setup once and executes all three examples - **Individual Examples**: - `runUnmuxedExample()` - Basic G→G payment - `runMuxedToUnmuxedExample()` - Muxed→Unmuxed payment - `runMuxedToMuxedExample()` - Muxed→Muxed payment **_Note:_** _When running individual examples, ensure you call `preamble()` first to set up and fund the accounts._ ### More Examples As is the case for most protocol-level features, you can find more usage examples and inspiration in the relevant test suite for your favorite SDK. For example, [here](https://github.com/stellar/js-stellar-base/blob/master/test/unit/muxed_account_test.js) are some of the JavaScript test cases. ### FAQs **What happens if I pay a muxed address, but the recipient doesn’t support them?** In general, you should not send payments to muxed addresses on platforms that do not support them. These platforms will not be able to provide muxed destination addresses in the first place. Even still, if this does occur, parsing a transaction with a muxed parameter without handling them will lead to one of two things occurring: - If your SDK is out-of-date, parsing will error out. You should upgrade your SDK. For example, the JavaScript SDK will throw a helpful message: ``` “destination is invalid; did you forget to enable muxing?” ``` - If your SDK is up-to-date, you will see the muxed (`M...`) address parsed out. What happens next depends on your application. Note, however, that the operation will succeed on the network. In the case of payments, for example, the destination’s parent address will still receive the funds. **What happens if I want to pay a muxed account, but my platform does not support them?** In this case, do not use a muxed address. The platform will likely fail to create the operation. You probably want to use the legacy method of including a transaction memo, instead. **What do I do if I receive a transaction with muxed addresses and a memo ID?** In an ideal world, this situation would never happen. You can determine whether or not the underlying IDs are equal; if they aren’t, this is a malformed transaction and we recommend not submitting it to the network. **What happens if I get errors when using muxed accounts?** In up-to-date versions of Stellar SDKs, muxed accounts are natively supported by default. If you are using an older version of an SDK, however, they may still be hidden behind a feature flag. If you get errors when using muxed addresses on supported operations like: “destination is invalid; did you enable muxing?” We recommend upgrading to the latest version of any and all Stellar SDKs you use. However, if that’s not possible for some reason, you will need to enable the feature flag before interacting with muxed accounts. Consult your SDK’s documentation for details. **What happens if I pass a muxed address to an incompatible operation?** Only certain operations allow muxed accounts, as described above. Passing a muxed address to an incompatible parameter with an up-to-date SDK should result in a compilation or runtime error at the time of use. For example, when using the JavaScript SDK incorrectly: ```js const mAddress = "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAABUTGI4"; transactionBuilder.addOperation( Operation.setTrustLineFlags({ trustor: mAddress, // wrong! asset: someAsset, flags: { clawbackEnabled: false }, }), ); ``` The runtime result would be: “Error: invalid version byte. expected 48, got 96” This error message indicates that the `trustor` failed to parse as a Stellar account ID (`G...`). In other words, your code will fail and the invalid operation will never reach the network. **How do I validate Stellar addresses?** You should use the validation methods provided by your SDK or carefully adhere to [SEP-23](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md). For example, the JavaScript SDK provides the following methods for validating Stellar addresses: ```ts namespace StrKey { function isValidEd25519PublicKey(publicKey: string): boolean; function isValidMed25519PublicKey(publicKey: string): boolean; } ``` There are also abstractions for constructing and managing both muxed and regular accounts; consult your SDK documentation for details. ## Memo - differentiated accounts Prior to the introduction of muxed accounts, products and services that relied on pooled accounts often used transaction memos to differentiate between users. Supporting muxed accounts is better in the long term, but for now you may want to support both memos and muxed accounts as all exchanges, anchors, and wallets may not support muxed accounts. To learn about what other purposes memos can be used for, see our [Memos section](../../../learn/fundamentals/transactions/operations-and-transactions.mdx#memo). ## Why are muxed accounts better in the long term? Muxed accounts are a better approach to differentiating between individuals in a pooled account because they have better: - Shareability — rather than worrying about error-prone things like copy-pasting memo IDs, you can just share your M... address. - SDK support — the various SDKs support this abstraction natively, letting you create, manage, and work with muxed accounts easily. This means that you may see muxed addresses appear when parsing any of the fields that support them, so you should be ready to handle them. Refer to your SDK’s documentation for details; for example, [v7.0.0](https://github.com/stellar/js-stellar-base/releases/tag/v7.0.0) of the JavaScript SDK library stellar-base describes all of the fields and functions that relate to muxed accounts. - Efficiency — by combining related virtual accounts under a single account’s umbrella, you can avoid holding reserves and paying fees for all of them to exist in the ledger individually. You can also combine multiple payments to multiple destinations within a single transaction since you do not need the per-transaction memo field anymore. --- ## Send to and receive payments from Contract Accounts Payments between Contract Accounts (C addresses) and Classic Accounts (G addresses) are supported on a protocol level, but there are differences in the implementations that are required to support them. This guide will cover implementations for applications that use classic G accounts and want to support transacting with Contract Accounts. ## Receiving payments from Contract Accounts (C to G) If a sender is using a Contract Account, and sending an [SAC token](../../../tokens/stellar-asset-contract.mdx) (such as XLM, USDC, etc.), the transaction will be received by the G account normally since it is supported on a protocol level. ### Receiving payments from Contract Accounts requiring a memo Entities such as exchanges and asset issuers usually use omnibus accounts, using a shared receiving address and a unique identifier memo like: - **Account:** `GBLVHX33XGOBDOXK7ERDL34NVH6WW7VTT2OBAHPJ7G3D423HBG5NOMY7` - **Memo ID:** `123456789` Transactions involving Contract Accounts make use of contract invocations to execute a 'transfer' function on the asset contract. This kind of transaction doesn't allow for the 'memo' field to be used, so instead, the sending app will encode it in the address, generating a unique muxed address that includes the receiving account (GBLVHX…) and provided id in the muxed id field (123456789). To enable support for deposits coming from Contract Accounts, nothing needs to change in the information displayed to customers or the deposit accounts used by the exchange. Wallets will still use the G account and memo provided to send a payment to the exchange's G account including the memo with the unique ID. That's the main difference between receiving from customers using G or Contract Accounts: instead of looking at the memo field to identify the individual account, you'll look at the muxed ID. Everything else stays the same. #### Example using Horizon **Receiving from a G address** The memo field will be present in the response of Horizon's [accounts/:account_id/transactions](../../../data/apis/horizon/api-reference/get-transactions-by-account-id.api.mdx) endpoint **Example response from a G address:** ```json ... { "memo":"12345", "memo_bytes":"MTIzNDU=", "_links":{ "self":{ "..." }, "id":"05e11abd0d70776d62f1e3c7c2ba6a08f88641bb226d6be783b8e850bc70b345", "paging_token":"4097295721172992", "successful":true, "hash":"05e11abd0d70776d62f1e3c7c2ba6a08f88641bb226d6be783b8e850bc70b345", "ledger":953976, "created_at":"2026-02-10T23:49:33Z", "source_account":"GB5FM5GICRYGBFMEGPMYEFDZQHGUYF75Z7WNB2JL5A3A57HIB4CP3TCL", "source_account_sequence":"4021081526501377", "fee_account":"GB5FM5GICRYGBFMEGPMYEFDZQHGUYF75Z7WNB2JL5A3A57HIB4CP3TCL", "fee_charged":"100", "max_fee":"100", "operation_count":1, "envelope_xdr":"AAAAAgAAAAB6...sKa6KzAKWP/BabMM", "result_xdr":"AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", "fee_meta_xdr":"AAAAAgAAAAMAD...AAAAQAAAAAAAAAAAAAAAAAAAA==", "memo_type":"text", "signatures":[ "7pD5p6H9ivnGNj1WO4CVW9HWQx2uPAv0xwpIDqsdXMALO4pqb2GwwBFXNc3+dZb0+9n5YrCmuiswClj/wWmzDA==" ], "preconditions":{ "timebounds":{ "min_time":"0", "max_time":"1770767550" } } } }, ... ``` **Receiving from a Contract Account** The payment details will be present in Horizon's [accounts/:account_id/payments](../../../data/apis/horizon/api-reference/get-payments-by-account-id.api.mdx) endpoint, under 'asset_balance_changes', which will present the encoded ID and the underlying G address separately. It is important to note the 'asset_balance_changes' section only supports Stellar assets and transactions that make use of the `transfer` function in the [Stellar Asset Contract (SAC)](../../../tokens/stellar-asset-contract.mdx). **Example response from a Contract Account:** ```json ..., { "_links":{ ... }, "id":"258816945460609125", "paging_token":"258816945460609025", "transaction_successful":true, "source_account":"GBFB3VVWWJUOS2WPIXLKEPK2B5LNIA6UY43FARXY4H3LCFCT2KESVS6A", "type":"invoke_host_function", "type_i":24, "created_at":"2025-12-12T02:48:28Z", "transaction_hash":"5fcb052da40a6570e983ebf48d11e037b159964d27d7a77111e241ff82a7e58", "function":"HostFunctionTypeHostFunctionTypeInvokeContract", "parameters":[ { "value":"AAAAEgAAAAGt785ZruUpaPdgYdSUwlJbdWWfpClqZfSZ7ynlZHfklg==", "type":"Address" }, { "value":"AAAADwAAAAh0cmFuc2Zlcg==", "type":"Sym" }, { "value":"AAAAEgAAAAE9m2qWxXmeXBEUPeloYrEu8eZsK1yvbKfUgWuBEu+IYw==", "type":"Address" }, { "value":"AAAAEgAAAAIAAAAAAAAAezLeBRB7Xvlp9tNUC1vK3+K4158NprSAUc3gng6hBtBY", "type":"Address" }, { "value":"AAAACgAAAAAAAAAAAAAAAAABhqA=", "type":"I128" } ], "address":"", "salt":"", "asset_balance_changes":[ { "asset_type":"credit_alphanum4", "asset_code":"USDC", "asset_issuer":"GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", "type":"transfer", "from":"CA6ZW2UWYV4Z4XARCQ66S2DCWEXPDZTMFNOK63FH2SAWXAIS56EGHGH5", "to":"GB5FM5GICRYGBFMEGPMYEFDZQHGUYF75Z7WNB2JL5A3A57HIB4CP3TCL", "amount":"0.0100000", "destination_muxed_id":"123" } ] }, ... ``` For a detailed code example, see the section 'Monitoring Payments -> Using the Horizon API' of [this repository's examples](https://github.com/fazzatti/c-address-payment-examples#monitoring-payments). #### Example using RPC Since the release of the Unified Asset Events ([CAP-67](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md)) on Protocol 23, a number of transactions involving Stellar assets now emit standardized events that can be ingested by both the Horizon API and the Stellar RPC. If funds are sent through a 'payment' operation or by invoking the 'transfer' function of the asset contract, the same kind of event is emitted for both cases. To monitor these payments, the method '[getEvents](../../../data/apis/rpc/api-reference/methods/getEvents.mdx)' of the Stellar RPC is used to stream events emitted by the Stellar Asset Contract. A filter can be used to specify the target asset (SAC contract ID) and accounts. **From a G address** When the receiver is a normal G address, the value portion of the event is just the amount received. ```json { "inSuccessfulContractCall": true, "topicJson": [ { "symbol": "transfer" }, { "address": "GD6MYLASTCZ3H4UFLH2YSIASHJNBFUCKC5YHGU44VCH2BGHN3WJQX6LG" }, { "address": "GA4PDUF3BLBSK47UTNY44AIZAP5EPHW2UIJRKB5HN76HHC55IWKSIHFD" }, { "string": "native" } ], "valueJson": { "i128": "10000000" } } ``` **From a Contract Account** When the transaction is sent using a muxed account, the value portion of the event is broken down into an object containing the amount and the unique ID encoded in the address. ```json { "inSuccessfulContractCall": true, "topicJson": [ { "symbol": "transfer" }, { "address": "GD6MYLASTCZ3H4UFLH2YSIASHJNBFUCKC5YHGU44VCH2BGHN3WJQX6LG" }, { "address": "GA4PDUF3BLBSK47UTNY44AIZAP5EPHW2UIJRKB5HN76HHC55IWKSIHFD" }, { "string": "native" } ], "valueJson": { "map": [ { "key": { "symbol": "amount" }, "val": { "i128": "10000000" } }, { "key": { "symbol": "to_muxed_id" }, "val": { "u64": "123456789" } } ] } } ``` For a detailed code example, see the section ‘_Monitoring Payments \-\> Using the Stellar RPC_’ of [this repository's examples](https://github.com/fazzatti/c-address-payment-examples#using-the-stellar-rpc). ### Sending payments from Contract Accounts to a recipient that requires a memo If a sender is using a Contract Account, and sending an [SAC token](../../../tokens/stellar-asset-contract.mdx) (such as XLM, USDC, etc.) to a recipient that requires a memo, (an exchange, for example) it should create a muxed address that includes the receiving account (GBLVHX…) and provided memo id in the muxed id field (123456789). In the user inteface, the resulting memo address doesn't need to be surfaced to the user, it should be used directly in the transaction. If the receiving party already provides an M address, it should be used directly, without requiring a memo field. Since this is the value provided by the user, it should be used in the transaction and displayed to the user. ## Sending payments to Contract Accounts (G to C) ### Sending a ‘transfer’ via Stellar RPC This approach is simpler but requires the use of an RPC and involves a slightly different transaction workflow than a traditional native operation. For a detailed code example, see the section ‘_Sending Payments through the SAC \-\> Using the Stellar RPC_’ of [this repository's examples](https://github.com/fazzatti/c-address-payment-examples#sending-payments-through-the-sac). **Assembling the operation and transaction** To perform a ‘transfer’ through an SAC, it is necessary to assemble a ‘_InvokeHostFunctionOp_’ operation with similar arguments as a ‘payment’ operation along with the contract identifier and target function name(‘transfer’). This can be seen in detail in the ‘[assembleTransferOperation](https://github.com/fazzatti/c-address-payment-examples/blob/main/src/core/assemble-transfer-operation.ts)’ function of the demo. While the example demonstrates this process in greater detail, it is possible to use the [js-stellar-sdk](https://github.com/stellar/js-stellar-sdk) and its Contract client in a simpler manner. The transaction is then built by adding this operation and configuring the additional parameters as any other ‘payment’ transaction. **Simulating the transaction** After building the transaction, the RPC method for ‘simulateTransaction’ is used to simulate an execution of this transaction against the current state of the network, identify if it is expected to succeed and, also provide a number of additional parameters about this execution. This process is fully handled by the SDK in a very simple step by using the ‘_prepareTransaction_’ function of the RPC server client. It automatically simulates and provides an updated transaction object. See this process in detail [in the demo](https://github.com/fazzatti/c-address-payment-examples/blob/0e1c021e5afbefd5946878d53934b62cf5a8bb68/src/core/rpc-transaction.ts#L43). **Signing** When the same account is used as the ‘sender’ of the funds and also the ‘source’ of the transaction, the signing step will be exactly the same. Once the updated transaction object is returned from the previous step, you just sign it with the source/sender account and it will be ready. When the ‘source’ account of the transaction differs from the ‘sender’ account, an additional signed entry will be necessary to authorize the ‘transfer’ invocation explicitly. Refer to the [Soroban Authorization Framework documentation](../../../learn/fundamentals/contract-development/authorization.mdx#soroban-authorization-framework) for further details. **Sending the Transaction for processing** When a transaction is submitted for processing through the Stellar RPC, differently from the Horizon API, it will resolve immediately confirming its submission but not the processing. It is necessary to poll the transaction status for the next moment until you get a confirmation that it was successfully processed or a status update on a potential failure. ### Sending a ‘transfer’ via Horizon API When sending a ‘transfer’ via the Horizon API, this approach introduces a few extra steps when compared to the use of an RPC. For a detailed code example, see the section ‘_Sending Payments through the SAC \-\> Using the Horizon API_’ of [this repository's examples](https://github.com/fazzatti/c-address-payment-examples#using-the-horizon-api-1). **Assembling the operation and transaction** This process is mostly the same as described in the previous section ‘[Sending a ‘transfer’ via Stellar RPC](#sending-a-transfer-via-stellar-rpc)’ with some slight modifications. Since the Horizon API **does not** provide an endpoint to simulate a transaction execution, this means that when the transaction is assembled we need to ensure it also contains: - The **Soroban authorization entries** that fulfill the smart contracts authorization requirements. - The **Soroban data** object, detailing the footprint, resources, and resource fee expected for this contract invocation. These are normally populated automatically based on the output of the simulation and by simply signing and adding the authorization entries generated by the RPC. Here, we need to include these manually. The **auth entries** can be easily predicted and assembled based on the configuration. Assuming the ‘sender’ account is the same as the ‘source’ account of the transaction, a source-account credential needs to be added to the operation, indicating this invocation will be used the same envelope signature to authorize the ‘transfer’. See this process in detail in the [‘assembleSourceAuthEntry’ function](https://github.com/fazzatti/c-address-payment-examples/blob/main/src/core/assemble-source-auth.ts) of the demo. Once the auth entries are assembled and signed(when not a ‘source-account’ entry), they need to be included in the operation, in the ‘auth’ parameter. For the **Soroban Data** on the other hand, the process can be a bit harder to execute manually. Considering all SAC contracts use the same native implementation, we can expect all ‘transfer’ invocations to be very similar. The amount of resources consumed shouldn't vary much with the exception of some edge cases such as the transaction involving archived entries or a custom authorization for a smart wallet as the sender. See this process of manually assembling the Soroban Data in [the function ’getSorobanData’](https://github.com/fazzatti/c-address-payment-examples/blob/main/src/core/get-sorobandata.ts) of the demo. Once the **Soroban Data** is assembled, it is included in the **Transaction Builder.** **Signing** After building the transaction, assuming the ‘auth entries’ from the previous step were configured correctly, the transaction can be signed normally by the source account. **Sending the Transaction for processing** This step remains the same as any transaction submitted through the Horizon API. --- ## Send and receive payments ## Payments Overview A payment constitutes the transfer of a token from one account to another. On Stellar network a token can be either a Stellar asset or a custom contract token which follows the [SEP-41 token standard](../../../tokens/stellar-asset-contract.mdx#overview). There are two primary ways payments are transacted on Stellar: - Using Stellar's payment-related [operations](../../../learn/fundamentals/transactions/list-of-operations.mdx) - [Invoking a function](../../../learn/fundamentals/contract-development/contract-interactions/overview.mdx) on a token contract. The approach you should take depends on the use case: - If you want to make a payment of a Stellar asset between two Stellar accounts, use the payment operations. Transaction fees are cheaper when using them compared to the fees for invoking the asset's token contract directly which is referred to as the [Stellar Asset Contract](../../../tokens/stellar-asset-contract.mdx#overview). - If you want to make a payment of a Stellar asset between a Stellar account and a contract address, or between two contract addresses, then the asset's contract must be used. Stellar's payment-related operations cannot have contract addresses as their source or destination. - If you want to make a payment of a custom contract token which is not a Stellar asset but follows the [SEP-41 token standard](../../../tokens/stellar-asset-contract.mdx#overview), you must use the token's contract. Stellar's payment operations can only be used to transfer Stellar assets. To learn more about the differences between Stellar assets and contract tokens, see the [Tokens](../../../tokens/README.mdx) overview. :::info In the following code samples, proper error checking is omitted for brevity. However, you should _always_ validate your results, as there are many ways that requests can fail. ::: ## Using Payments Example This example highlights the approach mentioned for transacting payments using operations and assets and using the [Stellar RPC](../../../data/apis/rpc/README.mdx) with [Stellar Client SDKs](../../../tools/sdks/client-sdks.mdx#overview) to perform all actions needed. ### Send a Payment Lets demonstrate a payment of an asset on Stellar. We will build a transaction with a payment operation to send 10 Lummens from a sender account to a receiver account, sign it as the sender account, and submit it to the network. - Submitting the transaction to the SDF-maintained public testnet instance of Stellar RPC server. - When submitting transactions to the RPC server it's possible that you will not receive a response from the server due to network conditions. - In such a situation it's impossible to determine the status of your transaction. - Highlights a recommendation to always save a transaction (or transaction encoded in XDR format) in a variable or a database and resubmit it if you don't know its status. - The transaction in serialized XDR format is idempotent meaning if the transaction has already been successfully applied to the ledger, RPC will simply return the saved result and not attempt to submit the transaction again. - Only in cases where a transaction's status is unknown (and thus will have a chance of being included into a ledger) will a resubmission to the network occur.
```js // send_payment.js // follow the https://github.com/stellar/js-stellar-sdk?tab=readme-ov-file#installation const rpcServer = new StellarSdk.rpc.Server( "https://soroban-testnet.stellar.org", ); async function sendPayments() { const { Keypair } = StellarSdk; const senderKeyPair = Keypair.random(); const recipientKeyPair = Keypair.random(); let senderAccount; try { // Request airdrop for the sender account - this creates, funds and returns the Account object senderAccount = await rpcServer.requestAirdrop(senderKeyPair.publicKey()); console.log("Sender Account funded with airdrop"); console.log("Sender Account ID:", senderAccount.accountId()); console.log("Sender Sequence number:", senderAccount.sequence.toString()); // Note - this persistence of sender id to a file on file system is only done for demo purposes. // It shares the created sender account with the payment monitor example which will run next. // Since this example is using file system it therefore is intended to run only on Node. // In a real application the stellar js sdk can be used in browser or Node. fs.writeFileSync("sender_public.key", senderAccount.accountId()); console.log("\n\n"); await rpcServer.requestAirdrop(recipientKeyPair.publicKey()); console.log("Recipient Account funded with airdrop"); console.log("Recipient Account ID:", recipientKeyPair.publicKey()); } catch (err) { console.error("Airdrop / Account loading failed:", err); return; } // Now call sendPayment with the funded account, in a loop of once every 30 seconds while (true) { console.log("\n\nSending payment..."); await sendPayment( senderKeyPair, senderAccount, recipientKeyPair.publicKey(), ); await new Promise((resolve) => setTimeout(resolve, 30000)); // wait for 30 seconds } } async function sendPayment(sender, senderAccount, recipient) { // The next step is to parametrize and build the transaction object: // Using the source account we just loaded we begin to assemble the transaction. // We set the fee to the base fee, which is 100 stroops (0.00001 XLM). // We also set the network passphrase to TESTNET. const transaction = new StellarSdk.TransactionBuilder(senderAccount, { fee: StellarSdk.BASE_FEE, networkPassphrase: StellarSdk.Networks.TESTNET, }) // We then add a payment operation to the transaction oject. // This operation will send 10 XLM to the destination account. // Obs.: Not specifying a explicit source account here means that the // operation will use the source account of the whole transaction, which we specified above. .addOperation( StellarSdk.Operation.payment({ destination: recipient, asset: StellarSdk.Asset.native(), amount: "10", }), ) // We include an optional memo which oftentimes is used to identify the transaction // when working with pooled accounts or to facilitate reconciliation. .addMemo(StellarSdk.Memo.id("1234567890")) // Finally, we set a timeout for the transaction. // This means that the transaction will not be valid anymore after 180 seconds. .setTimeout(180) .build(); // We sign the transaction with the source account's secret key. transaction.sign(sender); // Now we can send the transaction to the network. // The sendTransaction method immediately returns a reply with the transaction hash // and the status "PENDING". This means the transaction was received and is being processed. const sendTransactionResponse = await rpcServer.sendTransaction(transaction); // Here we check the status of the transaction as there are // a possible outcomes after sending a transaction that would have // to be handled accordingly, such as "DUPLICATE" or "TRY_AGAIN_LATER". if (sendTransactionResponse.status !== "PENDING") { throw new Error( `Failed to send transaction, status: ${sendTransactionResponse.status}`, ); } console.log( "Payment Transaction submitted, hash:", sendTransactionResponse.hash, ); // Here we poll the transaction status to await for its final result. // We can use the transaction hash to poll the transaction status later. const finalStatus = await rpcServer.pollTransaction( sendTransactionResponse.hash, ); // The pollTransaction method will return the final status of the transaction // after the specified number of attempts or when the transaction is finalized. // We then check the final status of the transaction and handle it accordingly. switch (finalStatus.status) { case StellarSdk.rpc.Api.GetTransactionStatus.FAILED: console.error("Transaction failed, status:", finalStatus.status); if (finalStatus.resultXdr) { console.error( "Transaction Result XDR (decoded):", JSON.stringify(finalStatus.resultXdr, null, 2), ); } throw new Error(`Transaction failed with status: ${finalStatus.status}`); case StellarSdk.rpc.Api.GetTransactionStatus.NOT_FOUND: throw new Error(`Transaction failed with status: ${finalStatus.status}`); case StellarSdk.rpc.Api.GetTransactionStatus.SUCCESS: console.log("Success! Committed on Ledger:", finalStatus.ledger); break; } } sendPayments().catch((error) => { console.error("Error executing sendPayments function:", error); process.exit(1); }); ``` ```java // SendPaymentExample.java // Java 17, Stellar Java SDK (lightsail-network/java-stellar-sdk) public class SendPaymentJavaExample { public static void main(String[] args) throws Exception { final String RPC_URL = "https://soroban-testnet.stellar.org"; SorobanServer rpc = new SorobanServer(RPC_URL); // Generate sender/recipient and fund via Friendbot (testnet) // get the friendbot url from rpc info var info = rpc.getNetwork(); System.out.println("Friendbot URL: " + info.getFriendbotUrl()); KeyPair sender = KeyPair.random(); KeyPair recipient = KeyPair.random(); friendbotFund(sender.getAccountId(), info.getFriendbotUrl()); friendbotFund(recipient.getAccountId(), info.getFriendbotUrl()); Files.writeString(Paths.get("sender_public.key"), sender.getAccountId(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); // Load sender sequence using RPC getLedgerEntries Account source = new Account(sender.getAccountId(), rpc.getAccount(sender.getAccountId()).getSequenceNumber()); // do this in a loop, send payment once every 30 seconds while (true) { // Build classic payment: 10 XLM, memo id, 180s timeout Transaction tx = new TransactionBuilder(source, Network.TESTNET) .setBaseFee(Transaction.MIN_BASE_FEE) .addOperation(PaymentOperation.builder().destination(recipient.getAccountId()).asset(new AssetTypeNative()) .amount(new BigDecimal("10")).build()) .addMemo(new MemoId(1234567890L)) .setTimeout(180) .build(); tx.sign(sender); // Submit via RPC var sendResp = rpc.sendTransaction(tx); if (!SendTransactionStatus.PENDING.equals(sendResp.getStatus())) { throw new RuntimeException("Failed to send transaction, status: " + sendResp.getStatus()); } System.out.println("Submitted. Hash: " + sendResp.getHash()); // Poll final status (throws on FAILED/NOT_FOUND) var finalResp = pollTransaction(rpc, sendResp.getHash()); System.out.println("Success! Committed on ledger: " + finalResp.getLedger()); Thread.sleep(30000L); // wait 30s } } // Fund account with Friendbot (testnet) static void friendbotFund(String accountId, String friendbotUrl) throws Exception { OkHttpClient http = new OkHttpClient.Builder().build(); String url = friendbotUrl + "?addr=" + URLEncoder.encode(accountId, StandardCharsets.UTF_8); Request req = new Request.Builder().url(url).build(); Response res = http.newCall(req).execute(); if (res.code() != 200) { throw new RuntimeException("Friendbot funding failed: " + res.code() + " " + res.body().string()); } System.out.println("Funded: " + accountId); } // Helper: wrap SorobanServer.pollTransaction and enforce failure handling static GetTransactionResponse pollTransaction(SorobanServer rpc, String txHash) throws Exception { GetTransactionResponse resp = rpc.pollTransaction(txHash); if (GetTransactionStatus.SUCCESS.equals(resp.getStatus())) { return resp; } if (GetTransactionStatus.FAILED.equals(resp.getStatus())) { if (resp.getResultXdr() != null) { System.err.println("Failed TX, Result XDR (base64): " + resp.getResultXdr()); } throw new RuntimeException("Transaction failed."); } if (GetTransactionStatus.NOT_FOUND.equals(resp.getStatus())) { throw new RuntimeException("Transaction not found."); } throw new RuntimeException("Unexpected status: " + resp.getStatus()); } } ``` ```go // sender.go package main "context" "crypto/tls" "fmt" "net/http" "net/url" "os" "strings" "time" "github.com/stellar/go-stellar-sdk/keypair" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/txnbuild" sdk "github.com/stellar/go-stellar-sdk/clients/rpcclient" protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" ) // friendbotFund funds an account using the Stellar testnet friendbot func friendbotFund(accountID, friendbotURL string) error { // Construct the URL with account parameter fullURL := friendbotURL + "?addr=" + url.QueryEscape(accountID) // Make HTTP GET request resp, err := http.Get(fullURL) if err != nil { return fmt.Errorf("failed to make friendbot request: %v", err) } defer resp.Body.Close() // Check if response is successful (2xx status code) if resp.StatusCode/100 != 2 { return fmt.Errorf("friendbot funding failed: %d", resp.StatusCode) } fmt.Printf("Funded: %s\n", accountID) return nil } // check panics if there's an error func check(err error) { if err != nil { panic(err) } } // SignAndSend builds, signs, and submits a transaction with the given operations func SignAndSend( ctx context.Context, client *sdk.Client, account txnbuild.Account, signers []*keypair.Full, operations ...txnbuild.Operation, ) protocol.GetTransactionResponse { // Build, sign, and submit the transaction tx, err := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: account, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Preconditions: txnbuild.Preconditions{ TimeBounds: txnbuild.NewInfiniteTimeout(), }, Operations: operations, }, ) check(err) for _, signer := range signers { tx, err = tx.Sign(network.TestNetworkPassphrase, signer) check(err) } txnB64, err := tx.Base64() check(err) txSendResp, err := client.SendTransaction(ctx, protocol.SendTransactionRequest{Transaction: txnB64}) check(err) for i := range 5 { txResp, err := client.GetTransaction(ctx, protocol.GetTransactionRequest{Hash: txSendResp.Hash}) check(err) switch txResp.Status { case "NOT_FOUND": case "SUCCESS": return txResp case "FAILED": panic(fmt.Errorf("transaction failed: %s", strings.Join(txResp.DiagnosticEventsXDR, "\n"))) } // Increase delay for each polling request time.Sleep(time.Duration(i) * time.Second) } panic(fmt.Errorf("transaction never found: %s", txSendResp.Hash)) } // sendPayment creates and submits a payment transaction using the helper function func sendPayment(ctx context.Context, rpcClient *sdk.Client, sourceAccount txnbuild.Account, signerKP *keypair.Full, destinationAddr string) error { // Create payment operation paymentOp := txnbuild.Payment{ Destination: destinationAddr, Amount: "10", Asset: txnbuild.NativeAsset{}, } // Use the helper function to build, sign, and submit fmt.Printf("Submitting payment of 10 XLM to %s...\n", destinationAddr) txResp := SignAndSend(ctx, rpcClient, sourceAccount, []*keypair.Full{signerKP}, &paymentOp) fmt.Printf("Success! Committed on ledger: %d\n", txResp.Ledger) return nil } func main() { const RPC_URL = "https://soroban-testnet.stellar.org" rpcClient := sdk.NewClient(RPC_URL, nil) // Get network info to obtain friendbot URL ctx := context.Background() networkInfo, err := rpcClient.GetNetwork(ctx) if err != nil { panic(fmt.Sprintf("Failed to get network info: %v", err)) } fmt.Printf("Friendbot URL: %s\n", networkInfo.FriendbotURL) // Generate sender and recipient keypairs sender, err := keypair.Random() if err != nil { panic(fmt.Sprintf("Failed to generate sender keypair: %v", err)) } recipient, err := keypair.Random() if err != nil { panic(fmt.Sprintf("Failed to generate recipient keypair: %v", err)) } // Fund accounts via friendbot err = friendbotFund(sender.Address(), networkInfo.FriendbotURL) if err != nil { panic(fmt.Sprintf("Failed to fund sender: %v", err)) } err = friendbotFund(recipient.Address(), networkInfo.FriendbotURL) if err != nil { panic(fmt.Sprintf("Failed to fund recipient: %v", err)) } // Save sender public key to file err = os.WriteFile("sender_public.key", []byte(sender.Address()), 0644) if err != nil { panic(fmt.Sprintf("Failed to write sender public key: %v", err)) } // Load sender account info senderAccount, err := rpcClient.LoadAccount(ctx, sender.Address()) if err != nil { panic(fmt.Sprintf("Failed to get sender account: %v", err)) } // Payment loop - send payment every 30 seconds for { err := sendPayment(ctx, rpcClient, senderAccount, sender, recipient.Address()) if err != nil { panic(fmt.Sprintf("Payment failed: %v", err)) } fmt.Println("Waiting 30 seconds before next payment...") time.Sleep(30 * time.Second) // Reload account to get updated sequence number senderAccount, err = rpcClient.LoadAccount(ctx, sender.Address()) if err != nil { panic(fmt.Sprintf("Failed to reload sender account: %v", err)) } } } ```
### Monitoring Payments as event stream Leverage the latest [Protocol 23 (Whisk)](https://stellar.org/blog/developers/introducing-whisk-stellar-protocol-23) to capture payment activity as pure events. These events can be efficiently monitored using the RPC server's `getEvents` method, which provides server-side filtering and real-time payment detection. The RPC approach offers several advantages: - **Server-side event topic filtering**: Filter for specific events using topic patterns - **Efficient polling**: Use cursors and pagination for reliable event processing - **Unified event model**: supports [CAP-67](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md), all asset movements on the network are emitted as events with standardized topic names to indicate the type of movement (e.g., "transfer" for payments) - **Better error handling**: RPC stateless HTTP polling is more resilient than persistent streaming protocols like SSE or WebSockets Demonstrate near real-time monitoring of transactions from the Stellar network by establishing an asynchronous listener to consume events from RPC and filter them by topic for just our example payments generated by our previous payments script.
```js // monitor_payment.js // follow the https://github.com/stellar/js-stellar-sdk?tab=readme-ov-file#installation async function monitorPayments() { // Initialize RPC server for testnet const rpcServer = new StellarSdk.rpc.Server( "https://soroban-testnet.stellar.org", ); // One-time initialization - everything encapsulated here const monitoredFromAccount = fs .readFileSync("sender_public.key", "utf-8") .trim(); // create our payment event topic filter values const transferTopicFilter = StellarSdk.xdr.ScVal.scvSymbol("transfer").toXDR("base64"); const fromTopicFilter = StellarSdk.nativeToScVal(monitoredFromAccount, { type: "address", }).toXDR("base64"); // Get starting ledger const latestLedger = await rpcServer.getLatestLedger(); console.log( `Starting payment monitoring from ledger ${latestLedger.sequence} and from account ${monitoredFromAccount}`, ); let currentStartLedger = latestLedger.sequence; let currentCursor; while (true) { // Query for payments from our monitored account const eventsResponse = await rpcServer.getEvents({ startLedger: currentStartLedger, cursor: currentCursor, filters: [ { type: "contract", topics: [ [ transferTopicFilter, fromTopicFilter, "**", // filter will match on any 'to' address and any token(SAC or SEP-41) ], ], }, ], limit: 10, }); // Process any events found console.log(`Found ${eventsResponse.events.length} payment(s):`); for (const event of eventsResponse.events) { console.log("\n--- Payment Received ---"); console.log(`Ledger: ${event.ledger}`); console.log(`Transaction Hash: ${event.txHash}`); console.log(`Closed At: ${event.ledgerClosedAt}`); // Decode addresses from topics try { const fromAddress = StellarSdk.scValToNative(event.topic[1]); const toAddress = StellarSdk.scValToNative(event.topic[2]); console.log(`Transfer: ${fromAddress} → ${toAddress}`); } catch (error) { console.log(`From/To: Unable to decode addresses`, error); continue; } // Decode transfer amount from the event data try { // Protocol 23+ unified events model per CAP-67 // https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md // Event value field is scMap with {amount: i128, to_muxed_id: u64|bytes|string} (when muxed info present) const amount = StellarSdk.scValToNative(event.value)["amount"]; console.log( `Amount: ${amount.toString()} stroops (${(Number(amount) / 10000000).toFixed(7)} XLM)`, ); } catch (error) { console.log(`Failed to decode transfer amount:`, error); } // Decode asset from topics[3], this is only present in events from SAC tokens if (event.topic.length > 3) { const asset = StellarSdk.scValToNative(event.topic[3]); console.log(`Asset: ${asset}`); } } // Update cursor to drive next query currentCursor = eventsResponse.cursor; currentStartLedger = null; // Wait 5 seconds before next iteration await new Promise((resolve) => setTimeout(resolve, 5000)); } } // Start the application monitorPayments().catch((error) => { console.error("Failed during payment monitoring:", error); }); ``` ```java // MonitorPaymentExample.java // Java 17, Stellar Java SDK (lightsail-network/java-stellar-sdk) public class MonitorPaymentsJavaExample { public static void main(String[] args) throws Exception { final String RPC_URL = "https://soroban-testnet.stellar.org"; SorobanServer rpc = new SorobanServer(RPC_URL); // Load the account we monitor payments FROM (written by send_payment example) String monitoredFromAccount = Files.readString(Paths.get("sender_public.key"), StandardCharsets.UTF_8).trim(); // Build topic filters using SDK helpers (base64 XDR via toXdrBase64()) String transferTopicFilter = Scv.toSymbol("transfer").toXdrBase64(); String fromTopicFilter = Scv.toAddress(monitoredFromAccount).toXdrBase64(); // Starting point: latest ledger GetLatestLedgerResponse latest = rpc.getLatestLedger(); Long startLedger = latest.getSequence().longValue(); String cursor = null; System.out.printf("Starting payment monitoring from ledger %d and from account %s%n", startLedger, monitoredFromAccount); while (true) { // filter will match on any 'to' address and any token(SAC or SEP-41) GetEventsRequest.EventFilter eventFilter = GetEventsRequest.EventFilter.builder() .type(EventFilterType.CONTRACT) .topic(Arrays.asList(transferTopicFilter, fromTopicFilter, "**")) .build(); GetEventsRequest.PaginationOptions paginationOptions = GetEventsRequest.PaginationOptions.builder() .limit(10L) .cursor(cursor) .build(); GetEventsRequest getEventsRequest = GetEventsRequest.builder() .startLedger(startLedger) .filter(eventFilter) .pagination(paginationOptions) .build(); var eventsResp = rpc.getEvents(getEventsRequest); var events = eventsResp.getEvents(); System.out.println("Found " + events.size() + " payment(s):"); for (var ev : events) { System.out.println("\n--- Payment Received ---"); System.out.println("Ledger: " + ev.getLedger()); System.out.println("Transaction Hash: " + ev.getTransactionHash()); System.out.println("Closed At: " + ev.getLedgerClosedAt()); List topics = ev.getTopic(); if (topics.size() < 3) { System.out.println("Invalid event, not enough topics."); continue; } try { String fromAddr = Scv.fromAddress(SCVal.fromXdrBase64(topics.get(1))).toString(); String toAddr = Scv.fromAddress(SCVal.fromXdrBase64(topics.get(2))).toString(); System.out.println("Transfer: " + fromAddr + " → " + toAddr); } catch (Exception e) { System.out.println("From/To: Unable to decode addresses: " + e.getMessage()); } try { // the event value is a map with key of Symbol for 'amount' to i128 value Map map = Scv.fromMap(SCVal.fromXdrBase64(ev.getValue())); SCVal amount = map.get(Scv.toSymbol("amount")); if (amount != null) { String amountStr = Scv.fromInt128(amount).toString(); BigDecimal raw = new BigDecimal(amountStr); System.out.printf("Token Amount Transferred: (%.7f)%n", raw.scaleByPowerOfTen(-7)); } } catch (Exception e) { System.out.println("Failed to decode transfer amount: " + e.getMessage()); } try { if (topics.size() > 3) { // Decode asset from topics[3], this is only present in events from SAC tokens System.out.println("Asset: " + new String(Scv.fromString(SCVal.fromXdrBase64(topics.get(3))))); } } catch (Exception ex) { System.out.println("Asset: Unable to decode asset" + ex.getMessage()); } } // pagination: update cursor and clear startLedger for subsequent calls cursor = eventsResp.getCursor(); startLedger = null; Thread.sleep(5000L); } } } ``` ```go // monitor.go package main "context" "crypto/tls" "encoding/base64" "fmt" "net/http" "os" "strings" "time" "github.com/stellar/go-stellar-sdk/strkey" "github.com/stellar/go-stellar-sdk/xdr" sdk "github.com/stellar/go-stellar-sdk/clients/rpcclient" protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" ) func main() { const RPC_URL = "https://soroban-testnet.stellar.org" rpcClient := sdk.NewClient(RPC_URL, nil) ctx := context.Background() // Load the account we monitor payments FROM (written by send_payment example) senderKeyBytes, err := os.ReadFile("sender_public.key") if err != nil { panic(fmt.Sprintf("Failed to read sender_public.key: %v", err)) } monitoredFromAccount := strings.TrimSpace(string(senderKeyBytes)) fmt.Printf("Starting payment monitoring from account: %s\n", monitoredFromAccount) // Get latest ledger as starting point latestLedger, err := rpcClient.GetLatestLedger(ctx) if err != nil { panic(fmt.Sprintf("Failed to get latest ledger: %v", err)) } startLedger := latestLedger.Sequence var cursor string fmt.Printf("Starting payment monitoring from ledger %d\n", startLedger) for { err := monitorPayments(ctx, rpcClient, monitoredFromAccount, &startLedger, &cursor) if err != nil { fmt.Printf("Error monitoring payments: %v\n", err) } time.Sleep(5 * time.Second) } } func monitorPayments(ctx context.Context, client *sdk.Client, fromAccount string, startLedger *uint32, cursor *string) error { // Create a simple event filter for contract events eventFilter := protocol.EventFilter{ EventType: protocol.EventTypeSet{protocol.EventTypeContract: nil}, // We'll filter by topics in the processing logic instead } // Create request request := protocol.GetEventsRequest{ Filters: []protocol.EventFilter{eventFilter}, } // Set startLedger only on first call if *startLedger != 0 { request.StartLedger = *startLedger } // Set cursor for pagination if *cursor != "" { parsedCursor, err := protocol.ParseCursor(*cursor) if err != nil { return fmt.Errorf("failed to parse cursor: %v", err) } request.Pagination = &protocol.PaginationOptions{ Cursor: &parsedCursor, Limit: 10, } } else { request.Pagination = &protocol.PaginationOptions{ Limit: 10, } } // Get events eventsResp, err := client.GetEvents(ctx, request) if err != nil { return fmt.Errorf("failed to get events: %v", err) } // Filter events that match our criteria relevantEvents := []protocol.EventInfo{} for _, event := range eventsResp.Events { if isPaymentEvent(event, fromAccount) { relevantEvents = append(relevantEvents, event) } } fmt.Printf("Found %d payment(s):\n", len(relevantEvents)) // Process relevant events for _, event := range relevantEvents { err := processPaymentEvent(event) if err != nil { fmt.Printf("Error processing event: %v\n", err) continue } } // Update pagination *cursor = eventsResp.Cursor *startLedger = 0 // Clear start ledger for subsequent calls return nil } func isPaymentEvent(event protocol.EventInfo, fromAccount string) bool { // Check if this is a transfer event by looking at the first topic if len(event.TopicXDR) < 2 { return false } // Try to decode the first topic to see if it's "transfer" if isTransferTopic(event.TopicXDR[0]) && isFromAccount(event.TopicXDR[1], fromAccount) { return true } return false } func isTransferTopic(topicXDR string) bool { xdrBytes, err := base64.StdEncoding.DecodeString(topicXDR) if err != nil { return false } var scVal xdr.ScVal err = scVal.UnmarshalBinary(xdrBytes) if err != nil { return false } return scVal.Type == xdr.ScValTypeScvSymbol && scVal.Sym != nil && string(*scVal.Sym) == "transfer" } func isFromAccount(topicXDR, expectedAccount string) bool { decodedAddr, err := decodeAddressFromXDR(topicXDR) return err == nil && decodedAddr == expectedAccount } func createTransferTopicFilter() (string, error) { // Create "transfer" symbol and encode to base64 XDR symbol := xdr.ScSymbol("transfer") scVal := xdr.ScVal{ Type: xdr.ScValTypeScvSymbol, Sym: &symbol, } // Use XDR marshaling to base64 xdrBytes, err := scVal.MarshalBinary() if err != nil { return "", err } return base64.StdEncoding.EncodeToString(xdrBytes), nil } func createAddressTopicFilter(accountID string) (string, error) { // Convert address string to AccountId accountKey, err := xdr.AddressToAccountId(accountID) if err != nil { return "", err } // Create SCAddress for account scAddr := xdr.ScAddress{ Type: xdr.ScAddressTypeScAddressTypeAccount, AccountId: &accountKey, } scVal := xdr.ScVal{ Type: xdr.ScValTypeScvAddress, Address: &scAddr, } // Use XDR marshaling to base64 xdrBytes, err := scVal.MarshalBinary() if err != nil { return "", err } return base64.StdEncoding.EncodeToString(xdrBytes), nil } func processPaymentEvent(event protocol.EventInfo) error { fmt.Println("\n--- Payment Received ---") fmt.Printf("Ledger: %d\n", event.Ledger) fmt.Printf("Transaction Hash: %s\n", event.TransactionHash) fmt.Printf("Closed At: %s\n", event.LedgerClosedAt) // Parse topics if len(event.TopicXDR) < 3 { fmt.Println("Invalid event, not enough topics.") return nil } // Decode from and to addresses fromAddr, err := decodeAddressFromXDR(event.TopicXDR[1]) if err != nil { fmt.Printf("From/To: Unable to decode from address: %v\n", err) } else { toAddr, err := decodeAddressFromXDR(event.TopicXDR[2]) if err != nil { fmt.Printf("From/To: Unable to decode to address: %v\n", err) } else { fmt.Printf("Transfer: %s → %s\n", fromAddr, toAddr) } } // Decode amount from event value if event.ValueXDR != "" { amount, err := decodeAmountFromXDR(event.ValueXDR) if err != nil { fmt.Printf("Failed to decode transfer amount: %v\n", err) } else { fmt.Printf("Token Amount Transferred: %.7f\n", float64(amount)/10000000) // Scale by 10^7 } } // Decode asset if available if len(event.TopicXDR) > 3 { asset, err := decodeAssetFromXDR(event.TopicXDR[3]) if err != nil { fmt.Printf("Asset: Unable to decode asset: %v\n", err) } else { fmt.Printf("Asset: %s\n", asset) } } return nil } func decodeAddressFromXDR(base64XDR string) (string, error) { xdrBytes, err := base64.StdEncoding.DecodeString(base64XDR) if err != nil { return "", err } var scVal xdr.ScVal err = scVal.UnmarshalBinary(xdrBytes) if err != nil { return "", err } if scVal.Type != xdr.ScValTypeScvAddress || scVal.Address == nil { return "", fmt.Errorf("not an address type") } switch scVal.Address.Type { case xdr.ScAddressTypeScAddressTypeAccount: if scVal.Address.AccountId == nil { return "", fmt.Errorf("account ID is nil") } return scVal.Address.AccountId.Address(), nil case xdr.ScAddressTypeScAddressTypeContract: if scVal.Address.ContractId == nil { return "", fmt.Errorf("contract ID is nil") } return strkey.MustEncode(strkey.VersionByteContract, scVal.Address.ContractId[:]), nil default: return "", fmt.Errorf("unknown address type") } } func decodeAmountFromXDR(base64XDR string) (int64, error) { xdrBytes, err := base64.StdEncoding.DecodeString(base64XDR) if err != nil { return 0, err } var scVal xdr.ScVal err = scVal.UnmarshalBinary(xdrBytes) if err != nil { return 0, err } // The value should be a map containing an "amount" key if scVal.Type != xdr.ScValTypeScvMap || scVal.Map == nil { return 0, fmt.Errorf("value is not a map") } // Look for the "amount" key in the map for _, pair := range **scVal.Map { if pair.Key.Type == xdr.ScValTypeScvSymbol && pair.Key.Sym != nil && string(*pair.Key.Sym) == "amount" { // Found the amount key, extract the value if pair.Val.Type == xdr.ScValTypeScvI128 && pair.Val.I128 != nil { // Convert I128 to int64 (simplified - just use the low part for now) return int64(pair.Val.I128.Lo), nil } } } return 0, fmt.Errorf("amount not found in map") } func decodeAssetFromXDR(base64XDR string) (string, error) { xdrBytes, err := base64.StdEncoding.DecodeString(base64XDR) if err != nil { return "", err } var scVal xdr.ScVal err = scVal.UnmarshalBinary(xdrBytes) if err != nil { return "", err } if scVal.Type == xdr.ScValTypeScvString && scVal.Str != nil { return string(*scVal.Str), nil } return "", fmt.Errorf("not a string type") } ```
### Running Your Payment Pipeline with RPC 1. **Run the payment submission script** in one terminal. Leave it running, it will submit a payment once every 30 seconds.
```bash # js node send_payment.js # java # JRE 17 or higher should be installed - https://adoptium.net/temurin/releases/?version=17&package=jdk # download Stellar Java SDK jar, latest release on maven central JAR_REPO='https://repo1.maven.org/maven2/network/lightsail/stellar-sdk' JAR_VERSION="2.0.0" # replace with latest version if needed JAR_FILE="stellar-sdk-$JAR_VERSION-uber.jar" curl -fsSL -o "$JAR_FILE" "$JAR_REPO/$JAR_VERSION/$JAR_FILE" javac -cp "$JAR_FILE" SendPaymentExample.java java -cp ".:$JAR_FILE" SendPaymentExample # go mkdir -p payments_example/sender cd payments_example go mod init payments_example go get github.com/stellar/go-stellar-sdk@latest # save sender.go code to sender/sender.go file go build -o sender sender/sender.go ./sender ```
2. **Run the payment monitor script** in a separate terminal.
```bash # js node monitor_payments.js # java javac -cp "$JAR_FILE" MonitorPaymentExample.java java -cp ".:$JAR_FILE" MonitorPaymentExample # go mkdir -p payments_example/monitor # save monitor.go code to monitor/monitor.go file go build -o monitor monitor/monitor.go ./monitor ```
3. **Observe the payment events** as they're detected and displayed on console. ## Summary - Payments from transaction operations or contract invocations on Stellar network can be monitored by leveraging the unified events model as of [Protocol 23 (Whisk)](https://stellar.org/blog/developers/introducing-whisk-stellar-protocol-23) and the RPC `getEvents` method. - **Payments are 'transfer' events**: the [unified events model](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md) ensures whether a payment happened through an operation or contract invocation it will result in the same 'transfer' event being emitted. - **Event Type**: `"contract"` denotes payments are an application level event rather than 'system' event. - **Transfer Topics Model**: an array of four topics, specified in [CAP-67](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md) and summarized: - `topic[0]` = Event name 'transfer' determines the next 3 topics - `topic[1]` = Transfer Sender address - `topic[2]` = Transfer Recipient address - `topic[3]` = Asset identifier (only present for Stellar Assets, through their built-in contracts (SAC)) - RPC provides precise filtering of Stellar network events and this includes event topics model defined for unified events in [CAP-67](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md). You save time by using the RPC server-side filter capabilities to focus on processing only events relative to your application such as payments from a specific account in this example. - **Wildcards**: The RPC `getEvents` allows to use "\*" to match any value in a topic position - **Server-side Filtering**: RPC `getEvents` applies the filters and only matching events are returned, reducing bandwidth - **Cursor-based Pagination**: RPC `getEvents` retrieves event data efficiently with a paging mechanism --- ## Signing Soroban contract invocations When invoking Soroban smart contracts that require authorization, there are two distinct signing approaches. Understanding when to use each method is essential for building powerful applications that interact with smart contracts. ## Overview | Method | Who can Sign? | Best For | | --- | --- | --- | | Transaction signing | G-accounts | G-accounts who fully trust and know how to thoroughly verify the authenticity and security of the transactions. 📌 **Note:** This is the only method supported by Stellar Classic. | | Auth-entry signing | G-accounts and C-accounts | Smart wallets, multi-party auth, stricter authorization control, relayed and/or sponsored transactions | In both methods, the sequence number is "spent" by the transaction source account. ## Method 1: Transaction signing Full transaction signing is the simpler approach where the same account acts as both the transaction source (paying fees and consuming sequence) and the authorizer of the contract invocation. This method works **only with G-accounts** (Stellar accounts starting with `G`). ### When to use - The calling account owns the keys and can sign the full transaction - The caller is willing to pay transaction fees, which can only be paid in XLM and by a G-account - You want the simplest integration path - Single-party authorization is sufficient ### How it works 1. **Client** builds a transaction with an `invokeHostFunction` operation 2. **Client** simulates the transaction to get resource requirements and footprint 3. **Client** signs the entire transaction envelope with the source account's keypair 4. **Client** submits the signed transaction to the network When the transaction source account is the same as the address being authorized, the signature on the transaction itself implicitly authorizes the invocation—no separate auth entry signature is needed. This is called "source account authorization" and uses the `sorobanCredentialsSourceAccount` credential type. ### Code example ```typescript BASE_FEE, Keypair, nativeToScVal, Networks, Operation, TransactionBuilder, } from "@stellar/stellar-sdk"; const rpcUrl = "https://soroban-testnet.stellar.org"; const server = new Server(rpcUrl); const sourceKeypair = Keypair.fromSecret("S..."); async function invokeWithFullSigning( contractId: string, recipientAddress: string, amount: bigint, ): Promise { const sourceAccount = await server.getAccount(sourceKeypair.publicKey()); const transaction = new TransactionBuilder(sourceAccount, { fee: BASE_FEE, networkPassphrase: Networks.TESTNET, }) .addOperation( Operation.invokeContractFunction({ contract: contractId, function: "transfer", args: [ nativeToScVal(sourceKeypair.publicKey(), { type: "address" }), nativeToScVal(recipientAddress, { type: "address" }), nativeToScVal(amount, { type: "i128" }), ], }), ) .setTimeout(30) .build(); // prepareTransaction simulates and assembles in one step const preparedTx = await server.prepareTransaction(transaction); // Sign the transaction envelope preparedTx.sign(sourceKeypair); // Submit to network const response = await server.sendTransaction(preparedTx); return response; } ``` :::note[Source account authorization] For transaction signing, `prepareTransaction` handles simulation internally. Since the source account's signature on the transaction envelope implicitly authorizes the invocation, no separate auth entry signing is needed—and therefore no second simulation is required. ::: ### Key characteristics - **Sequence number**: Consumed from the source account - **Fees**: Paid by the source account - **Authorization**: Implicit via transaction signature (for source account credentials) - **Limitation**: Cannot be used with C-accounts (contract accounts) ## Method 2: Auth-entry signing Auth-entry signing decouples authorization from transaction submission. The authorizer signs only the specific contract invocation (an "auth entry"), while a **separate account** acts as the transaction source, paying fees and consuming its own sequence number. This method works for either G-account or C-account clients. ### When to use - The end-user has a C-account (commonly a smart wallet) — Method 2 is the **only** option for C-accounts, as they cannot sign transaction envelopes. G-accounts can also use this method. - The end-user doesn't (need to) have XLM to pay for fees - Applications want a fine-grained control over which parts of the contract invocation (and subinvocations) are authorized by each account. - Building smart contract protocols where the end-user doesn't submit the transaction - Building payment protocols where the transaction source account will be defined at a later point in time ### How it works 1. **Client** builds the transaction using `AssembledTransaction` 2. **Client** simulates (Recording Mode) to get the authorization tree 3. **Client** signs the auth entries using `signAuthEntries` 4. **Client** optionally re-simulates (Enforcing Mode) to validate their signatures 5. **Client** sends the transaction XDR to the fee-payer 6. **Fee-payer** parses the XDR and rebuilds with its own G-account as source 7. **Fee-payer** performs security verifications 🚨 to ensure the incoming transaction does not contain any malicious code. 8. **Fee-payer** simulates (Enforcing Mode) to catch errors before paying fees 9. **Fee-payer** signs the transaction envelope and submits ### Simulation modes: Recording vs Enforcing Transaction simulation has two modes that are critical to understand: | Mode | When used | What it does | | --- | --- | --- | | **Recording Mode** | First simulation, before signing | Returns the auth entries that need signatures. Skips `require_auth` validation. | | **Enforcing Mode** | Second simulation, after signing | Validates signatures and executes `__check_auth`. Returns accurate resource estimates. | :::warning[Enforcing Mode simulation is required for submission] The first simulation (Recording Mode) **does not execute** the `require_auth` checks — it only records which auth entries are needed. This means the resource estimates from the first simulation are **incomplete**. The **fee-payer** must simulate in Enforcing Mode, and the **client** is strongly recommended to simulate as well to ensure fees and auth checks are correct when the contract enforces signatures: | Who | Why | | --- | --- | | **Client** | Validates signatures before sending to fee-payer and ensures auth enforcement succeeds before it leaves the client. | | **Fee-payer** | Verifies the transaction will succeed before submitting and ensures auth enforcement will pass before paying fees. | Running Enforcing Mode simulation provides two critical benefits: - **Validates signatures and execution** — Catches auth errors and contract failures before submission. Failed simulations cost nothing; failed submissions cost real fees. - **Returns accurate resource estimates** — Recording Mode underestimates fees because it skips auth validation. See [Transaction Simulation - Authorization](../../../learn/fundamentals/contract-development/contract-interactions/transaction-simulation.mdx#authorization) for more details. ::: ### Auth entry structure An auth entry signature authorizes a specific invocation tree and includes: - **Address**: The account authorizing the invocation - **Signature expiration ledger**: When the signature becomes invalid (ledger-based, not timestamp) - **Nonce**: A unique value for replay protection - **Signature**: Signs the SHA-256 hash of the `ENVELOPE_TYPE_SOROBAN_AUTHORIZATION` preimage :::note[Signature expiration] Auth entry signatures expire based on ledger numbers, not timestamps. A typical offset is between 12 and 60 ledgers (approximately 1-5 minutes). The signature is valid until and including the `signatureExpirationLedger`, but invalid at `signatureExpirationLedger + 1`. Keep expiration windows as small as viable – shorter windows are safer and result in lower transaction costs. ::: ### Code example: Using `AssembledTransaction` This example shows a token transfer where the sender (client) uses `AssembledTransaction` to build and sign auth entries, then sends the transaction XDR to a fee-payer for submission. #### Step 1: Client builds and signs auth entries ```typescript AssembledTransaction, basicNodeSigner, } from "@stellar/stellar-sdk/contract"; const rpcUrl = "https://soroban-testnet.stellar.org"; const networkPassphrase = Networks.TESTNET; // Client's keypair (authorizes the transfer) const senderKeypair = Keypair.fromSecret("S..."); async function buildSignedAuthEntries( tokenContractId: string, recipientAddress: string, amount: bigint, ): Promise { // Build transaction using AssembledTransaction const tx = await AssembledTransaction.build({ contractId: tokenContractId, method: "transfer", args: [ nativeToScVal(senderKeypair.publicKey(), { type: "address" }), nativeToScVal(recipientAddress, { type: "address" }), nativeToScVal(amount, { type: "i128" }), ], networkPassphrase, rpcUrl, parseResultXdr: (result) => result, }); // Check simulation result (Recording Mode) if (Api.isSimulationError(tx.simulation)) { throw new Error(`Simulation failed: ${tx.simulation.error}`); } // Check who needs to sign const missingSigners = tx.needsNonInvokerSigningBy(); if (!missingSigners.includes(senderKeypair.publicKey())) { throw new Error("Sender not in required signers"); } // Sign auth entries using basicNodeSigner const signer = basicNodeSigner(senderKeypair, networkPassphrase); await tx.signAuthEntries({ address: senderKeypair.publicKey(), signAuthEntry: signer.signAuthEntry, expiration: tx.simulation.latestLedger + 60, // ~5 minutes }); // Re-simulate to validate signatures (📌 Enforcing Mode) await tx.simulate(); if (Api.isSimulationError(tx.simulation)) { throw new Error(`Signature validation failed: ${tx.simulation.error}`); } // Verify all signatures collected if (tx.needsNonInvokerSigningBy().length > 0) { throw new Error("Missing signatures"); } // Return transaction XDR for fee-payer return tx.built!.toXDR(); } ``` #### Step 2: Fee-payer rebuilds and submits ```typescript Keypair, Networks, Operation, Transaction, TransactionBuilder, xdr, } from "@stellar/stellar-sdk"; const rpcUrl = "https://soroban-testnet.stellar.org"; const networkPassphrase = Networks.TESTNET; const server = new Server(rpcUrl); const feePayerKeypair = Keypair.fromSecret("S..."); async function submitWithSignedAuth( transactionXdr: string, ): Promise { // Parse client's transaction to extract operation and Soroban data const clientTx = new Transaction(transactionXdr, networkPassphrase); const txEnvelope = xdr.TransactionEnvelope.fromXDR(transactionXdr, "base64"); const sorobanData = txEnvelope.v1()?.tx()?.ext()?.sorobanData(); if (!sorobanData) { throw new Error("Missing Soroban data"); } const invokeOp = clientTx.operations[0] as Operation.InvokeHostFunction; // 🚨 SECURITY: Verify the transaction/operation source is not the fee-payer's account, and that the auth entries do not reference the fee-payer's account // Rebuild transaction with fee-payer as source const feePayerAccount = await server.getAccount(feePayerKeypair.publicKey()); const rebuiltTx = new TransactionBuilder(feePayerAccount, { fee: clientTx.fee, networkPassphrase, sorobanData, }) .setTimeout(30) .addOperation( Operation.invokeHostFunction({ func: invokeOp.func, auth: invokeOp.auth || [], source: invokeOp.source, }), ) .build(); // 📌 Simulate before submitting to catch errors without paying fees (Enforcing Mode) const simResult = await server.simulateTransaction(rebuiltTx); if (Api.isSimulationError(simResult)) { throw new Error(`Fee-payer simulation failed: ${simResult.error}`); } const assembledTx = assembleTransaction(rebuiltTx, simResult).build(); // Fee-payer signs and submits assembledTx.sign(feePayerKeypair); return await server.sendTransaction(assembledTx); } ``` ### Key characteristics - **Sequence number**: Consumed from the fee-payer account - **Fees**: Paid by the fee-payer account (in XLM) - **Client authorization**: Explicit via signed auth entries (separate from transaction signature) - **Flexibility**: Works with both G-accounts and C-accounts (contract accounts) - **Use case**: Sponsored transactions, relayed transactions, smart wallets, multi-party authorization, fine-grained authorization control ### C-account (smart wallet) authorization C-accounts (contract accounts starting with `C`) cannot sign transaction envelopes—they can **only** authorize via auth entries. This is because C-accounts don't have traditional keypairs; their authorization logic is defined by the contract code itself (e.g., a smart wallet contract that verifies passkey signatures). For C-accounts: - The auth entry signature is produced by the contract's authentication mechanism (e.g., passkeys, multisig logic) - A separate G-account must always act as the transaction source to pay fees - Wallet interfaces and signing utilities provide `signAuthEntry` APIs for this purpose #### Signing auth entries with different methods **Using `basicNodeSigner` for G-accounts (Node.js/backend)** The `basicNodeSigner` utility creates signing functions from a keypair, providing both `signAuthEntry` and `signTransaction` methods. This is useful for backend services or testing. ```typescript const keypair = Keypair.fromSecret("S..."); const networkPassphrase = "Test SDF Network ; September 2015"; // Create a signer that provides both auth entry and transaction signing const signer = basicNodeSigner(keypair, networkPassphrase); // Sign an auth entry const signedAuthEntry = await signer.signAuthEntry(authEntry); // Sign a transaction envelope const signedTransaction = await signer.signTransaction(transactionXDR); ``` **Using Freighter for C-accounts (browser/wallet)** Wallet interfaces like [Freighter](../freighter/sign-auth-entries.mdx) provide `signAuthEntry` for C-accounts (smart wallets) where the signing logic is defined by the contract. ```typescript // Freighter's signAuthEntry returns the signed auth entry const signedAuthEntry = await freighterApi.signAuthEntry(preimageXdr); ``` :::caution[C-account limitations] C-accounts: - Cannot be the transaction source account - Cannot sign transaction envelopes - Must rely on a G-account to pay fees and submit transactions - Require their auth entries to be signed according to their contract logic ::: ## Comparison summary | Aspect | Transaction signing | Auth-entry signing | | --- | --- | --- | | **Transaction source** | Client (G-account) | Fee-payer (G-account) | | **Sequence consumed from** | Client | Fee-payer | | **Who signs auth entries?** | N/A (implicit via tx signature) | Client (G or C-account) | | **Who signs tx envelope?** | Client | Fee-payer | | **Account types supported** | G-accounts only | G and C-accounts | ### Visual: Transaction signing ``` ┌───────────────────────────────────────────────────┐ │ Transaction Envelope │ │ ┌─────────────────────────────────────────────┐ │ │ │ Source: Client G-account │ │ │ │ Fees: Paid by client │ │ │ │ ┌───────────────────────────────────────┐ │ │ │ │ │ invokeHostFunction Operation │ │ │ │ │ │ Auth: Source account credential │ │ │ │ │ └───────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────┘ │ │ Envelope signature: Client 🔑 │ └───────────────────────────────────────────────────┘ ``` ### Visual: Auth-entry signing ``` ┌───────────────────────────────────────────────────┐ │ Transaction Envelope │ │ ┌─────────────────────────────────────────────┐ │ │ │ Source: Fee-payer G-account │ │ │ │ Fees: Paid by fee-payer │ │ │ │ ┌───────────────────────────────────────┐ │ │ │ │ │ invokeHostFunction Operation │ │ │ │ │ │ Auth: Signed auth entries ◄─────────┼──┼─── Client signs 🔑 │ │ │ (G or C-account) │ │ │ │ │ └───────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────┘ │ │ Envelope signature: Fee-payer 🔑 │ └───────────────────────────────────────────────────┘ ``` ## Real-world use case: Sponsored transactions A common pattern in payment protocols is for a fee-payer to submit transactions on behalf of users: 1. **Client** builds a transaction and simulates it (Recording Mode) 2. **Client** signs the auth entries (authorizing the payment) 3. **Client** optionally re-simulates (Enforcing Mode) to validate signatures 4. **Client** sends the signed auth entries to the fee-payer service 5. **Fee-payer** rebuilds the transaction with its own G-account as source 6. **Fee-payer** re-simulates (Enforcing Mode) to get accurate resource estimates and validate signatures 7. **Fee-payer** signs the transaction envelope and submits This allows the fee-payer to sponsor fees while the client retains exclusive control over authorizing their funds. ## Note on Fee-bump transactions Regardless of the method, the user can still use fee bump transactions as an additional layer in order to separate the account spending their sequence number from the account paying the fees. More info on fee bump transactions can be found in the [Fee-bump transactions](./fee-bump-transactions.mdx) guide. ## Common pitfalls and gotchas - **Using Horizon URLs**: Soroban signing requires Soroban RPC, not Horizon. Refer to [Soroban RPC Providers](../../../data/apis/rpc/providers.mdx) for the correct URL to use. - **Forgetting to assemble**: Fee-payer flows must `assembleTransaction` after simulation to apply footprint + resource fees. - **Missing auth on rebuilt ops**: When rebuilding, include `sorobanData` in the `invokeHostFunction` operation. - **Wrong signer**: C-accounts cannot sign envelopes, only auth entries. - **Stale auth expiration**: Keep `signatureExpirationLedger` short and aligned with expected submission time. ## Further reading - [Transaction simulation](../../../learn/fundamentals/contract-development/contract-interactions/transaction-simulation.mdx) — Details on simulation and authorization modes - [Multi-party auth example](../../../learn/fundamentals/contract-development/contract-interactions/transaction-simulation.mdx#example-2-multi-party-authentication) — Walkthrough of multi-party signing in simulation using full transaction signing - [Contract authorization](../auth/contract-authorization.mdx) — How `require_auth` works in contracts - [Sign authorization entries with Freighter](../freighter/sign-auth-entries.mdx) — Wallet integration for auth entry signing - [Send to and receive from C-accounts](./send-and-receive-c-accounts.mdx) — Working with contract accounts - [Fee-bump transactions](./fee-bump-transactions.mdx) — Using fee bump transactions to pay for transaction fees on behalf of another account without re-signing the transaction - [Stellar auth example (community)](https://github.com/fazzatti/stellar-auth-example) — End-to-end auth-entry signing walkthrough - [Stellar Asset Contract (SAC)](../tokens/stellar-asset-contract.mdx) — Background on the SAC `transfer`, used in the code examples above --- ## simulateTransaction RPC method guide ## Overview The `simulateTransaction` endpoint in Stellar RPC allows you to submit a trial contract invocation to simulate how it would be executed by the Stellar network. This simulation calculates the effective transaction data, required authorizations, and minimal resource fee. It provides a way to test and analyze the potential outcomes of a transaction without actually submitting it to the network. It can be a nice way to get contract data as well sometimes. While calling the method on the rpc server is not the ONLY way to simulate a transaction, it will likely be the most common and easiest way. Here we will look at the objects involved and their definitions. ## RPC Services Stellar Development Foundation provides a testnet RPC service at `http://soroban-testnet.stellar.org`. For public network providers please refer to the [Ecosystem RPC Providers](../../../data/apis/rpc/providers.mdx) list. ### Testnet Endpoint: https://soroban-testnet.stellar.org:443 **SimulateTransactionParams** is the argument passed to the `simulateTransaction` RPC endpoint: ```typescript interface SimulateTransactionParams { transaction: string; // The Stellar transaction to be simulated, serialized as a base64 string. resourceConfig?: { instructionLeeway: number; // Allow this many extra instructions when budgeting resources. }; } ``` **SimulateTransactionResult** is the return result from the call. It includes [many useful things](../../../data/apis/rpc/api-reference/methods/simulateTransaction.mdx)! ## Things `simulateTransaction` is used for: 1. **Preparing `invokeHostFunctionOp` Transactions**: Anytime you need to submit an `invokeHostFunctionOp` transaction to the network. 2. **Footprint Determination**: To determine the ledger footprint, which includes all the data entries the transaction will read or write. 3. **Authorization Identification**: To identify the authorizations required for the transaction. 4. **Error Detection**: To detect potential errors and issues before actual submission, saving time and network resources. 5. **Restoring Archived Ledger Entries or Contract Code**: To prepare and restore archived data before actual transaction submission. 6. **Simulating Contract Getter Calls**: To retrieve certain data from the contract without affecting the ledger state. (It's worth noting you could also retrieve certain contract data direct from the ledgerkeys without simulation if it doesn't require any manipulation within the contract logic.) 7. **Resource Calculation**: To calculate the necessary resources (CPU instructions, memory, etc.) that a transaction will consume. ## How to Call `simulateTransaction` ### Using Fetch Here's an example of how to call the `simulateTransaction` endpoint directly using `fetch` in JavaScript: ```javascript async function simulateTransaction(transactionXDR) { const requestBody = { jsonrpc: "2.0", id: 8675309, method: "simulateTransaction", params: { transaction: transactionXDR, resourceConfig: { instructionLeeway: 50000, }, }, }; const response = await fetch("https://soroban-testnet.stellar.org:443", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(requestBody), }); const result = await response.json(); console.log(JSON.parse(result)); } // Example XDR transaction envelope // Replace the following placeholder with your actual XDR transaction envelope const transactionXDR = "AAAAAgAAAAAg4dbAxsGAGICfBG3iT2cKGYQ6hK4sJWzZ6or1C5v6GAAAAGQAJsOiAAAAEQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABzAP+dP0PsNzYvFF1pv7a8RQXwH5eg3uZBbbWjE9PwAsAAAAJaW5jcmVtZW50AAAAAAAAAgAAABIAAAAAAAAAACDh1sDGwYAYgJ8EbeJPZwoZhDqEriwlbNnqivULm/oYAAAAAwAAAAMAAAAAAAAAAAAAAAA="; // An example of where to get the XDR is from TransactionBuilder class from the sdk as shown in the next example. simulateTransaction(transactionXDR); ``` ### Using the JavaScript SDK The Stellar SDK provides a convenient method to simulate a transaction: ```javascript Keypair, rpc as StellarRpc, scValToNative, TransactionBuilder, BASE_FEE, Networks, Operation, } from "@stellar/stellar-sdk"; const FRIENDBOT_URL = "https://friendbot-testnet.stellar.org/"; const rpc_url = "https://soroban-testnet.stellar.org:443"; // Generate a new keypair for transaction authorization. const keypair = Keypair.random(); const secret = keypair.secret(); const publicKey = keypair.publicKey(); console.log("publicKey:", publicKey); // you need to fund the account. await fetch(`https://friendbot-testnet.stellar.org/?addr=${publicKey}`).then( (res) => { console.log(`funded account: ${publicKey}`); }, ); // Initialize the rpcServer const RpcServer = new StellarRpc.Server(rpc_url, { allowHttp: true }); // Load the account (getting the sequence number for the account and making an account object.) const account = await RpcServer.getAccount(publicKey); // Define the transaction const transaction = new TransactionBuilder(account, { fee: BASE_FEE, }) .setNetworkPassphrase(Networks.TESTNET) .setTimeout(30) .addOperation( Operation.invokeContractFunction({ function: "symbol", // the contract function and address need to be set by you. contract: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", args: [], }), ) .build(); // If you want to get this as an XDR string directly, you would use `transaction.toXDR('base64')` RpcServer.simulateTransaction(transaction).then((sim) => { console.log("cost:", sim.cost); console.log("result:", sim.result); // the result is a ScVal and so we can parse that to human readable output using the sdk's `scValToNative` function: console.log("humanReadable Result:", scValToNative(sim.result?.retval)); console.log("error:", sim.error); console.log("latestLedger:", sim.latestLedger); }); ``` #### Running the example To run the above code, you will need to install the Stellar SDK into your project. You can do this by running the following command in your project directory: `npm install @stellar/stellar-sdk` Once your project is set up, you can create a new mjs file and paste the code above. You can then run the file using Node.js by running: `node .mjs` ## Understanding the Footprint A footprint is a set of ledger keys that the transaction will read or write. These keys are marked as either read-only or read-write: - **Read-Only Keys**: Available for reading only. - **Read-Write Keys**: Available for reading and writing. The footprint ensures that a transaction is aware of all the ledger entries it will interact with, preventing unexpected errors during execution. ## Assembling a Transaction Once you have simulated the transaction and obtained the necessary data, you can assemble the transaction for actual submission. The `assembleTransaction` function in the SDK helps with this process, but you can also call `prepareTransaction` to have it both simulate and assemble the transaction for you in one step. Using the JavaScript SDK, we can call [`assembleTransaction`](https://stellar.github.io/js-stellar-sdk/module-rpc.html#.assembleTransaction) to easily assemble a transaction. ## Handling Archived Ledger Entries When a ledger entry is archived, it needs to be restored before the transaction can be submitted. This is indicated in the `restorePreamble` field of the result. ```typescript interface RestorePreamble { minResourceFee: string; // Absolute minimum resource fee to add when submitting the RestoreFootprint operation. transactionData: string; // The recommended Soroban Transaction Data to use when submitting the RestoreFootprint operation. } ``` Here is an example for handling restoration using the `restorePreamble` to restore archived data: ```typescript // Make sure to add the necessary imports: Account, Keypair, Operation, SorobanDataBuilder, rpc as StellarRpc, TimeoutInfinite, Transaction, TransactionBuilder, scValToNative, xdr, } from "@stellar/stellar-sdk"; /** * Simulates a restoration transaction to determine if restoration is needed. * This function first checks the ledger entry for the given WASM hash. If the entry is found and has expired, * it attempts a restoration. If the entry hasn't expired yet but the TTL needs extension, it proceeds with TTL extension. * @param contract - The address of the contract to check * @param txParams - The transaction parameters including account and signer. * @returns A promise that resolves to a simulation response, indicating whether restoration or TTL extension is needed. */ export async function simulateRestorationIfNeeded( contract: ContractAddress, txParams: TxParams, ): Promise< StellarRpc.Api.SimulateTransactionRestoreResponse | string | undefined > { try { const RpcServer = new StellarRpc.Server( "https://soroban-testnet.stellar.org", { allowHttp: true }, ); const account = await RpcServer.getAccount(txParams.account.accountId()); const contract = new Contract(contract); const ledgerKey = contract.getFootprint(); const response = await RpcServer.getLedgerEntries(ledgerKey); // Here we parse the response to make sure we got a response and that the liveUntilLedgerSeq parameter is there to make sure it's the response we want before continuing. if ( response.entries && response.entries.length > 0 && response.entries[0].liveUntilLedgerSeq ) { const expirationLedger = response.entries[0].liveUntilLedgerSeq; const desiredLedgerSeq = response.latestLedger + 500000; // Be very aware of how many ledgers you want to extend it by. It could quickly become extremely pricey in fees. let extendLedgers = desiredLedgerSeq - expirationLedger; if (extendLedgers < 10000) { extendLedgers = 10000; } console.log("Expiration:", expirationLedger); console.log("Desired TTL:", desiredLedgerSeq); const sorobanData = new SorobanDataBuilder() .setReadWrite([ledgerKey]) .build(); const restoreTx = new TransactionBuilder( account, txParams.txBuilderOptions, ) .setSorobanData(sorobanData) .addOperation(Operation.restoreFootprint({})) // The actual restore operation .build(); // Simulate a transaction with a restoration operation to check if it's necessary const restorationSimulation: StellarRpc.Api.SimulateTransactionResponse = await RpcServer.simulateTransaction(restoreTx); //check if restore is necessary. this code also checks if the simulation was successful. const restoreNeeded = StellarRpc.Api.isSimulationRestore( restorationSimulation, ); console.log(`restoration needed: ${restoreNeeded}`); // Check if the simulation indicates a need for restoration if (restoreNeeded) { return restorationSimulation as StellarRpc.Api.SimulateTransactionRestoreResponse; } else { console.log("No restoration needed., bumping the ttl."); const account1 = await RpcServer.getAccount( txParams.account.accountId(), ); const bumpTTLtx = new TransactionBuilder( account1, txParams.txBuilderOptions, ) .setSorobanData( new SorobanDataBuilder().setReadOnly([ledgerKey]).build(), ) .addOperation( Operation.extendFootprintTtl({ extendTo: desiredLedgerSeq, }), ) // The actual TTL extension operation .build(); const ttlSimResponse: StellarRpc.Api.SimulateTransactionResponse = await RpcServer.simulateTransaction(bumpTTLtx); const assembledTx = StellarRpc.assembleTransaction( bumpTTLtx, ttlSimResponse, ).build(); const signedTx = new Transaction( await txParams.signerFunction(assembledTx.toXDR()), Networks.TESTNET, ); // submit the assembled and signed transaction to bump it. try { const response = await sendTransaction(signedTx, (result) => { console.log(`bump ttl for contract result: ${result}`); return result; }); return response; } catch (error) { console.error("Transaction submission failed with error:", error); throw error; } } } else { console.log("No ledger entry found for the given WASM hash."); } } catch (error) { console.error("Failed to simulate restoration:", error); throw error; } } /** * Handles the restoration of a Soroban contract. * @param {StellarRpc.Api.SimulateTransactionRestoreResponse} simResponse - The simulation response containing restoration information. * @param {TxParams} txParams - The transaction parameters. * @returns {Promise} A promise that resolves when the restoration transaction has been submitted. */ export async function handleRestoration( simResponse: StellarRpc.Api.SimulateTransactionRestoreResponse, txParams: TxParams, ): Promise { const RpcServer = new StellarRpc.Server( "https://soroban-testnet.stellar.org", { allowHttp: true }, ); const restorePreamble = simResponse.restorePreamble; console.log("Restoring for account:", txParams.account.accountId()); const account = await RpcServer.getAccount(txParams.account.accountId()); // Construct the transaction builder with the necessary parameters const restoreTx = new TransactionBuilder(account, { ...txParams.txBuilderOptions, fee: restorePreamble.minResourceFee, // Update fee based on the restoration requirement }) .setSorobanData(restorePreamble.transactionData.build()) // Set Soroban Data from the simulation .addOperation(Operation.restoreFootprint({})) // Add the RestoreFootprint operation .build(); // Build the transaction const simulation: StellarRpc.Api.SimulateTransactionResponse = await RpcServer.simulateTransaction(restoreTx); const assembledTx = StellarRpc.assembleTransaction( restoreTx, simulation, ).build(); const signedTx = new Transaction( await txParams.signerFunction(assembledTx.toXDR()), Networks.TESTNET, ); console.log("Submitting restoration transaction"); try { // Submit the transaction to the network const response = await RpcServer.sendTransaction(signedTx); console.log( "Restoration transaction submitted successfully:", response.hash, ); } catch (error) { console.error("Failed to submit restoration transaction:", error); throw new Error("Restoration transaction failed"); } } ``` ## Fees and Resource Usage Soroban smart contracts on Stellar use a multidimensional resource fee model, charging fees for several resource types: 1. **CPU Instructions**: Number of CPU instructions the transaction uses. 2. **Ledger Entry Accesses**: Reading or writing any single ledger entry. 3. **Ledger I/O**: Number of bytes read from or written to the ledger. 4. **Transaction Size**: Size of the transaction submitted to the network in bytes. 5. **Events & Return Value Size**: Size of the events produced by the contract and the return value of the top-level contract function. 6. **Ledger Space Rent**: Payment for ledger entry TTL extensions and rent payments for increasing ledger entry size. Fees are calculated based on the resource consumption declared in the transaction. Refundable fees are charged before execution and refunded based on actual usage, while non-refundable fees are calculated from CPU instructions, read bytes, write bytes, and transaction size. [Check out this document for a more in depth understanding of fees and metering.](../../../learn/fundamentals/fees-resource-limits-metering.mdx) ## Error Handling The transaction simulation mechanism provides an estimation of CPU and memory consumption of a contract invocation during a transaction. It also highlights [potential errors and resource limitations](../../../learn/fundamentals/contract-development/errors-and-debugging/debugging-errors.mdx#1-transaction-simulation) of the invocation before actual submission. ## Backend Code and Workflow The `simulateTransaction` endpoint leverages various backend components to simulate the execution of a transaction. Here is a brief explanation of how it works: 1. **Invocation of Simulation**: - The simulation is initiated by calling `simulate_invoke_host_function_op` which takes in parameters such as the transaction to be simulated, resource configuration, and other necessary details. 2. **Snapshot Source and Network Configuration**: - The simulation utilizes a snapshot source (`MockSnapshotSource`) and network configuration (`NetworkConfig`) to mimic the state of the ledger and network conditions. 3. **Resource Calculation**: - The function `simulate_invoke_host_function_op_resources` computes the resources (CPU instructions, memory bytes) required for the transaction by analyzing ledger changes. 4. **Execution and Result Handling**: - The core of the execution is handled by `invoke_host_function_in_recording_mode`, which records the transaction's impact on the ledger. - The results are then processed, including any required authorizations, emitted events, and transaction data. 5. **Adjustments and Fees**: - Adjustments to resource usage and fees are made based on predefined configurations (`SimulationAdjustmentConfig`), ensuring accurate fee estimation. These functions are defined in the [`rs-soroban-env`](https://github.com/stellar/rs-soroban-env) and also in a [`soroban-simulation`](https://github.com/stellar/rs-soroban-env/tree/main/soroban-simulation) [crate](https://crates.io/crates/soroban-simulation) and handle the core logic for simulating transactions. ## Further Reading For more information and examples, check out the code and other documentation: - [openRpc Documentation](../../../data/apis/rpc/api-reference/methods/simulateTransaction.mdx) - [openRpc json Specification](https://github.com/stellar/stellar-docs/tree/main/openrpc) - [preflight.go](https://github.com/stellar/stellar-rpc/blob/release/v22.1.2/cmd/stellar-rpc/internal/preflight/preflight.go) - [Soroban Example Code](https://github.com/stellar/soroban-examples) - [Stellar SDK Documentation](https://stellar.github.io/js-stellar-sdk) --- ## Sponsored reserves Sponsored reserves were introduced in [CAP-33](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0033.md) and allow an account (sponsoring account) to pay the base reserves for another account (sponsored account). While this relationship exists, base reserve requirements that would normally accumulate on the sponsored account now accumulate on the sponsoring account. Both the Begin Sponsoring Future Reserves and the End Sponsoring Future Reserves operations must appear in the sponsorship transaction, guaranteeing that both accounts agree to the sponsorship. Anything that increases the minimum balance can be sponsored (account creation, offers, trustlines, data entries, signers, claimable balances). To learn about base reserves, see our section on [Lumens](../../../learn/fundamentals/lumens.mdx#base-reserves). :::note Use the [Stellar Wallet Sponsorship Calculator](../../../tools/developer-tools/wallets.mdx#stellar-wallet-sponsorship-calculator) to estimate the XLM requirements for wallets looking to use sponsored reserves and fee-bump transactions to cover account creation, transaction fees, trustlines, and more. ::: ## Sponsored reserves operations ### Begin and end sponsorships To create a sponsored reserve, you have to use a sandwich transaction that includes three operations. - The first operation: Begin Sponsoring Future Reserves initiates the sponsorship and requires the sponsoring account's signature. - The second operation: specifies what is being sponsored. - The third operation: End Sponsoring Future Reserves, allows the sponsored account to accept the sponsorship and requires the sponsored account’s signature. Begin Sponsoring Future Reserves establishes the is-sponsoring-future-reserves-for relationship where the sponsoring account is the source account of the operation. The account specified in the operation is the sponsored account. End Sponsoring Future Reserves ends the current is-sponsoring-future-reserves-for relationship for the source account of the operation. At the end of any transaction, there must be no ongoing is-sponsoring-future-reserves-for relationships, which is why these two operations must be used together in a single transaction. View operation details in our [List of Operations section](../../../learn/fundamentals/transactions/list-of-operations.mdx). ### Revoke sponsorship Allows the sponsoring account to remove or transfer sponsorships of existing ledgerEntries and signers. If the ledgerEntry or signer is not sponsored, the owner of the ledgerEntry or signer can establish a sponsorship if it is the beneficiary of an is-sponsoring-future-reserves-for relationship. Operation logic - Entry/signer is sponsored - Source account is currently the beneficiary of a is-sponsoring-future-reserves-for relationship - Transfer sponsorship of entry/signer from source account to the account that is-sponsoring-future-reserves-for source account - Source account is not the beneficiary of a is-sponsoring-future-reserves-for relationship - Remove the sponsorship from the entry/signer - Entry/signer is not sponsored - Source account is currently the beneficiary of a is-sponsoring-future-reserves-for relationship - Establish sponsorship between entry/signer and the account that is-sponsoring-future-reserves-for source account - Source account is not the beneficiary of a is-sponsoring-future-reserves-for relationship - No-Op View operation details in our [List of Operations section](../../../learn/fundamentals/transactions/list-of-operations.mdx#begin-sponsoring-future-reserves). ## Effect on minimum balance Once sponsorships are introduced, the minimum balance calculation is: (2 base reserves + `numSubEntries` + `numSponsoring` - `numSponsored`) \* `baseReserve` + `liabilities.selling`. When account A is sponsoring future reserves for account B, any reserve requirements that would normally accumulate on B will instead accumulate on A, shown in `numSponsoring`. The fact that these reserves are being provided by another account will be reflected on B in `numSponsored`, which cancels out the increase in `numSubEntries`, keeping the minimum balance unchanged for B. When a sponsored entry or subentry is removed, `numSponsoring` is decreased on the sponsoring account and `numSponsored` is decreased on the sponsored account. Because sponsorship can cover an account's own base reserves as well as its subentries, a brand-new account can be created with a `startingBalance` of `0`: wrap the `CreateAccount` operation in a `BeginSponsoringFutureReserves`/`EndSponsoringFutureReserves` sandwich (as in the examples below) and the sponsor carries the two base reserves so the account never has to hold the minimum balance itself. To learn more about minimum balance requirements, see our section on [Lumens](../../../learn/fundamentals/lumens.mdx#minimum-balance). ## Effect on claimable balances All claimable balances are sponsored through built-in logic in the claimable balance operations. The account that creates the claimable balance pays the base reserve to get the claimable balance on the ledger. When the claimable balance is claimed by the claimant(s), the claimable balance is removed from the ledger, and the account that created it gets the base reserve back. Read more about claimable balances in our [Claimable Balances guide](./claimable-balances.mdx). ## Examples Each of the following examples builds on itself, referencing variables from previous snippets. The following examples will demonstrate: 1. [Sponsor trustline creation](#1-sponsoring-trustlines) for another account 2. [Transfer the sponsorship](#2-transferring-sponsorship) responsibility from one account to another 3. [Revoke the sponsorship](#3-sponsorship-revocation) by an account entirely For brevity in the Golang examples, we’ll assume the existence of a `SignAndSend`(...) method (defined below) which creates and submits a transaction with the proper parameters and basic error-checking. ### Preamble We’ll start by including the boilerplate of account and asset creation. ```js Keypair, Asset, TransactionBuilder, Operation, Networks, BASE_FEE, } from "@stellar/stellar-sdk"; let server = new Server("https://soroban-testnet.stellar.org"); async function main() { // Create & fund the new accounts. let keypairs = [Keypair.random(), Keypair.random(), Keypair.random()]; for (const keypair of keypairs) { console.log(`Funding:\n ${keypair.secret()}\n ${keypair.publicKey()}`); await server .requestAirdrop(keypair.publicKey()) .catch((err) => console.error(" failed:", err)); } // Arbitrary assets to sponsor trustlines for. Let's assume they make sense. let S1 = keypairs[0], A = keypairs[1], S2 = keypairs[2]; let assets = [ new Asset("ABCD", S1.publicKey()), new Asset("EFGH", S1.publicKey()), new Asset("IJKL", S2.publicKey()), ]; // ... ``` ```go package main "context" "fmt" "net/http" "strings" "time" "github.com/stellar/go-stellar-sdk/keypair" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/txnbuild" sdk "github.com/stellar/go-stellar-sdk/clients/rpcclient" protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" ) func main() { client := sdk.NewClient("https://soroban-testnet.stellar.org", nil) // Both S1 and S2 will be sponsors for A at various points in time. S1, A, S2 := keypair.MustRandom(), keypair.MustRandom(), keypair.MustRandom() addressA := A.Address() for _, pair := range []*keypair.Full{S1, A, S2} { resp, err := http.Get("https://friendbot.stellar.org/?addr=" + pair.Address()) check(err) resp.Body.Close() fmt.Println("Funded", pair.Address()) } // Load the corresponding account for both A and C. ctx := context.Background() s1Account, err := client.LoadAccount(ctx, S1.Address()) check(err) aAccount, err := client.LoadAccount(ctx, addressA) check(err) s2Account, err := client.LoadAccount(ctx, S2.Address()) check(err) // Arbitrary assets to sponsor trustlines for. Let's assume they make sense. assets := []txnbuild.CreditAsset{ {Code: "ABCD", Issuer: S1.Address()}, {Code: "EFGH", Issuer: S1.Address()}, {Code: "IJKL", Issuer: S2.Address()}, } // ... ``` ### 1. Sponsoring trustlines Now, let’s sponsor trustlines for Account A. Notice how the `CHANGE_TRUST` operation is sandwiched between the begin and end sponsoring operations and that all relevant accounts need to sign the transaction.
```js // // 1. S1 will sponsor a trustline for Account A. // let s1Account = await server.getAccount(S1.publicKey()).catch(accountFail); let tx = new TransactionBuilder(s1Account, { fee: BASE_FEE }) .addOperation( Operation.beginSponsoringFutureReserves({ sponsoredId: A.publicKey(), }), ) .addOperation( Operation.changeTrust({ source: A.publicKey(), asset: assets[0], limit: "1000", // This limit can vary according with your application; // if left empty, it defaults to the max limit. }), ) .addOperation( Operation.endSponsoringFutureReserves({ source: A.publicKey(), }), ) .setNetworkPassphrase(Networks.TESTNET) .setTimeout(180) .build(); // Note that while either can submit this transaction, both must sign it. tx.sign(S1, A); let txResponse = await sendTransaction(tx); if (!txResponse) { return; } console.log("Sponsored a trustline of", A.publicKey()); ``` ```go // // 1. S1 will sponsor a trustline for Account A. // sponsorTrustline := []txnbuild.Operation{ &txnbuild.BeginSponsoringFutureReserves{ SourceAccount: s1Account.GetAccountID(), SponsoredID: addressA, }, &txnbuild.ChangeTrust{ Line: assets[0].MustToChangeTrustAsset(), Limit: txnbuild.MaxTrustlineLimit, }, &txnbuild.EndSponsoringFutureReserves{}, } // Note that while A can submit this transaction, both sign it. SignAndSend(client, aAccount, []*keypair.Full{S1, A}, sponsorTrustline...) fmt.Println("Sponsored a trustline of", A.Address()) ```
```js // // 2. Both S1 and S2 sponsor trustlines for Account A for different assets. // let aAccount = await server.getAccount(A.publicKey()).catch(accountFail); tx = new TransactionBuilder(aAccount, { fee: BASE_FEE }) .addOperation( Operation.beginSponsoringFutureReserves({ source: S1.publicKey(), sponsoredId: A.publicKey(), }), ) .addOperation( Operation.changeTrust({ asset: assets[1], limit: "5000", }), ) .addOperation(Operation.endSponsoringFutureReserves()) .addOperation( Operation.beginSponsoringFutureReserves({ source: S2.publicKey(), sponsoredId: A.publicKey(), }), ) .addOperation( Operation.changeTrust({ asset: assets[2], limit: "2500", }), ) .addOperation(Operation.endSponsoringFutureReserves()) .setNetworkPassphrase(Networks.TESTNET) .setTimeout(180) .build(); // Note that all 3 accounts must approve/sign this transaction. tx.sign(S1, S2, A); txResponse = await sendTransaction(tx); if (!txResponse) { return; } console.log("Sponsored two trustlines of", A.publicKey()); ``` ```go // // 2. Both S1 and S2 sponsor trustlines for Account A for different assets. // sponsorTrustline = []txnbuild.Operation{ &txnbuild.BeginSponsoringFutureReserves{ SourceAccount: s1Account.GetAccountID(), SponsoredID: addressA, }, &txnbuild.ChangeTrust{ Line: assets[1].MustToChangeTrustAsset(), Limit: txnbuild.MaxTrustlineLimit, }, &txnbuild.EndSponsoringFutureReserves{}, &txnbuild.BeginSponsoringFutureReserves{ SourceAccount: s2Account.GetAccountID(), SponsoredID: addressA, }, &txnbuild.ChangeTrust{ Line: assets[2].MustToChangeTrustAsset(), Limit: txnbuild.MaxTrustlineLimit, }, &txnbuild.EndSponsoringFutureReserves{}, } // Note that all 3 accounts must approve/sign this transaction. SignAndSend(client, aAccount, []*keypair.Full{S1, S2, A}, sponsorTrustline...) fmt.Println("Sponsored two trustlines of", A.Address()) ```
### 2. Transferring sponsorship Suppose that now Signer 1 wants to transfer the responsibility of sponsoring reserves for the trustline to Sponsor 2. This is accomplished by sandwiching the transfer between the `BEGIN/END_SPONSORING_FUTURE_RESERVES` operations. Both of the participants must sign the transaction, though either can submit it. An intuitive way to think of a sponsorship transfer is that the very act of sponsorship is being sponsored by a new account. That is, the new sponsor takes over the responsibilities of the old sponsor by sponsoring a revocation. ```js // // 3. Transfer sponsorship of B's second trustline from S1 to S2. // tx = new TransactionBuilder(s1Account, { fee: BASE_FEE }) .addOperation( Operation.beginSponsoringFutureReserves({ source: S2.publicKey(), sponsoredId: S1.publicKey(), }), ) .addOperation( Operation.revokeTrustlineSponsorship({ account: A.publicKey(), asset: assets[1], }), ) .addOperation(Operation.endSponsoringFutureReserves()) .setNetworkPassphrase(Networks.TESTNET) .setTimeout(180) .build(); // Notice that while the old sponsor *sends* the transaction, both sponsors // must *approve* the transfer. tx.sign(S1, S2); txResponse = await sendTransaction(tx); if (!txResponse) { return; } console.log("Transferred sponsorship for", A.publicKey()); ``` ```go // // 3. Transfer sponsorship of B's second trustline from S1 to S2. // transferOps := []txnbuild.Operation{ &txnbuild.BeginSponsoringFutureReserves{ SourceAccount: s2Account.GetAccountID(), SponsoredID: S1.Address(), }, &txnbuild.RevokeSponsorship{ SponsorshipType: txnbuild.RevokeSponsorshipTypeTrustLine, Account: &addressA, TrustLine: &txnbuild.TrustLineID{ Account: addressA, Asset: assets[1].MustToTrustLineAsset(), }, }, &txnbuild.EndSponsoringFutureReserves{}, } // Notice that while the old sponsor *sends* the transaction (in this case), // both sponsors must *approve* the transfer. SignAndSend(client, s1Account, []*keypair.Full{S1, S2}, transferOps...) fmt.Println("Transferred sponsorship for", A.Address()) ``` At this point, Signer 1 is only sponsoring the first asset (arbitrarily coded as ABCD), while Signer 2 is sponsoring the other two assets. (Recall that initially Signer 1 was also sponsoring EFGH.) ### 3. Sponsorship revocation Finally, we can demonstrate complete revocation of sponsorships. Below, Signer 2 removes themselves from all responsibility over the two asset trustlines. Notice that Account A is not involved at all, since revocation should be performable purely at the sponsor’s discretion. ```js // // 4. S2 revokes sponsorship of B's trustlines entirely. // let s2Account = await server.getAccount(S2.publicKey()).catch(accountFail); tx = new TransactionBuilder(s2Account, { fee: BASE_FEE }) .addOperation( Operation.revokeTrustlineSponsorship({ account: A.publicKey(), asset: assets[1], }), ) .addOperation( Operation.revokeTrustlineSponsorship({ account: A.publicKey(), asset: assets[2], }), ) .setNetworkPassphrase(Networks.TESTNET) .setTimeout(180) .build(); tx.sign(S2); txResponse = await sendTransaction(tx); if (!txResponse) { return; } console.log("Revoked sponsorship for", A.publicKey()); } // ends main() ``` ```go // // 4. S2 revokes sponsorship of B's trustlines entirely. // revokeOps := []txnbuild.Operation{ &txnbuild.RevokeSponsorship{ SponsorshipType: txnbuild.RevokeSponsorshipTypeTrustLine, Account: &addressA, TrustLine: &txnbuild.TrustLineID{ Account: addressA, Asset: assets[1].MustToTrustLineAsset(), }, }, &txnbuild.RevokeSponsorship{ SponsorshipType: txnbuild.RevokeSponsorshipTypeTrustLine, Account: &addressA, TrustLine: &txnbuild.TrustLineID{ Account: addressA, Asset: assets[2].MustToTrustLineAsset(), }, }, } SignAndSend(client, s2Account, []*keypair.Full{S2}, revokeOps...) fmt.Println("Revoked sponsorship for", A.Address()) } // ends main() ``` ### Sponsorship Source Accounts When it comes to the SourceAccount fields of the sponsorship sandwich, it's important to refer to the wisdom of [CAP-33](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0033.md#abstract): > This relation is initiated by `BeginSponsoringFutureReservesOp`, where the sponsoring account is the source account, and is terminated by `EndSponsoringFutureReserveOp`, where the sponsored account is the source account. Since the source account defaults to the transaction submitter when omitted, this field needs always needs to be set for either the `Begin` or the `End`. For example, the following is an identical expression of the earlier Golang example of sponsoring a trustline, just submitted by the sponsor (Sponsor 1) rather than the sponsored account (Account A). Notice the differences in where `SourceAccount` is set: ```go sponsorTrustline := []txnbuild.Operation{ &txnbuild.BeginSponsoringFutureReserves{ SponsoredID: addressA, }, &txnbuild.ChangeTrust{ SourceAccount: aAccount.AccountID, Line: &assets[0], Limit: txnbuild.MaxTrustlineLimit, }, &txnbuild.EndSponsoringFutureReserves{ SourceAccount: aAccount.AccountID, }, } // Again, both participants must still sign the transaction: the sponsored // account must consent to the sponsorship. SignAndSend(client, s1Account.AccountID, []*keypair.Full{S1, A}, sponsorTrustline...) ``` ### Other examples If you’d like other examples or want to view a more-generic pseudo-code breakdown of these sponsorship scenarios, you can refer to [CAP-33](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0033.md#example-revoke-sponsorship) directly. ### Footnote For the above examples, an implementation of `SignAndSend` (Golang) and some (very) rudimentary error checking code (all languages) might look something like this: ```js async function sendTransaction(tx) { const sendResp = await server.sendTransaction(tx); if (sendResp.status !== "PENDING") throw sendResp; const getResp = await server.pollTransaction(sendResp.hash); if (getResp.status !== "SUCCESS") throw getResp; return getResp; } function accountFail(err) { console.error(" Failed to load account:", err); } ``` ```go // Builds a transaction containing `operations...`, signed (by `signers`), and // submitted using the given `client` on behalf of `account`. func SignAndSend( client *sdk.Client, account txnbuild.Account, signers []*keypair.Full, operations ...txnbuild.Operation, ) protocol.GetTransactionResponse { // Build, sign, and submit the transaction tx, err := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: account, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Preconditions: txnbuild.Preconditions{ TimeBounds: txnbuild.NewInfiniteTimeout(), }, Operations: operations, }, ) check(err) for _, signer := range signers { tx, err = tx.Sign(network.TestNetworkPassphrase, signer) check(err) } txnB64, err := tx.Base64() check(err) txSendResp, err := client.SendTransaction(context.Background(), protocol.SendTransactionRequest{Transaction: txnB64}) check(err) for i := range 5 { txResp, err := client.GetTransaction(context.Background(), protocol.GetTransactionRequest{Hash: txSendResp.Hash}) check(err) switch txResp.Status { case "NOT_FOUND": case "SUCCESS": return txResp case "FAILED": panic(fmt.Errorf("transaction failed: %s", strings.Join(txResp.DiagnosticEventsXDR, "\n"))) } // Increase delay for each polling request time.Sleep(time.Duration(i) * time.Second) } panic(fmt.Errorf("transaction never found: %s", txSendResp.Hash)) } func check(err error) { if err != nil { panic(err) } } ``` --- ## Submit a transaction to Stellar RPC using the JavaScript SDK Here is a simple, rudimentary looping mechanism to submit a transaction to Stellar RPC and wait for a result. ```typescript const RPC_SERVER = "https://soroban-testnet.stellar.org/"; const server = new Server(RPC_SERVER); // Submits a tx and then polls for its status until a timeout is reached. async function submitTx( tx: Transaction | FeeBumpTransaction, ): Promise { return server .sendTransaction(tx) .then(async (reply) => { if (reply.status !== "PENDING") { throw reply; } return server.pollTransaction(reply.hash, { sleepStrategy: (_iter: number) => 500, attempts: 5, }); }) .then((finalStatus) => { switch (finalStatus.status) { case Api.GetTransactionStatus.FAILED: case Api.GetTransactionStatus.NOT_FOUND: throw tmpStatus; case Api.GetTransactionStatus.SUCCESS: return status; } }); } ``` :::caution Remember: You should always handle errors gracefully! This is a fail-hard and fail-fast approach that should only be used in these examples. ::: --- ## Upload WebAssembly (Wasm) bytecode using code Install WebAssembly (Wasm) bytecode using code # Install WebAssembly (Wasm) bytecode using code This process uploads the contract code to the Stellar Testnet in a transaction, the uploaded Wasm blob is a [contract source](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-02.md#contract-source), which can be thought of as a 'class' of a contract. Multiple [instances of a contract can be deployed](../conventions/deploy-contract.mdx) which share the same source, but have their own state. ## Prerequisites Before you begin, ensure you have the following installed to compile a smart contract: 1. [Rust](https://www.rust-lang.org) and Cargo (for compiling smart contracts) 2. The [Stellar CLI](https://github.com/stellar/stellar-cli) ## Initialize and build a sample Rust contract ```bash stellar contract init hello-world cd hello-world # use rust compiler to compile Rust project into a WebAssembly (Wasm) dynamic library (cdylib). stellar contract build # cargo rustc --manifest-path=contracts/hello_world/Cargo.toml --crate-type=cdylib --target=wasm32v1-none --release ``` We are initializing the default `hello_world` contract in [soroban_examples](https://github.com/stellar/soroban-examples) to the directory `hello-world`. You may use the `-w` option to create with another example like `account`. After building the contract for release, the generated .wasm file is at the path `hello-word/target/wasm32v1-none/release/hello_world.wasm` ## Upload Wasm to the Stellar blockchain We can use one of the [client SDKs](../../../tools/sdks/client-sdks.mdx), install necessary dependencies for your chosen programming language: - JavaScript SDK: [Node.js](https://nodejs.org/en) and npm - Python SDK: [pip](https://pip.pypa.io/en/stable/installation) Create a new directory for your project and navigate into it: ```bash mkdir install-wasm cd install-wasm ``` ### Running the install script Different programming languages can be used to upload the `.wasm` file, its SHA256 hash is used for deploying the contract. This guide will walk you through installing the Wasm of the contract using the JavaScript SDK: [js-stellar-sdk](https://github.com/stellar/js-stellar-sdk). Create a new Node.js project with a JavaScript file, and install necessary dependencies: ```bash touch index.js npm init es6 -y npm install @stellar/stellar-sdk fs ``` Run the script with `node index.js`, it reads the Wasm file, gets account details, and uploads the contract in a transaction: ```javascript // Import necessary modules in your JavaScript file: async function uploadWasm(filePath) { // reads the compiled Wasm file to buffer const bytecode = fs.readFileSync(filePath); // retrieves account details from the network const account = await server.getAccount(sourceKeypair.publicKey()); // installs the bytecode with a `uploadContractWasm` Stellar operation wrapped in a transaction sent to the network const operation = StellarSDK.Operation.uploadContractWasm({ wasm: bytecode }); return await buildAndSendTransaction(account, operation); } // constructs a transaction, signs it, and submits it to the network, handling any necessary retries for transaction confirmation. async function buildAndSendTransaction(account, operations) { const transaction = new StellarSDK.TransactionBuilder(account, { fee: StellarSDK.BASE_FEE, networkPassphrase: StellarSDK.Networks.TESTNET, }) .addOperation(operations) .setTimeout(30) .build(); const tx = await server.prepareTransaction(transaction); tx.sign(sourceKeypair); console.log("Submitting transaction..."); let response = await server.sendTransaction(tx); const hash = response.hash; console.log(`Transaction hash: ${hash}`); console.log("Awaiting confirmation..."); while (true) { response = await server.getTransaction(hash); if (response.status !== "NOT_FOUND") { break; } await new Promise((resolve) => setTimeout(resolve, 1000)); } if (response.status === "SUCCESS") { console.log("Transaction successful."); return response; } else { console.log("Transaction failed."); throw new Error("Transaction failed"); } } // Upload contract to the testnet const server = new StellarSDK.rpc.Server( "https://soroban-testnet.stellar.org:443", ); // Replace `Your_Secret_Key` const sourceKeypair = StellarSDK.Keypair.fromSecret("Your_Secret_Key"); // Adjust this path as necessary const wasmFilePath = "../target/wasm32v1-none/release/hello_world.wasm"; try { let uploadResponse = await uploadWasm(wasmFilePath); const byteArray = uploadResponse.returnValue.bytes(); const wasmHash = byteArray.toString("hex"); console.log(`Wasm hash: ${wasmHash}`); } catch (error) { console.error(error); } ``` This guide will walk you through installing the Wasm of the contract using the Python SDK: [py-stellar-base](https://stellar-sdk.readthedocs.io/en/soroban). Create a new Python script, and install the necessary dependencies: ```bash touch install.py pip install stellar-sdk ``` Run the script with `python3 install.py`, it reads the Wasm file, gets account details, and uploads the contract in a transaction: ```py from stellar_sdk import Keypair, Network, SorobanServer, TransactionBuilder, xdr as stellar_xdr from stellar_sdk.exceptions import PrepareTransactionException from stellar_sdk.soroban_rpc import GetTransactionStatus def print_wasm_hash(wasm_bytes): # Create a SHA256 hash object sha256_hash = hashlib.sha256() # Update the hash object with the WASM bytes sha256_hash.update(wasm_bytes) # Get the digest and convert it to a stellar_sdk.xdr.Hash object hash_bytes = sha256_hash.digest() xdr_hash = stellar_xdr.Hash(hash_bytes) hash_hex_str = xdr_hash.hash.hex() print(f"Wasm Hash: {hash_hex_str}") async def upload_wasm(file_path): # Read the compiled Wasm file with open(file_path, 'rb') as file: wasm_bytes = file.read() # Retrieve account details from the network account = server.load_account(source_keypair.public_key) # print wasm hash for later deployment print_wasm_hash(wasm_bytes) # Install the bytes with an 'uploadContractWasm' Stellar operation return await build_and_send_transaction(account, wasm_bytes) async def build_and_send_transaction(account, bytes): transaction = ( TransactionBuilder( source_account=account, network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, ) .append_upload_contract_wasm_op(bytes) .set_timeout(30) .build() ) print("Preparing transaction...") try: tx = server.prepare_transaction(transaction) except PrepareTransactionException as e: print(f"Got exception: {e}") return None tx.sign(source_keypair) print("Submitting transaction...") response = server.send_transaction(tx) hash = response.hash print(f"Transaction hash: {hash}") print("Awaiting confirmation...") while True: response = server.get_transaction(hash) if response.status != GetTransactionStatus.NOT_FOUND: break time.sleep(1) if response.status == GetTransactionStatus.SUCCESS: print("Transaction successful.") return response else: print("Transaction failed.") raise Exception("Transaction failed") # Upload contract to the testnet server = SorobanServer("https://soroban-testnet.stellar.org:443") # Replace 'Your_Secret_Key' with your actual secret key source_keypair = Keypair.from_secret("Your_Secret_Key") # Adjust this path as necessary wasm_file_path = "../target/wasm32v1-none/release/hello_world.wasm" async def main(): try: await upload_wasm(wasm_file_path) except Exception as error: print(f"Error: {error}") # Run the main function asyncio.run(main()) ``` This guide will walk you through uploading the Wasm of the contract using the Rust SDK: [rs-soroban-client](https://github.com/rahul-soshte/rs-soroban-client). Create a new Cargo project, and install the necessary dependencies: ```bash cargo init cargo add soroban_client cargo add tokio -F macros,full ``` Copy the code below into `src/main.rs` then run the program with `cargo run`, it reads the Wasm file, gets account details, and uploads the contract in a transaction: ```rust use std::time::Duration; use soroban_client::{ account::{Account, AccountBehavior}, keypair::{Keypair, KeypairBehavior}, network::{NetworkPassphrase, Networks}, operation::Operation, soroban_rpc::TransactionStatus, transaction::{TransactionBehavior, TransactionBuilder, TransactionBuilderBehavior}, Options, Server, }; #[tokio::main] pub async fn main() -> Result<(), Box> { let server_url = "https://soroban-testnet.stellar.org"; let server = Server::new(server_url, Options::default())?; // Adjust this path as necessary let wasm_file_path = "../target/wasm32v1-none/release/hello_world.wasm"; // Read the wasm file let wasm_bytes = std::fs::read(wasm_file_path)?; // Build the xdr::HostFunction::UploadContractWasm operation let upload_wasm_op = Operation::new() .upload_wasm(&wasm_bytes, None) .expect("Cannot create upload_wasm operation"); // Replace Your_Secret_Key with your actual secret key let source_keypair = Keypair::from_secret("Your_Secret_Key")?; let source_public_key = &source_keypair.public_key(); // Get account information from server let account_data = server.get_account(source_public_key).await?; // Build the Account to use in the transaction let mut source_account = Account::new(source_public_key, &account_data.sequence_number())?; // Build the operation let tx = TransactionBuilder::new(&mut source_account, Networks::testnet(), None) .fee(1000u32) .add_operation(upload_wasm_op) .build(); let mut ptx = server.prepare_transaction(&tx).await?; ptx.sign(&[source_keypair]); println!("> Uploading WASM executable"); let response = server.send_transaction(ptx).await?; let hash = &response.hash; println!(">> Tx hash: {hash}"); let wasm_hash = match server.wait_transaction(hash, Duration::from_secs(15)).await { Ok(tx_result) if tx_result.status == TransactionStatus::Success => { let (_meta, ret_val) = tx_result.to_result_meta().expect("No meta found"); if let Some(scval) = ret_val { let bytes: Vec = scval.try_into().expect("Cannot convert ScVal to Vec"); *bytes.last_chunk::<32>().expect("Not 32 bytes") } else { return Err(">> None return value".into()); } } _ => { println!(">> Failed to upload the WASM executable"); return Err(">> Failed to upload the wasm".into()); } }; let hex_wasm_hash: String = wasm_hash.iter().map(|b| format!("{:02x}", b)).collect(); println!(">> Wasm hash: {hex_wasm_hash}"); println!(); Ok(()) } ``` Replace `"Your_Secret_Key"` with your actual secret key. ```bash stellar keys generate --global hello --network testnet stellar keys show hello ``` The Stellar CLI can be used to generate identities, e.g. `hello`, and show its secret key. :::tip Ensure that you handle secret and private keys securely in production environments and never expose them in your code repositories. ::: ```bash Submitting transaction... Transaction hash: cef7a63667fe5b0ddcde5562d90e0a40bc04c69616916d1d7fa74a8571bbd82f Awaiting confirmation... Transaction successful. Wasm hash: 275405755441e4be59555bb5c5fd81e84ed21659015d8f3594796c1cf3f380db ``` The returned upload transaction hash can be viewed with online tools: [stellar.expert/explorer/testnet/tx/cef7a63667fe5b0ddcde5562d90e0a40bc04c69616916d1d7fa74a8571bbd82f](https://stellar.expert/explorer/testnet/tx/cef7a63667fe5b0ddcde5562d90e0a40bc04c69616916d1d7fa74a8571bbd82f) [Uploaded contracts are stored in ContractCodeEntry ledger entries.](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-02.md#uploading-wasm-sources-using-invokehostfunctionop) These entries are keyed by the hash of the Wasm used to upload them. You may use the Wasm hash for deployment with the Stellar CLI: ```bash stellar contract deploy \ --source-account hello \ --network testnet \ --wasm-hash 275405755441e4be59555bb5c5fd81e84ed21659015d8f3594796c1cf3f380db # CC6NAQE3ZHRQV3NPQB3F3NYEFBAMEABA4KQTM6A2V5V7PBR5H3UEU3MW ``` View the deployed contract using the returned identifier with online tools: [stellar.expert/explorer/testnet/contract/CC6NAQE3ZHRQV3NPQB3F3NYEFBAMEABA4KQTM6A2V5V7PBR5H3UEU3MW](https://stellar.expert/explorer/testnet/contract/CC6NAQE3ZHRQV3NPQB3F3NYEFBAMEABA4KQTM6A2V5V7PBR5H3UEU3MW) --- ## Security Best Practices A series of guides and documentation to help developers build safe and secure products on Stellar. --- ## Onchain Monitoring Templates Learn how to monitor your important positions and protocols on Stellar. --- ## On-Chain Monitoring Plan Template (Builders) This template turns a completed [threat model](../threat-modeling/README.mdx) into an operational monitoring plan for a protocol or set of smart contracts on Stellar. Where the threat model asks _what could go wrong and how will we design against it_, this plan asks _how will we watch for it in production_. Each threat identified in the threat model becomes one or more observable on-chain effects, and each effect becomes a rule that the monitoring solution watches while the contracts are live. **Directions**: Fill out each section below to the best of your ability. Carry the threat identifiers over from the STRIDE threat model so the two documents stay linked. Work through the sections in order; every threat should leave the exercise with at least one monitor and a defined response. **Keep completed plans internal.** A filled-in monitoring plan is a map of what is (and isn’t) monitored. Treat a populated version as sensitive. ## What are we monitoring? **Directions**: Describe the system in scope, then inventory the components and on-chain addresses this plan covers. _Input text: high-level description of the protocol, the value it holds, and the parts of it this plan covers._ | Component | On-chain address (`C...` contract / `G...` account) | Notes | | --- | --- | --- | | _e.g. Lending pool core_ | _C..._ | _Holds pooled collateral and debt_ | | | | | :::note Keep this inventory current. Addresses can change with new versions, and an out-of-date address list is the most common reason a monitor silently stops working. If it's discovered later that scope has changed, update this section with the latest information. ::: ## What could go wrong? **Directions**: List the threats this plan monitors, carried over from the threat model. Reuse the identifiers from the STRIDE model (for example, `Elevation.1`) so each monitor traces back to a known threat. Assign each threat a severity using the reminders below. ### Severity reminders | Severity | Meaning | | --- | --- | | **Critical** | Direct, large-scale loss of funds or control; requires immediate response. | | **High** | Serious impact to funds, users, or availability; requires prompt response. | | **Medium** | Limited or containable impact; response can be scheduled. | | **Low** | Minor or informational; monitored for awareness. | ### Threat register | Threat ID | Threat (from threat model) | Affected component | Severity | | --- | --- | --- | --- | | _e.g. Elevation.1_ | _Unauthorized change of contract admin/owner_ | _Lending pool core, access control_ | _Critical_ | | | | | | ## What does exploitation look like on-chain? **Directions**: For each threat, describe how exploitation might unfold and the observable on-chain effect(s) it would leave behind. There can be several scenarios, and several effects, per threat. | Threat ID | Exploitation scenario | Observable on-chain effect(s) | | --- | --- | --- | | _e.g. Elevation.1_ | _A leaked admin key reassigns ownership, then raises borrow caps and drains reserves._ | _`set_admin` / ownership-transfer event; privileged parameter change from a non-allowlisted caller_ | | | | | :::note If a threat has no observable on-chain effect (for example, a compromised frontend or an oracle's upstream data source), record it here and plan to monitor it off-chain. Don't drop it just because it isn't visible on-chain. ::: ## What will we monitor for? **Directions**: Map each observable effect to a monitoring rule, stating the trigger condition and the baseline that makes it meaningful. Uniquely identify each monitor as `.M.` so it traces back to its threat, the same way remediations in the threat model are identified as `.R.`. | Monitor ID | Observable on-chain effect | Trigger condition & baseline | Monitoring rule (plain-language intent) | | --- | --- | --- | --- | | _e.g. Elevation.1.M.1_ | _Owner/admin address changes_ | _Any `set_admin` event. Baseline: zero; never expected in normal operation._ | _Alert immediately on any admin change on the core contract._ | | _e.g. Elevation.1.M.2_ | _Privileged call from an unexpected address_ | _Caller not in the known-admin allowlist. Baseline: only the multisig calls these._ | _Alert on privileged calls from non-allowlisted addresses._ | | | | | | Each monitor can be stated in a single line: - We address **[Threat ID]** in **[affected component]** by monitoring for **[on-chain effect]** on **[address]**. - _For example: **Elevation.1.M.1** - We address **Elevation.1** in **the lending pool factory** by monitoring for **deploy invocations from users not on an approved list** at address **CABC123...**._ ## What happens when an alert fires? **Directions**: For each monitor, define what happens when it triggers: who is notified, through which channel, any automated action, and who owns the rule. This is where the monitoring vendor's automated-response capability (for example, pausing a contract) is recorded. | Monitor ID | Severity | Response / action (who, channel, automated action) | Owner | Status | Last reviewed | | --- | --- | --- | --- | --- | --- | | _e.g. Elevation.1.M.1_ | _Critical_ | _Page on-call security (PagerDuty + Slack); trigger guardian pause; open incident bridge._ | _Protocol Security_ | _Active_ | _YYYY-MM-DD_ | | | | | | | | **Status values**: _Active_ (live and alerting), _Tuning_ (live but thresholds being refined), _Planned_ (agreed but not yet implemented). ## Did we do a good job? - Does every threat in the threat model have at least one monitor, or a documented reason it can't be monitored on-chain? - Is every trigger threshold grounded in an actual baseline rather than a guess? - Does every monitor have a defined response, an owner, and a status? - Have any monitors fired? Were they true positives? Did the response work as written? - Are all on-chain addresses still current after the latest deploy or upgrade? - Were any threats found that only surface off-chain, needing a different kind of monitoring? Treat this plan as a living document: revisit it whenever the contracts, addresses, or threat model change. --- ## Position Monitoring Plan Template (Users) This template helps you, as someone holding positions on Stellar, set up monitoring for what you own, without needing a formal threat model. Fill in one row per position or wallet you care about: what could go wrong, when you want to be alerted, and what you'll do about it. **Directions**: Fill out each section below to the best of your ability. Keep the same position in the same row across every table so the plan reads straight across. ## What am I protecting? **Directions**: List the positions and wallets you hold and care about. _Input text: a short description of each position and where it lives._ | Position | Wallet / position address (`G...` account / `C...` contract) | Value at risk (optional) | | --- | --- | --- | | _e.g. XLM collateral in a lending position_ | _G..._ | _Dollar amount or percentage_ | | | | | ## What would going wrong look like? **Directions**: In plain language, note what "something going wrong" would look like for each position, and the level of concern using the reminders below. ### How concerned am I? | Level | Meaning | | ------------------- | ------------------------------------------- | | **Drop everything** | I need to act right now to avoid loss. | | **Important** | I want to know quickly and will act soon. | | **Keep an eye** | Useful to know; I'll check when convenient. | | **Low** | Informational only. | | Position | What "going wrong" looks like | How concerned am I | | --- | --- | --- | | _e.g. XLM collateral..._ | _My position drifts toward liquidation; the protocol's admin changes unexpectedly; the pool drains suddenly._ | _Important_ | | | | | ## What will alert me? **Directions**: Turn each concern into a specific condition you can be alerted on, and choose how you want to be notified. | Position | Alert me when... | Notify me by | | --- | --- | --- | | _e.g. XLM collateral..._ | _Health factor drops below 1.5; any admin change on the market; pool liquidity falls more than 30% in an hour._ | _Push + Telegram_ | | | | | :::note Don't assume the protocol is watching your specific position. Its monitoring is tuned to the health of the whole system; yours should be tuned to your concerns. ::: ## What will I do? **Directions**: Decide your action before an alert fires — it's much harder to think clearly once one does. | Position | If it fires, I will... | Status | | --- | --- | --- | | _e.g. XLM collateral..._ | _Add collateral or withdraw before liquidation._ | _Watching_ | | | | | **Status values**: _Watching_ (alerts active) or _Paused_ (temporarily off). ## Did I set this up well? - Does every position I care about have at least one alert? - Are my "alert me when" conditions specific enough that I could act on them? - Do I know what I'll do before an alert fires? - Is my list of addresses still current? - Am I relying on a protocol to watch something that only I can see for my own position? Revisit this whenever you open, close, or change a position. --- ## Securing Web-Based Projects Any application managing cryptocurrency is a frequent target of malicious actors and needs to follow security best practices. The below checklist offers guidance on the most common vulnerabilities. However, even if you follow every piece of advice, security is not guaranteed. Web security and malicious actors are constantly evolving, so it’s good to maintain a healthy amount of paranoia. ## SSL/TLS Ensure that TLS is enabled. Redirect HTTP to HTTPS where necessary to ensure that Man in the Middle attacks can’t occur and sensitive data is securely transferred between the client and browser. Enable TLS and get an SSL certificate for free at [LetsEncrypt](https://letsencrypt.org/getting-started). If you don’t have SSL/TLS enabled, stop everything and do this first. ## Content security policy (CSP) headers CSP headers tell the browser where it can download static resources from. For example, if you astralwallet.io and it requests a JavaScript file from myevilsite.com, your browser will block it unless it was whitelisted with CSP headers. You can read about how to implement CSP headers [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). Most web frameworks have a configuration file or extensions to specify your CSP policy, and the headers are auto-generated for you. For example, see [Helmet](https://www.npmjs.com/package/helmet) for Node.js. This would have prevented the [Blackwallet Hack](https://www.ccn.com/yet-another-crypto-wallet-hack-causes-users-lose-400000). ## HTTP strict-transport-security headers This is an HTTP header that tells the browser that all future connections to a particular site should use HTTPS. To implement this, add the [header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) to your website. Some web frameworks (like [Django](https://docs.djangoproject.com/en/2.0/topics/security/#ssl-https)) have this built-in. This would have prevented the [MyEtherWallet DNS Hack](https://bitcoinmagazine.com/articles/popular-ether-wallet-mew-hijacked-dns-attack). ## Storing sensitive data Ideally, you don’t have to store much sensitive data. If you must, be sure to tread carefully. There are many strategies to store sensitive data: - Ensure sensitive data is encrypted using a proven cipher like AES-256 and stored separately from application data. Always pick up AEAD mode. - Any communication between the application server and secret server should be in a private network and/or authenticated via HMAC. Your cipher strategy will change based on whether you will be sending the ciphertext over the wire multiple times. - Back up any encryption keys you may use offline and store them only in-memory in your app. - Consult a good cryptographer and read up on best practices. Look into the documentation of your favorite web framework. - Rolling your own crypto is a bad idea. Always use tried and tested libraries such as [NaCI](). ## Monitoring Attackers often need to spend time exploring your website for unexpected or overlooked behavior. Examining logs defensively can help you catch onto what they’re trying to achieve. You can at least block their IP or automate blocking based on suspicious behavior. It’s also worth setting up an error reporting (like [Sentry](https://sentry.io/welcome)). Often, people trigger strange bugs when trying to hack things. ## Authentication weaknesses You must build your authentication securely if you have logins for users. The best way to do this is to use something off the shelf. Both Ruby on Rails and Django have robust, built-in authentication schemes. Many JSON web token implementations are poorly done, so ensure the library you use is audited. Hash passwords with a time-tested scheme are good. And Balloon Hashing is also worth looking into. We strongly prefer 2FA and require U2F or [TOTP](https://tools.ietf.org/html/rfc6238) 2FA for sensitive actions. 2FA is important as email accounts are usually not very secure. Having a second factor of authentication ensures that users who accidentally stay logged on or have their password guessed are still protected. Finally, require strong passwords. Common and short passwords can be brute-forced. Dropbox has a great [open-source tool](https://blogs.dropbox.com/tech/2012/04/zxcvbn-realistic-password-strength-estimation) that gauges password strength fairly quickly, making it usable for user interactions. ## Denial of service attacks (DOS) DOS attacks are usually accomplished by overloading your web servers with traffic. To mitigate this risk, rate limit traffic from IPs and browser fingerprints. Sometimes people will use proxies to bypass IP rate-limiting. In the end, malicious actors can always find ways to spoof their identity, so the surest way to block DOS attacks is to implement proof of work checks in your client or use a managed service like [Cloudflare](https://www.cloudflare.com/ddos). ## Lockdown unused ports Attackers will often scan your ports to see if you were negligent and left any open. Services like Heroku do this for you- [read about how to enable this on AWS](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/authorizing-access-to-an-instance.html). ## Phishing and social engineering Phishing attacks will thwart any well-formed security infrastructure. Have clear policies published on your website and articulate them to users when they sign up (you will never ask for their password, etc.). Sign messages to your users and prompt users to check the website's domain they are on. ## Scan your website and libraries for vulnerabilities Use a tool like [Snyk](https://snyk.io) to scan your third-party client libraries for vulnerabilities. Make sure to keep your third-party libraries up to date. Often, upgrades are triggered by security exploits. You can use [Mozilla Observatory](https://observatory.mozilla.org) to check your HTTP security as well. ## Cross-Site Request Forgery Protection (CSRF), SQL injections Most modern web and mobile frameworks handle both CSRF protection and SQL injections. Ensure CSRF protection is enabled and that you are using a database ORM instead of running raw SQL based on user input. For example, see what [Ruby on Rails documentation](http://guides.rubyonrails.org/security.html#sql-injection) says about SQL injections. --- ## Threat Modeling Readiness How-To Guide This Threat Modeling How-To Guide is intended for Stellar ecosystem partners to fund an audit through the Audit Bank. This document is aimed at developers who are ready to audit their application. --- ## STRIDE Threat Model Template ## What are we working on? **Directions**: fill out the information below to the best of your ability. _**Input text**: high level description of the project and what is being designed_ _Add at least one visual dataflow diagram that shows process flows between different entities in the system_ :::note If additional information is needed to understand the flow, include additional diagrams or information here. The goal of this section is to capture the documentation needed to accomplish building a successful threat model. If it is discovered later that additional information is required, ensure the “What are we working on?” section is updated with the latest information. ::: ## What can go wrong? ### STRIDE reminders | Mnemonic Threat | Definition | Question | | --- | --- | --- | | **S**poofing | The ability to impersonate another user or system component to gain unauthorized access. | Is the user who they say they are? | | **T**ampering | Unauthorized alteration of data or code. | Has the data or code been modified in some way? | | **R**epudiation | The ability for a system or user to deny having taken a certain action. | Is there enough data to “prove” the user took the action if they were to deny it? | | **I**nformation Disclosure | The over-sharing of data expected to be kept private. | Is there anywhere where excessive data is being shared or controls are not properly in place to protect private information? | | **D**enial of Service | The ability for an attacker to negatively affect the availability of a system. | Can someone, without authorization, impact the availability of the service or business? | | **E**levation of Privilege | The ability for an attacker to gain additional privileges and roles beyond what they initially were granted. | Are there ways for a user, without proper authentication (verifying identity) and authorization (verifying permission) to gain access to additional privileges, either through standard (normally legitimate) or illegitimate means? | _To complete the STRIDE table below, apply the questions above to each of the interactions in the data flow diagram between different entities. There should be at least one issue identified for each of S, T, R, I, D, and E. Make sure to uniquely identify them, so they are easier to track in the next step._ ### Threat table | Threat | Issues | | --- | --- | | **S**poofing | **Spoof.1** - _Description of at least one spoofing issue_ | | **T**ampering | **Tamper.1** - _Description of at least one tampering issue_ | | **R**epudiation | **Repudiate.1** - _Description of at least one repudiation issue_ | | **I**nformation Disclosure | **Info.1** - _Description of at least one information disclosure issue_ | | **D**enial of Service | **DoS.1** - _Description of at least one denial of service issue_ | | **E**levation of Privilege | **Elevation.1** - _Description of at least one elevation of privilege issue_ | ## What are we going to do about it? _To complete the table below, design or identify mitigations that can address each of the issues identified. Alternatively, the risk may be documented and corporately accepted by the business._ | Threat | Issues | | --- | --- | | **S**poofing | **Spoof.1.R.1** - _Description of the first remediation for this issue. Oftentimes, there may be more than one singular remediation required to fully address an issue. Ensure sufficient detail is provided to fully mitigate or remediate the issue._ **Spoof.1.R.2** - _Second remediation step for the first Spoofing issue, etc._ | | **T**ampering | **Tamper.1.R.1** - _Description of the first remediation for this issue. Oftentimes, there may be more than one singular remediation required to fully address an issue. Ensure sufficient detail is provided to fully mitigate or remediate the issue._ | | **R**epudiation | **Repudiate.1.R.1** - _Description of the first remediation for this issue. Oftentimes, there may be more than one singular remediation required to fully address an issue. Ensure sufficient detail is provided to fully mitigate or remediate the issue._ | | **I**nformation Disclosure | **Info.1.R.1** - _Description of the first remediation for this issue. Oftentimes, there may be more than one singular remediation required to fully address an issue. Ensure sufficient detail is provided to fully mitigate or remediate the issue._ | | **D**enial of Service | **DoS.1.R.1** - _Description of the first remediation for this issue. Oftentimes, there may be more than one singular remediation required to fully address an issue. Ensure sufficient detail is provided to fully mitigate or remediate the issue._ | | **E**levation of Privilege | **Elevation.1.R.1** - _Description of the first remediation for this issue. Oftentimes, there may be more than one singular remediation required to fully address an issue. Ensure sufficient detail is provided to fully mitigate or remediate the issue._ | ## Did we do a good job? - Has the data flow diagram been referenced since it was created? - Did the STRIDE model uncover any new design issues or concerns that had not been previously addressed or thought of? - Did the treatments identified in the “What are we going to do about it” section adequately address the issues identified? - Have additional issues been found after the threat model? - Any additional thoughts or insights on the threat modeling process that could help improve it next time? --- ## Pizza Restaurant Example STRIDE Threat Model ## What are we working on? The project being reviewed is an online pizza restaurant. To help make the experience more engaging, it includes a social component where people can see what pizzas have been ordered and customers can track their pizza through the creation process. For simplicity, only credit cards are taken for payment, only one payment processor is used, and all pizza is picked up from the store (no delivery). The typical “happy path” flow includes: 1. The customer builds their pizza on the website from a list of ingredients. 2. When finished building, the website calculates the price of the pizza by querying the database. 3. The customer enters their payment information on the website and the website connects to the credit card processor to process the payment. 4. The pizza order is added to the database. 5. For the social component, once the order is in the database, it is also displayed on the starting page of the webpage with the user’s username. 6. Pizza makers in the pizza shop check the database and begin building the pizza through various stations, including dough, sauce, cheese, toppings, baking, and boxing. One pizza worker is at each station. 7. At each step, the pizza maker updates the database with current progress. As the database is updated, the pizza tracker on the website is updated, showing current progress and who has completed that step. 8. Customer picks up their pizza once it is ready. ![Threat Modeling](/assets/security/threat-modeling.png) ## What can go wrong? ### STRIDE reminders | Mnemonic Threat | Definition | Question | | --- | --- | --- | | **S**poofing | The ability to impersonate another user or system component to gain unauthorized access. | Is the user who they say they are? | | **T**ampering | Unauthorized alteration of data or code. | Has the data or code been modified in some way? | | **R**epudiation | The ability for a system or user to deny having taken a certain action. | Is there enough data to “prove” the user took the action if they were to deny it? | | **I**nformation Disclosure | The over-sharing of data expected to be kept private. | Is there anywhere where excessive data is being shared or controls are not properly in place to protect private information? | | **D**enial of Service | The ability for an attacker to negatively affect the availability of a system. | Can someone, without authorization, impact the availability of the service or business? | | **E**levation of Privilege | The ability for an attacker to gain additional privileges and roles beyond what they initially were granted. | Are there ways for a user, without proper authentication (verifying identity) and authorization (verifying permission) to gain access to additional privileges, either through standard (normally legitimate) or illegitimate means? | ### Threat table | Threat | Issues | | --- | --- | | **S**poofing | **Spoof.1** - (Step 1) An attacker could submit an order as another user. **Spoof.2** - (Step 3) If credit card information is stored in the user profile, a spoofed pizza order could be submitted automatically to the CC processor, making another user pay for the pizza order. **Spoof.3** - (Step 7) An attacker “blasts” erroneous pizza updates at the website, eroding trust in the data and process. | | **T**ampering | **Tamper.1** - (Step 2) An attacker submits their own query injecting SQL into the DB query. **Tamper.2** - (Step 2) An attacker modifies the values returned from their pizza price query and gives themselves a discount. **Tamper.3** - (Step 4) An attacker builds and quotes a “cheap” pizza, but after payment, submits a much more expensive order. **Tamper.4** - (Step 6) Pizza Maker modifies an order at someone else’s station to make them look incompetent. | | **R**epudiation | **Repudiate.1** - (Step 3) An attacker places an order but refutes the CC charge. **Repudiate.2** - (Step 4) An attacker submits an order for another user directly to the DB without ever interfacing with the rest of the system. **Repudiate.3** - (Step 4) Multiple valid orders can be submitted with the same CC processing token. | | **I**nformation Disclosure | **Info.1** - (Step 4) An attacker farms sequential order IDs to interact with them. **Info.2** - (Step 2) An attacker is able to query more information and is able to access stored CC info. **Info.3** - (Step 4) - An attacker is able to farm sequential coupon codes. **Info.4** - (Step 5) - When the website is updated with the newest pizza order, behind the scenes, it returns the entire pizza order but only displays the pizza. An attacker can farm PII from the pizza order page. | | **D**enial of Service | **DoS.1** - An attacker targets the website to take it down. **DoS.2** - (Step 1) An attacker identifies and repeatedly calls the API endpoint to create a new user. **DoS.3** - (Step 4) An attacker bulk drops orders to lock up the ordering process. **DoS.4** - (Step 2) An attacker bulk drops orders to lock up the pricing process. | | **E**levation of Privilege | **Elevation.1** - An attacker is able to trick the pizza website into displaying the admin interface on the website. **Elevation.2** - (Step 6) A pizza maker uses his manager’s access to delete pizza orders so he doesn’t have to make them. | ## What are we going to do about it? | Threat | Issues | | --- | --- | | **S**poofing | **Spoof.1** - (Step 1) An attacker could submit an order as another user. **S1R1** - Ensure proper authentication is required before a website visitor is authorized to submit an order. Ensure the location information from the user profile is used for order information. **Spoof.2** - (Step 3) If credit card information is stored in the user profile, a spoofed pizza order could be submitted automatically to the CC processor, making another user pay for the pizza order. **S2R1** - Ensure all data required to process a credit card payment is not stored together in the customer profile. All data should be encrypted at rest and in transit. One way of accomplishing the end goal would be to use CCV # as only ephemeral data and capture it each time the credit card is used. CCV should never be stored. **Spoof.3** - (Step 7) An attacker “blasts” erroneous pizza updates at the website, eroding trust in the data and process. **S3R1** - Ensure pizza states updates are performed via a PULL style where the website queries a server it trusts for pizza status updates. **S3R2** - Alternatively, allow PUSH style updates from the pizza server, but only expose that API endpoint internally. | | **T**ampering | **Tamper.1** - (Step 2) An attacker submits their own query injecting SQL into the DB query. **T1R1** - Ensure all queries for all database lookups that utilize untrusted user data are performed with prepared statements and all input is properly encoded. **Tamper.2** - (Step 2) An attacker modifies the values returned from their pizza price query and gives themselves a discount. **T2R1** - The pricing info coming back from the database server should only ever be used for display purposes. The price should be calculated on the server separately when the charge is actually applied and the financial tracking information should only trust the specific pizza order that is customized. All calculations for price should be performed separately on the back-end. **Tamper.3** - (Step 4) An attacker builds and quotes a “cheap” pizza, but after payment, submits a much more expensive order. **T3R1** - The pizza order that was priced should be placed directly from the database server to the CC processor. This prevents quoting one pizza but ordering another. **Tamper.4** - (Step 6) Pizza Maker modifies an order at someone else’s station to make them look incompetent. **T4R1** - This is an “insider threat” type of attack. Each pizza maker should have a way of authenticating to their station, so all actions are trackable and auditable. One approach could be to have each pizza maker have a keycard that authenticates them to their pizza station. The keycard should be connected to the pizza maker, such that if they leave, the keycard is automatically removed. The keycard must be present for changes to be submitted to the database server. | | **R**epudiation | **Repudiate.1** - (Step 3) An attacker places an order but refutes the CC charge. **R1R1** - Ensure that when a new user profile is created, a public/private key pair is generated. These should be used to sign and validate messages between the user and the server. When a pizza order is submitted to the server, the server calculates the hash of the order along with a secret salt and sends the salted hash back to the originating user. The originating user signs the salted hash with their private key and submits the signed bundle back to the server. The server should store the order, the secret salt (through persistence in the process environment), the salted hash, and the signed bundle. If a user refutes the order, the server can simply verify that the user’s public key properly decrypts the signed bundle. **Repudiate.2** - (Step 4) An attacker submits an order for another user directly to the DB without ever interfacing with the rest of the system. **R2R1** - Ensure order submissions require an active JWT session token. Ensure the session timeout for a session reflects the actual usage patterns expected (it doesn’t take 7 hours to order a pizza). **R2R2** - Ensure that a pizza order has been properly quoted by recalculating the pizza price at submission time. **Repudiate.3** - (Step 4) Multiple valid orders can be submitted with the same CC processing token. **R3R1** - Ensure CC processing tokens are sufficiently short-lived. **R3R2** - Work with CC processor to invalidate the token after a transaction is confirmed. | | **I**nformation Disclosure | **Info.1** - (Step 4) An attacker farms sequential order IDs to interact with them. **I1R1** - Ensure all IDs (order IDs, user IDs, transaction IDs, etc) use non-sequential, difficult to guess GUIDs. **Info.2** - (Step 2) An attacker is able to query more information and is able to access stored CC info. **I2R1** - Utilize a filtering framework like CASL to limit what information is available through queries. **I2R2** - Ensure CC info is stored in a separate context and not easily reachable (network segmented to only allow very specific known actions to reach the CC environment). **I2R3** - Ensure tokenization is used to obfuscate the CC info. One example could look like the following: 1. User submits their order and the signing process takes place. 2. The website prompts the user for their CVV and submits it to the database server. 3. The database server then submits the order price they have confirmed along with a CVV and a CC number identifier to the API endpoint to submit a payment request. 4. The payment request API call submits a payment request to the CC processor. 5. When success is returned, the API call resolves to being paid. This method never exposes the CC to the database or web server. **Info.3** - (Step 4) - An attacker is able to farm sequential coupon codes. **I3R1** - Ensure coupon codes are not sequential. **I3R2** - Alternatively, if availability of the coupon code is more important (i.e., it is being used to drive traffic and there is minimal concern with the secrecy), then be sure to build in the discount price into cost projection models. Additionally, consider setting a maximum number of coupons to be used to provide an upper bound for cost in the event of abuse. **Info.4** - (Step 5) - When the website is updated with the newest pizza order, behind the scenes, it returns the entire pizza order but only displays the pizza. An attacker can farm PII from the pizza order page. **I4R1** - Ensure that the query used to present the data on the pizza order page only returns the data strictly necessary to accomplish the update to the status page in context. If address, email address, or CC number is not necessary to accomplish the update for a pizza status, do not include those fields in the query. | | **D**enial of Service | **DoS.1** - An attacker targets the website to take it down. **D1R1** - Ensure the website follows some mitigation strategy for DDoS protection. This could include CloudFlare DDoS, Cloudflare WAF, F5 DDoS Mitigation, or others. **D1R2** - As an alternative or in addition, rate limiting should be applied at the server level to limit the capability of imposing a DDoS attack. **DoS.2** - (Step 1) An attacker identifies and repeatedly calls the API endpoint to create a new user. **D2R1** - Set up rate limiting for sensitive API calls (like this one). Ensure CAPTCHA or other bot preventions are in place to make it more difficult. **DoS.3** - (Step 4) An attacker bulk drops orders to lock up the ordering process. **D3R1** - Set up Cloudflare DDoS and Cloudflare WAF. **D3R2** - Set up rate limiting for the API call to submit orders. **D3R3** - Set up X-Forwarded-For headers on the server to enable rate limiting by origin IP. **D3R4** - Set up rate limiting by IP. **D3R5** - Ensure the website can identify large numbers of orders from a single user and limit it to an appropriate value. **DoS.4** - (Step 2) An attacker bulk drops orders to lock up the pricing process. **D4R1** - The same mitigations apply here as for DoS.3. **D4R2** - Additionally, ensure the pricing system also identifies large number of price lookups or price re-lookups originating from the same IPs. | | **E**levation of Privilege | **Elevation.1** - An attacker is able to trick the pizza website into displaying the admin interface on the website. **E1R1** - Ensure the administrative interface for the website is presented at a different URI. **E1R2** - Ensure the invocation of sensitive or administrative tasks is done by administrators as identified from a lookup of the user’s session on the server. **Elevation.2** - (Step 6) A pizza maker uses his manager’s access to delete pizza orders so he doesn’t have to make them. **E2R1** - Ensure the keycard that grants access is not left unattended without the manager present. | ## Did we do a good job? - Has the data flow diagram been referenced since it was created? - Yes, it was invaluable in building the threat profile. - Did the STRIDE model uncover any new design issues or concerns that had not been previously addressed or thought of? - Yes. We realized we needed to change the pizza station authentication scheme. Additionally, we reframed the queries for the pizza order social display to only deliver the needed information. - Did the treatments identified in the “What are we going to do about it” section adequately address the issues identified? - Yes. We are continuing to monitor these in the event additional mitigations are needed. - Have additional issues been found after the threat model? - None yet. As new features are added, the threat model will be updated to reflect the new additions. - Any additional thoughts or insights on the threat modeling process that could help improve it next time? - None. --- ## Threat Modeling Readiness This document explains the threat modeling processes regarded as industry best practice and now required as a precondition for audits requested from the SDF Audit Bank. **Background:** Completion of the 15-minute YouTube series “World’s Shortest Threat Modeling Course” [here](https://www.youtube.com/playlist?list=PLCVhBqLDKoOOZqKt74QI4pbDUnXSQo0nf). **Background:** At a minimum, it will be helpful to have a procedural design or data flow diagram that documents, in sufficient detail, the data flow through the system to be audited. ### What is threat modeling? Threat modeling provides a structured way of thinking critically about the data flows, trust boundaries, and internal processes for software. Effectively, threat modeling provides a guided way to think through the security implications of design decisions and can help uncover previously unidentified or unseen security threats or design issues. ### Why is threat modeling important? Threat modeling provides a framework and space to think critically about the software being designed or developed. By fully taking advantage of the benefits threat modeling provides, security threats and design issues can be identified earlier in the development lifecycle, resulting in cleaner and more secure code being run when users begin interacting with it. By identifying issues earlier in the development process, cost for re-work is reduced, making for more efficient use of resources. Ecosystem developers who have properly addressed insights gleaned from threat modeling will benefit from more in-depth and valuable audit results. --- ## Threat Modeling How-To Guide ## How do we model threats? Threat modeling, at its heart, is really about asking four simple questions: 1. What are we working on? 2. What can go wrong? 3. What are we going to do about it? 4. Did we do a good job? ### How to answer: “What are we working on?” The crux of this question is documentation. More specifically, this question is guiding the development team to start thinking about (and documenting) the core processes being developed in terms of processes, data flows, data storage, areas of control, and identifying places where trust assumptions are being made between and among systems and processes. The answer to “What are we working on?” should include a verbal description of the use case for the application and a data-flow diagram showing: - External entities - Entities outside the control of the application or company. - Processes - Code and systems under the control of the application or company. - Data flows - Representation of data moving from one system to another. - Data storage - Representation of data being stored somewhere (this can include Stellar smart contract (Soroban) storage, online databases, fileservers, etc). - Trust boundaries - Outline and group together areas where interactions between systems make or include trust assumptions. For example, in a dataflow where a user submits data on a webpage and it gets processed on the backend, a trust boundary should enclose the backend process, since data can be submitted to the backend outside any filtering controls on the front-end. ### How to answer: “What can go wrong?” Understanding the use case and the data flow from “What are we working on?” can greatly inform thinking around what can go wrong. However, to ensure consistency and thoroughness for the review, it is useful to follow a framework that helps guide the thinking around threats and what can go wrong. Stellar Development Foundation (SDF) follows the STRIDE model. Described below, the STRIDE model helps to provide the consistent framework to ensure high-quality threat models. For each of the S, T, R, I, D, and E acronyms in the table below, ensure, at a minimum, at least one issue is identified. Oftentimes, there will be multiple issues for each threat. It is useful to identify each issue with an identifier indicating both the threat it is connected with as well as its number. Since there can be multiple spoofing issues, for example, it can be helpful to identify the issues as Spoof.1, Spoof.2, and so on. For especially complex data flows, it can even be beneficial to apply a full stride model on a subprocess in the flow. Ensure that your identification can differentiate between these as well. | | Definition | Question to Ask | Example | | --- | --- | --- | --- | | **S**poofing | Spoofing is the ability for an attacker to pretend to be someone they are not, often taking advantage of gaps in end-user verification in downstream systems. | Could the action being taken be induced by someone other than the person believed to have taken the action? | Calling and ordering pizza pretending to be someone else. | | **T**ampering | Tampering is the ability for an attacker to modify data being submitted or sent to have a different effect than that anticipated. | Could the request have been modified in some way to take an action other than originally intended? | A software bug changes all selected pizza toppings to “cheese.” | | **R**epudiation | Repudiation is the ability for a user to claim they did not take the action that was taken. | Can the user “refute” the action, claiming they did not take it? | The user claims they never ordered the pizza they ordered. | | **I**nformation Disclosure | Information Disclosure is the over-sharing of data that is expected to be kept private. | Are there areas where more information is being shared or limited information is being shared with more people than is strictly necessary? | Pizza company displays the most recent pizza order, name, and telephone number on their webpage. | | **D**enial of Service | Denial of Service is the ability for an attacker to negatively affect the availability of a system. | Is there a part of the application that is susceptible to being overwhelmed or made entirely unavailable due to overwhelming demand? | Pizza company has a single phone line that only returns busy when called while in use. | | **E**levation of Privilege | Elevation of Privilege refers to the ability for an attacker to gain additional privileges and roles beyond what they initially were granted, either through legitimate or illegitimate means. | Can someone gain additional privileges without proper authentication and authorization? | Pizza maker signs into the computer with his manager’s keycard and deletes the order so he doesn’t have to make the pizza. | ### How to answer: “What are we going to do about it?” For each issue identified in the STRIDE model, think through the ways that issue can happen and identify a “treatment” or way of addressing the issue. Answers here should be detailed in how the issue will address the threat identified. This could include code blocks detailing changes or verbal descriptions of how the issue will be addressed. ### How to answer: “Did we do a good job?” The point of this section is to understand if the analysis done during the other sections was sufficiently deep and broad as to provide value. Some questions to ask and answer here might include: - **Has the data flow diagram been referenced since it was created?** - If so, congratulations, you have created a reusable tool that can continue adding value. - If not, there may need to be more detail or better organization. The data flow diagram should be a useful tool that is regularly referenced. - **Did the STRIDE model uncover any new design issues or concerns that had not been previously addressed or thought of?** - If so, excellent! The STRIDE model should help to guide thinking towards security issues that may not have been thought of during the initial design phase. - If not, ensure you have at least one issue for each threat. Pay special attention to trust boundaries and processes handling sensitive data or taking particularly powerful actions. - **Did the treatments identified in the “What are we going to do about it” section adequately address the issues identified?** - If so, excellent! The threat modeling exercise has helped to build a more secure application. - If not, you may want to provide greater detail and thought around how to respond to identified issues. - **Have additional issues been found after the threat model?** - If so, excellent! The threat model is a living tool that should be redone any time significant changes have taken place to the architecture. During the design phase for these changes or additions, make sure to analyze the system with the newly designed changes to identify new potential issues in the new changes and in the interface to the old system. - If not, continue to refine your threat model and design as new information is learned about the system. The threat model is a living tool that can continue providing benefits as new information is learned about the system. --- ## Build, Test, and Deploy Smart Contracts on Stellar: Tools & Best Practices # Write Smart Contracts This section will walk you through how to get set up to write smart contracts on Stellar, plus an introduction to testing, storing data, and deploying your contracts. It also provides an array of example contracts for use. --- ## Example Smart Contracts: Learn, Build, Test & Deploy on the Network # Example Contracts The Stellar team has put together a large collection of [example contracts] to demonstrate use of smart contracts on Stellar. For many of these example contracts, we've written an accompanying tutorial that will walk you through the example contract and describe a bit more about its design. The examples listed below are provided in a sequential manner. The first listed example contracts create a solid foundation of concepts that will be required during the later examples. While you are absolutely free to choose, read, and use any of the example contracts you like, please keep in mind that the order you see is intentional. [example contracts]: https://github.com/stellar/soroban-examples --- ## Allocator {`Use the allocator feature to emulate heap memory in a smart contract.`} The [allocator example] demonstrates how to utilize the allocator feature when writing a contract. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [allocator example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/alloc The `soroban-sdk` crate provides a lightweight bump-pointer allocator which can be used to emulate heap memory allocation in a Wasm smart contract. ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, navigate to the `alloc` directory, and use `cargo test`. ```sh cd alloc cargo test ``` You should see the output: ``` running 1 test test test::test ... ok ``` ## Dependencies This example depends on the `alloc` feature in `soroban-sdk`. To include it, add "alloc" to the "features" list of `soroban-sdk` in the `Cargo.toml` file: ```rust title="alloc/Cargo.toml" [dependencies] soroban-sdk = { version = "23.0.1", features = ["alloc"] } [dev_dependencies] soroban-sdk = { version = "23.0.1", features = ["testutils", "alloc"] } ``` ## Code ```rust title="alloc/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl, Env}; extern crate alloc; #[contract] pub struct AllocContract; #[contractimpl] impl AllocContract { /// Allocates a temporary vector holding values (0..count), then computes and returns their sum. pub fn sum(_env: Env, count: u32) -> u32 { let mut v1 = alloc::vec![]; (0..count).for_each(|i| v1.push(i)); let mut sum = 0; for i in v1 { sum += i; } sum } } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/alloc ## How it Works ```rust extern crate alloc; ``` Imports the `alloc` crate, which is required in order to support allocation under `no_std`. See [Contract Rust dialect] for more info about `no_std`. [contract rust dialect]: ../../../learn/fundamentals/contract-development/rust-dialect.mdx ```rust let mut v1 = alloc::vec![]; ``` Creates a contiguous growable array `v1` with contents allocated on the heap memory. :::info The heap memory in the context of a smart contract actually refers to the Wasm linear memory. The `alloc` will use the global allocator provided by the soroban sdk to interact with the linear memory. ::: :::caution Using heap allocated array is typically slow and computationally expensive. Try to avoid it and instead use a fixed-sized array or `soroban_sdk::vec!` whenever possible. This is especially the case for a large-size array. Whenever the array size grows beyond the current linear memory size, which is multiple of the page size (64KB), the [`wasm32::memory_grow`](https://doc.rust-lang.org/core/arch/wasm32/fn.memory_grow.html) is invoked to grow the linear memory by more pages as necessary, which is very computationally expensive. ::: The remaining code pushes values `(0..count)` to `v1`, then computes and returns their sum. This is the simplest example to illustrate how to use the allocator. --- ## Batched Atomic Swaps Swap a token pair among groups of authorized users. The [atomic swap batching example] swaps a pair of tokens between the two groups of users that authorized the `swap` operation from the [Atomic Swap] example. This contract basically batches the multiple swaps while following some simple rules to match the swap participants. Follow the comments in the code for more information. [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [atomic swap]: atomic-swap.mdx [atomic swap batching example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/atomic_multiswap [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] --- ## Atomic Swap Swap tokens atomically between authorized users. The [atomic swap example] swaps two tokens between two authorized parties atomically while following the limits they set. This is example demonstrates advanced usage of Soroban auth framework and assumes the reader is familiar with the [auth example](../example-contracts/auth.mdx) and with Soroban token usage. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [atomic swap example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/atomic_swap ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example use `cargo test`. ```sh cargo test -p soroban-atomic-swap-contract ``` You should see the output: ``` running 1 test test test::test_atomic_swap ... ok ``` ## Code ```rust title="atomic_swap/src/lib.rs" #[contract] pub struct AtomicSwapContract; #[contractimpl] impl AtomicSwapContract { // Swap token A for token B atomically. Settle for the minimum requested price // for each party (this is an arbitrary choice; both parties could have // received the full amount as well). pub fn swap( env: Env, a: Address, b: Address, token_a: Address, token_b: Address, amount_a: i128, min_b_for_a: i128, amount_b: i128, min_a_for_b: i128, ) { // Verify preconditions on the minimum price for both parties. if amount_b < min_b_for_a { panic!("not enough token B for token A"); } if amount_a < min_a_for_b { panic!("not enough token A for token B"); } // Require authorization for a subset of arguments specific to a party. // Notice, that arguments are symmetric - there is no difference between // `a` and `b` in the call and hence their signatures can be used // either for `a` or for `b` role. a.require_auth_for_args( (token_a.clone(), token_b.clone(), amount_a, min_b_for_a).into_val(&env), ); b.require_auth_for_args( (token_b.clone(), token_a.clone(), amount_b, min_a_for_b).into_val(&env), ); // Perform the swap by moving tokens from a to b and from b to a. move_token(&env, &token_a, &a, &b, amount_a, min_a_for_b); move_token(&env, &token_b, &b, &a, amount_b, min_b_for_a); } } fn move_token( env: &Env, token: &Address, from: &Address, to: &Address, max_spend_amount: i128, transfer_amount: i128, ) { let token = token::Client::new(env, token); let contract_address = env.current_contract_address(); // This call needs to be authorized by `from` address. It transfers the // maximum spend amount to the swap contract's address in order to decouple // the signature from `to` address (so that parties don't need to know each // other). token.transfer(from, &contract_address, &max_spend_amount); // Transfer the necessary amount to `to`. token.transfer(&contract_address, to, &transfer_amount); // Refund the remaining balance to `from`. token.transfer( &contract_address, from, &(max_spend_amount - transfer_amount), ); } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/atomic_swap ## How it Works The example contract requires two `Address`-es to authorize their parts of the swap operation: one `Address` wants to sell a given amount of token A for token B at a given price and another `Address` wants to sell token B for token A at a given price. The contract swaps the tokens atomically, but only if the requested minimum price is respected for both parties. Open the `atomic_swap/src/lib.rs` file or see the code above to follow along. ### Swap authorization ```rust ... a.require_auth_for_args( (token_a.clone(), token_b.clone(), amount_a, min_b_for_a).into_val(&env), ); b.require_auth_for_args( (token_b.clone(), token_a.clone(), amount_b, min_a_for_b).into_val(&env), ); ... ``` Authorization of `swap` function leverages `require_auth_for_args` Soroban host function. Both `a` and `b` need to authorize symmetric arguments: token they sell, token they buy, amount of token they sell, minimum amount of token they want to receive. This means that `a` and `b` can be freely exchanged in the invocation arguments (as long as the respective arguments are changed too). ### Moving the tokens ```rust ... // Perform the swap via two token transfers. move_token(&env, token_a, &a, &b, amount_a, min_a_for_b); move_token(&env, token_b, &b, &a, amount_b, min_b_for_a); ... fn move_token( env: &Env, token: &Address, from: &Address, to: &Address, max_spend_amount: i128, transfer_amount: i128, ) { let token = token::Client::new(env, token); let contract_address = env.current_contract_address(); // This call needs to be authorized by `from` address. It transfers the // maximum spend amount to the swap contract's address in order to decouple // the signature from `to` address (so that parties don't need to know each // other). token.transfer(from, &contract_address, &max_spend_amount); // Transfer the necessary amount to `to`. token.transfer(&contract_address, to, &transfer_amount); // Refund the remaining balance to `from`. token.transfer( &contract_address, from, &(&max_spend_amount - &transfer_amount), ); } ``` The swap itself is implemented via two token moves: from `a` to `b` and from `b` to `a`. The token move is implemented via allowance: the users don't need to know each other in order to perform the swap, and instead they authorize the swap contract to spend the necessary amount of token on their behalf via `transfer`. Soroban auth framework makes sure that the `transfer` signatures would have the proper context, and they won't be usable outside the `swap` contract invocation. ### Tests Open the [`atomic_swap/src/test.rs`] file to follow along. [`atomic_swap/src/test.rs`]: https://github.com/stellar/soroban-examples/tree/v23.0.0/atomic_swap/src/test.rs Refer to another examples for the general information on the test setup. The interesting part for this example is verification of `swap` authorization: ```rust contract.swap( &a, &b, &token_a.address, &token_b.address, &1000, &4500, &5000, &950, ); assert_eq!( env.auths(), std::vec![ ( a.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( contract.address.clone(), symbol_short!("swap"), ( token_a.address.clone(), token_b.address.clone(), 1000_i128, 4500_i128 ) .into_val(&env), )), sub_invocations: std::vec![AuthorizedInvocation { function: AuthorizedFunction::Contract(( token_a.address.clone(), symbol_short!("transfer"), (a.clone(), contract.address.clone(), 1000_i128,).into_val(&env), )), sub_invocations: std::vec![] }] } ), ( b.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( contract.address.clone(), symbol_short!("swap"), ( token_b.address.clone(), token_a.address.clone(), 5000_i128, 950_i128 ) .into_val(&env), )), sub_invocations: std::vec![AuthorizedInvocation { function: AuthorizedFunction::Contract(( token_b.address.clone(), symbol_short!("transfer"), (b.clone(), contract.address.clone(), 5000_i128,).into_val(&env), )), sub_invocations: std::vec![] }] } ), ] ); ``` `env.auths()` returns all the authorizations. In the case of `swap` four authorizations are expected. Two for each address authorizing, because each address authorizes not only the swap, but the `approve` all on the token being sent. --- ## Auth Implement authentication and authorization. The [auth example] demonstrates how to implement authentication and authorization using the Soroban Host-managed auth framework. This example is an extension of the [storing data example]. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [storing data example]: ../getting-started/storing-data.mdx [auth example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/auth ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ``` git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, navigate to the `auth` directory, and use `cargo test`. ``` cd auth cargo test ``` You should see the output: ``` running 1 test test test::test ... ok ``` ## Code ```rust title="auth/src/lib.rs" #[contracttype] pub enum DataKey { Counter(Address), } #[contract] pub struct IncrementContract; #[contractimpl] impl IncrementContract { /// Increment increments a counter for the user, and returns the value. pub fn increment(env: Env, user: Address, value: u32) -> u32 { // Requires `user` to have authorized call of the `increment` of this // contract with all the arguments passed to `increment`, i.e. `user` // and `value`. This will panic if auth fails for any reason. // When this is called, Soroban host performs the necessary // authentication, manages replay prevention and enforces the user's // authorization policies. // The contracts normally shouldn't worry about these details and just // write code in generic fashion using `Address` and `require_auth` (or // `require_auth_for_args`). user.require_auth(); // This call is equilvalent to the above: // user.require_auth_for_args((&user, value).into_val(&env)); // The following has less arguments but is equivalent in authorization // scope to the above calls (the user address doesn't have to be // included in args as it's guaranteed to be authenticated). // user.require_auth_for_args((value,).into_val(&env)); // Construct a key for the data being stored. Use an enum to set the // contract up well for adding other types of data to be stored. let key = DataKey::Counter(user.clone()); // Get the current count for the invoker. let mut count: u32 = env.storage().persistent().get(&key).unwrap_or_default(); // Increment the count. count += value; // Save the count. env.storage().persistent().set(&key, &count); // Return the count to the caller. count } } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/auth ## How it Works The example contract stores a per-`Address` counter that can only be incremented by the owner of that `Address`. Open the `auth/src/lib.rs` file or see the code above to follow along. ### `Address` ```rust #[contracttype] pub enum DataKey { Counter(Address), } ``` `Address` is a universal Soroban identifier that may represent a Stellar account, a contract, or a contract account (a contract that defines a custom authentication scheme and authorization policies). Contracts don't need to distinguish between these internal representations though. `Address` can be used any time some network identity needs to be represented, like to distinguish between counters for different users in this example. :::tip[Enum keys like `DataKey` are useful for organizing contract storage.] Different enum values create different key 'namespaces'. In the example the counter for each address is stored against `DataKey::Counter(Address)`. If the contract needs to start storing other types of data, it can do so by adding additional variants to the enum. ::: ### `require_auth` ```rust impl IncrementContract { pub fn increment(env: Env, user: Address, value: u32) -> u32 { user.require_auth(); ``` The `require_auth` method can be called for any `Address`. Semantically `user.require_auth()` here means 'require `user` to have authorized calling `increment` function of the current `IncrementContract` instance with the current call arguments, i.e. the current `user` and `value` argument values'. In simpler terms, this ensures that the `user` has allowed incrementing their counter value and nobody else can increment it. When using `require_auth` the contract implementation doesn't need to worry about the signatures, authentication, and replay prevention. All these features are implemented by the Soroban host and happen automatically as long as the `Address` type is used. `Address` has another method called `require_auth_for_args`. It works in the same fashion as `require_auth`, but allows customizing the arguments that need to be authorized. Note though, this should be used with care to ensure that there is a deterministic mapping between the contract invocation arguments and the `require_auth_for_args` arguments. The following two calls are functionally equivalent to `user.require_auth`: ```rust // Completely equivalent user.require_auth_for_args((&user, value).into_val(&env)); // The following has less arguments but is equivalent in authorization // scope to the above call (the user address doesn't have to be // included in args as it's guaranteed to be authenticated). user.require_auth_for_args((value,).into_val(&env)); ``` ### Tests Open the [`auth/src/test.rs`] file to follow along. [`auth/src/test.rs`]: https://github.com/stellar/soroban-examples/tree/v23.0.0/auth/src/test.rs ```rust title="auth/src/test.rs" #![cfg(test)] extern crate std; #[test] fn test() { let env = Env::default(); env.mock_all_auths(); let contract_id = env.register(IncrementContract, {}); let client = IncrementContractClient::new(&env, &contract_id); let user_1 = Address::generate(&env); let user_2 = Address::generate(&env); assert_eq!(client.increment(&user_1, &5), 5); // Verify that the user indeed had to authorize a call of `increment` with // the expected arguments: assert_eq!( env.auths(), std::vec![( // Address for which authorization check is performed user_1.clone(), // Invocation tree that needs to be authorized AuthorizedInvocation { // Function that is authorized. Can be a contract function or // a host function that requires authorization. function: AuthorizedFunction::Contract(( // Address of the called contract contract_id.clone(), // Name of the called function symbol_short!("increment"), // Arguments used to call `increment` (converted to the env-managed vector via `into_val`) (user_1.clone(), 5_u32).into_val(&env), )), // The contract doesn't call any other contracts that require // authorization, sub_invocations: std::vec![] } )] ); // Do more `increment` calls. It's not necessary to verify authorizations // for every one of them as we don't expect the auth logic to change from // call to call. assert_eq!(client.increment(&user_1, &2), 7); assert_eq!(client.increment(&user_2, &1), 1); assert_eq!(client.increment(&user_1, &3), 10); assert_eq!(client.increment(&user_2, &4), 5); } ``` In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. ```rust let env = Env::default(); ``` The test instructs the environment to mock all auths. All calls to `require_auth` or `require_auth_for_args` will succeed. ```rust env.mock_all_auths(); ``` The contract is registered with the environment using the contract type. ```rust let contract_id = env.register(IncrementContract, {}); ``` All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `IncrementContract`, and the client is named `IncrementContractClient`. ```rust let client = IncrementContractClient::new(&env, &contract_id); ``` Generate `Address`es for two users. Normally the exact value of the `Address` shouldn't matter for testing, so they're simply generated randomly. ```rust let user_1 = Address::random(&env); let user_2 = Address::random(&env); ``` Invoke `increment` function for `user_1`. ```rust assert_eq!(client.increment(&user_1, &5), 5); ``` In order to verify that the `require_auth` call(s) have indeed happened, use `auths` function that returns a vector of tuples containing the authorizations from the most recent contract invocation. ```rust assert_eq!( env.auths(), std::vec![( // Address for which auth is performed user_1.clone(), // Identifier of the called contract contract_id.clone(), // Name of the called function symbol_short!("increment"), // Arguments used to call `increment` (converted to the env-managed vector via `into_val`) (user_1.clone(), 5_u32).into_val(&env) )] ); ``` Invoke the `increment` function several more times for both users. Notice, that the values are tracked separately for each users. ```rust assert_eq!(client.increment(&user_1, &2), 7); assert_eq!(client.increment(&user_2, &1), 1); assert_eq!(client.increment(&user_1, &3), 10); assert_eq!(client.increment(&user_2, &4), 5); ``` ## Build the Contract To build the contract into a `.wasm` file, use the `stellar contract build` command. ```sh stellar contract build ``` The `.wasm` file should be found in the `target` directory after building: ``` target/wasm32v1-none/release/soroban_auth_contract.wasm ``` ## Run the Contract If you have [`stellar-cli`] installed, you can deploy the contract and invoke its functions. But since we are dealing with authorization and signatures, we need to set up some identities to use for testing and get their public keys: ```sh stellar keys generate acc1 --network testnet stellar keys generate acc2 --network testnet stellar keys address acc1 stellar keys address acc2 ``` Example output with two public keys of identities: ``` GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B ``` ### Deploy ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_auth_contract.wasm \ --alias auth_example \ --source-account acc1 \ --network testnet ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/soroban_auth_contract.wasm ` --alias auth_example ` --source-account acc1 ` --network testnet ``` ### Invoke Now the contract itself can be invoked. Notice the `--source-account` must be the identity name matching the address passed to the `--user` argument. This allows `Stellar CLI` to automatically sign the necessary payload for the invocation. ```sh stellar contract invoke \ --id auth_example \ --source-account acc1 \ --network testnet \ -- \ increment \ --user GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU \ --value 2 ``` ```powershell stellar contract invoke ` --id auth_example ` --source-account acc1 ` --network testnet ` -- ` increment ` --user GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU ` --value 2 ``` Run a few more increments for both accounts. ```sh stellar contract invoke \ --id auth_example \ --source-account acc2 \ --network testnet \ -- \ increment \ --user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B \ --value 5 ``` ```sh stellar contract invoke \ --id auth_example \ --source-account acc1 \ --network testnet \ -- \ increment \ --user GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU \ --value 3 ``` ```sh stellar contract invoke \ --id auth_example \ --source-account acc2 \ --network testnet \ -- \ increment \ --user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B \ --value 10 ``` ```powershell stellar contract invoke ` --id auth_example ` --source-account acc2 ` --network testnet ` -- ` increment ` --user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B ` --value 5 ``` ```powershell stellar contract invoke ` --id auth_example ` --source-account acc1 ` --network testnet ` -- ` increment ` --user GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU ` --value 3 ``` ```powershell stellar contract invoke ` --id auth_example ` --source-account acc2 ` --network testnet ` -- ` increment ` --user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B ` --value 10 ``` View the data that has been stored against each user with `stellar contract read`. ```sh stellar contract read --id auth_example --network testnet ``` ``` "[""Counter"",""GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU""]",5 "[""Counter"",""GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B""]",15 ``` It is also possible to preview the authorization payload that is being signed by providing `--auth` flag to the invocation: ```sh stellar contract invoke \ --id auth_example \ --source-account acc2 \ --network testnet \ --auth \ -- \ increment \ --user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B \ --value 123 ``` ```powershell stellar contract invoke ` --id auth_example ` --source-account acc2 ` --network testnet ` --auth ` -- ` increment ` --user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B ` --value 123 ``` ```json Contract auth: [{"address_with_nonce":null,"root_invocation":{"contract_id":"0000000000000000000000000000000000000000000000000000000000000001","function_name":"increment","args":[{"object":{"address":{"account":{"public_key_type_ed25519":"c7bab0288753d58d3e21cc3fa68cd2546b5f78ae6635a6f1b3fe07e03ee846e9"}}}},{"u32":123}],"sub_invocations":[]},"signature_args":[]}] ``` [`stellar-cli`]: ../getting-started/setup.mdx#install-the-stellar-cli ## Further reading [Authorization documentation](../../../learn/fundamentals/contract-development/authorization.mdx) provides more details on how Soroban auth framework works. [Timelock](../example-contracts/timelock.mdx) and [Single Offer](../example-contracts/single-offer-sale.mdx) examples demonstrate authorizing token operations on behalf of the user, which can be extended to any nested contract invocations. [Atomic Swap](../example-contracts/atomic-swap.mdx) example demonstrates multi-party authorization where multiple users sign their parts of the contract invocation. [Simple Account](../example-contracts/simple-account.mdx) example demonstrates a minimal contract account implementation and how `__check_auth` works end-to-end. --- ## BLS Signature BLS Signature The [BLS signature example] illustrates how to implement BLS signature verification inside a contract account. This example is based off of the [Complex Account example]. Although the main goal is to illustrate the practical use of the BLS12-381 functionalities in a relevant setting. It is good to have an understanding of how a contract account works, so walk through the [Simple Account example] first, but it is not required. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [BLS signature example]: https://github.com/stellar/soroban-examples/tree/main/bls_signature [Complex Account example]: ./complex-account.mdx [Simple Account example]: ./simple-account.mdx ## Background on BLS Signature There are plenty of good resources on BLS signature, for example the "BLS12-381 For The Rest Of Us" has a section on [BLS digital signature](https://hackmd.io/@benjaminion/bls12-381#BLS-digital-signatures). [BLS Signature for Busy People](https://gist.github.com/paulmillr/18b802ad219b1aee34d773d08ec26ca2) is a also a good resource for a quick overview. For full reference check out the [IETF draft](https://datatracker.ietf.org/doc/draft-irtf-cfrg-bls-signature). In short, we are verifying the following relation: ```math e(pk, H(m)) = e(g1, \sigma) ``` Where $pk$ is the public key, $H(m)$ is hash of the message onto the `G2` group, $g1$ is the generator point in the `G1` group and $sigma$ is the signature. $e(,)$ denotes the bilinear pairing between a point in `G1` and a point in `G2`. The nice thing about pairing based signature is it enables signature aggregation. I.e. if you have multiple signers `pk_0 .. pk_n` on the a single message, you can compute the aggregate public key $pk_{agg}$ by adding up all the public keys (recall each public key is just a point on the G1 group), the aggregate signature $\sigma_{agg}$ by adding up individual signatures (which is just a point on the G2 group). Then the aggregate signature verification is just ```math e(pk_{agg}, H(m)) = e(g1, \sigma_{agg}) ``` with a single pairing on chain, you can verify N signatures on the same message in constant time. In general, `n` unique messages takes `n + 1` pairing operations to verify all signatures. ### Hash of message `H(m)` The message will need to be hashed on the curve `H(m)` for pairing operation to be applied. We follow the approach outlined in [RFC 9380 - Hashing to Elliptic Curves](https://datatracker.ietf.org/doc/rfc9380). The hashing method requires a unique domain separation tag (DST), it is highly advisable that your application choose a unique DST. For the requirements of DST, refer to section 3.1 of the RFC, :::tip For digital signatures, the G1 and G2 groups can be used interchangeably. Public keys can be chosen as elements of G1 with signatures in G2, or the other way around. The choice involves trade-offs between execution speed and storage size. G1 offers smaller points and faster operations, whereas G2 has larger points and slower performance. ::: :::caution The example presented below is intended for demonstration purpose only - It has **not** undergone security auditing. - It is **not** safe for use in production environments. Implementing a production-safe signature scheme requires deep understanding of the underlying cryptography and security considerations. **Use this at your own risk**. ::: ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `main` branch of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ``` git clone -b main https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, navigate to the `bls_signature` directory, and use `cargo test`. ``` cd bls_signature cargo test ``` You should see the output: ``` running 1 test test test::test ... ok ``` ## Code ```rust title="bls_signature/src/lib.rs" #[contract] pub struct IncrementContract; // `DST `is the domain separation tag, intended to keep hashing inputs of your // contract separate. Refer to section 3.1 in the [Hashing to Elliptic // Curves](https://datatracker.ietf.org/doc/html/rfc9380) on requirements of // DST. const DST: &str = "BLSSIG-V01-CS01-with-BLS12381G2_XMD:SHA-256_SSWU_RO_"; #[derive(Clone)] #[contracttype] pub enum DataKey { Owners, Counter, Dst, } #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum AccError { InvalidSignature = 1, } #[contractimpl] impl IncrementContract { pub fn __constructor(env: Env, agg_pk: BytesN<96>) { // Initialize the account contract essentials: the aggregated pubkey and // the DST. Because the message to be signed (which is // the hash of some call stack) is the same for all signers, we can // simply aggregate all signers (adding up the G1 pubkeys) and store it. env.storage().persistent().set(&DataKey::Owners, &agg_pk); env.storage() .instance() .set(&DataKey::Dst, &Bytes::from_slice(&env, DST.as_bytes())); // initialize the counter, i.e. the business logic this signer contract // guards env.storage().instance().set(&DataKey::Counter, &0_u32); } pub fn increment(env: Env) -> u32 { env.current_contract_address().require_auth(); let mut count: u32 = env.storage().instance().get(&DataKey::Counter).unwrap_or(0); count += 1; env.storage().instance().set(&DataKey::Counter, &count); count } } #[contractimpl(contracttrait)] impl CustomAccountInterface for IncrementContract { type Signature = BytesN<192>; type Error = AccError; #[allow(non_snake_case)] fn __check_auth( env: Env, signature_payload: Hash<32>, agg_sig: Self::Signature, _auth_contexts: Vec, ) -> Result<(), AccError> { // The sdk module containing access to the bls12_381 functions let bls = env.crypto().bls12_381(); // Retrieve the aggregated pubkey and the DST from storage let agg_pk: BytesN<96> = env.storage().persistent().get(&DataKey::Owners).unwrap(); let dst: Bytes = env.storage().instance().get(&DataKey::Dst).unwrap(); // This is the negative of g1 (generator point of the G1 group) let neg_g1 = G1Affine::from_bytes(bytesn!(&env, 0x17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb114d1d6855d545a8aa7d76c8cf2e21f267816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca)); // Hash the signature_payload i.e. the msg being signed and to be // verified into a point in G2 let msg_g2 = bls.hash_to_g2(&signature_payload.into(), &dst); // Prepare inputs to the pairing function let vp1 = vec![&env, G1Affine::from_bytes(agg_pk), neg_g1]; let vp2 = vec![&env, msg_g2, G2Affine::from_bytes(agg_sig)]; // Perform the pairing check, i.e. e(pk, msg)*e(-g1, sig) == 1, which is // equivalent to checking `e(pk, msg) == e(g1, sig)`. // The LHS = e(sk * g1, msg) = sk * e(g1, msg) = e(g1, sk * msg) = e(g1, sig), // thus it must equal to the RHS if the signature matches. if !bls.pairing_check(vp1, vp2) { return Err(AccError::InvalidSignature); } Ok(()) } } ``` Ref: https://github.com/stellar/soroban-examples/tree/main/bls_signature ## How it Works The example contract stores a counter that can only be incremented if a set of owners have approved it. Open the `bls_signature/src/lib.rs` file or see the code above to follow along. ### The Contract ```rust #[contract] pub struct IncrementContract; #[derive(Clone)] #[contracttype] pub enum DataKey { Owners, Counter, Dst, } #[contractimpl] impl IncrementContract { pub fn __constructor(env: Env, agg_pk: BytesN<96>) { // Initialize the account contract essentials: the aggregated pubkey and // the DST. Because the message to be signed (which is // the hash of some call stack) is the same for all signers, we can // simply aggregate all signers (adding up the G1 pubkeys) and store it. env.storage().persistent().set(&DataKey::Owners, &agg_pk); env.storage() .instance() .set(&DataKey::Dst, &Bytes::from_slice(&env, DST.as_bytes())); // initialize the counter, i.e. the business logic this signer contract // guards env.storage().instance().set(&DataKey::Counter, &0_u32); } pub fn increment(env: Env) -> u32 { env.current_contract_address().require_auth(); let mut count: u32 = env.storage().instance().get(&DataKey::Counter).unwrap_or(0); count += 1; env.storage().instance().set(&DataKey::Counter, &count); count } } ``` This contract is fairly simple and standard. On `__constructor()`, which the host invokes at deployment, it initializes the aggregate public key of all the owners, the domain separation tag `DST`, and initializes the counter to 0. It contains a single function `increment` which calls `require_auth` that will check the authorization condition defined later, and if success, increment and return the counter. ### BLS Signature verification ```rust #[contractimpl(contracttrait)] impl CustomAccountInterface for IncrementContract { type Signature = BytesN<192>; type Error = AccError; #[allow(non_snake_case)] fn __check_auth( env: Env, signature_payload: Hash<32>, agg_sig: Self::Signature, _auth_contexts: Vec, ) -> Result<(), AccError> { // The sdk module containing access to the bls12_381 functions let bls = env.crypto().bls12_381(); // Retrieve the aggregated pubkey and the DST from storage let agg_pk: BytesN<96> = env.storage().persistent().get(&DataKey::Owners).unwrap(); let dst: Bytes = env.storage().instance().get(&DataKey::Dst).unwrap(); // This is the negative of g1 (generator point of the G1 group) let neg_g1 = G1Affine::from_bytes(bytesn!(&env, 0x17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb114d1d6855d545a8aa7d76c8cf2e21f267816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca)); // Hash the signature_payload i.e. the msg being signed and to be // verified into a point in G2 let msg_g2 = bls.hash_to_g2(&signature_payload.into(), &dst); // Prepare inputs to the pairing function let vp1 = vec![&env, G1Affine::from_bytes(agg_pk), neg_g1]; let vp2 = vec![&env, msg_g2, G2Affine::from_bytes(agg_sig)]; // Perform the pairing check, i.e. e(pk, msg)*e(-g1, sig) == 1, which is // equivalent to checking `e(pk, msg) == e(g1, sig)`. // The LHS = e(sk * g1, msg) = sk * e(g1, msg) = e(g1, sk * msg) = e(g1, sig), // thus it must equal to the RHS if the signature matches. if !bls.pairing_check(vp1, vp2) { return Err(AccError::InvalidSignature); } Ok(()) } } ``` The `CustomAccountInterface::__check_auth` function implements the custom signature verification logic for this account. `env.crypto().bls12_381()` initializes the bls12_381 module from which the BLS12-381 functions are available. The `signature_payload` contains the payload that was signed. `bls.hash_to_g2(&signature_payload.into(), &dst)` hashes the message into the G2 group. `agg_sig` contains the aggregate signature which is another point in G2. To perform the signature verification, we construct two vectors `Vec` and `Vec`, and call `bls.pairing_check` on them. The pairing check function performs `e(pk, msg)*e(-g1, sig) == 1` which is equivalent to checking `e(pk, msg) == e(g1, sig)`. ### Tests Open the [`bls_signature/src/test.rs`] file to follow along. [`bls_signature/src/test.rs`]: https://github.com/stellar/soroban-examples/tree/main/bls_signature/src/test.rs ```rust title="bls_signature/src/test.rs" #[test] fn test() { let env = Env::default(); let pk = aggregate_pk_bytes(&env); env.mock_all_auths(); let contract_id = env.register(IncrementContract, IncrementContractArgs::__constructor(&pk)); let client = IncrementContractClient::new(&env, &contract_id); let payload = BytesN::random(&env); let sig_val = sign_and_aggregate(&env, &payload.clone().into()).to_val(); env.try_invoke_contract_check_auth::(&client.address, &payload, sig_val, &vec![&env]) .unwrap(); env.cost_estimate().budget().print(); } ``` Most of what's here is needed in order to create the contract client and ensure the invocation of its `CustomAccountInterface` implementation for signature authorization. After the setup, calling `env.try_invoke_contract_check_auth` on the client will invoke the `__check_auth` logic we've defined in our contract. The invocation will return nothing on success, and will panic on failure. `env.cost_estimate().budget().print()` at the end prints out the budget. #### Signature aggregation We first declare 10 random key pairs. These will be used as the signers of this test contract. ```rust #[derive(Debug)] pub struct KeyPair { pub sk: [u8; 32], pub pk: [u8; 96], } static KEY_PAIRS: &[KeyPair] = &[ KeyPair { sk: hex!("18a5ac3cfa6d0b10437a92c96f553311fc0e25500d691ae4b26581e6f925ec83"), pk: hex!("0914e32703bad05ccf4180e240e44e867b26580f36e09331997b2e9effe1f509b1a804fc7ba1f1334c8d41f060dd72550901c5549caef45212a236e288a785d762a087092c769bfa79611b96d73521ddd086b7e05b5c7e4210f50c2ee832e183"), }, KeyPair { sk: hex!("738dbecafa122ee3c953f07e78461a4281cadec00c869098bac48c8c57b63374"), pk: hex!("05f4708a013699229f67d0e16f7c2af8a6557d6d11b737286cfb9429e092c31c412f623d61c7de259c33701aa5387b5004e2c03e8b7ea2740b10a5b4fd050eecca45ccf5588d024cbb7adc963006c29d45a38cb7a06ce2ac45fce52fc0d36572"), }, KeyPair { sk: hex!("4bff25b53f29c8af15cf9b8e69988c3ff79c80811d5027c80920f92fad8d137d"), pk: hex!("18d0fef68a72e0746f8481fa72b78f945bf75c3a1e036fbbde62a421d8f9568a2ded235a27ad3eb0dc234b298b54dd540f61577bc4c6e8842f8aa953af57a6783924c479e78b0d4959038d3d108b3f6dc6a1b02ec605cb6d789af16cfe67f689"), }, KeyPair { sk: hex!("2110f7dae25c4300e1a9681bad6311a547269dba69e94efd342cc208ff50813b"), pk: hex!("1643b04cc21f8af9492509c51a6e20e67fa7923f4fbd52f6fcf73c6a4013f864e3e29eb03f54d234582250ebb5df21140381d0c735e868adfe62f85cf8e85d279864333dbe70656a5f35ebc52c5b497f1c65c7a0144bb0c9a1d843f1a8fb9979"), }, KeyPair { sk: hex!("1e4b6d54ac58d317cbe6fb0472c3cbf2e60ea157edea21354cbc198770f81448"), pk: hex!("02286d1a83a93f35c3461dd71d0840e83e1cd3275ee1af1bfd90ec2366485e9f7f18730f5b686f4810480f1ce5c63dca13a2fac1774aa4e22c29abb9280796d72a2bd0ef963dc76fd45090012bae4a727a6dce49550d9bc9776705f825e24731"), }, KeyPair { sk: hex!("471145761f5cd9d0a9a511f1a80657edfcddc43424e4a5582040ea75c4649909"), pk: hex!("0b7920a3f2a50cfd6dc132a46b7163d3f7d6b1d03d9fcf450eb05dfa89991a269e707e3412270dc422b664d7adda782c11c973232e975ef0d4b4fb5626b563df542fd1862f80bce17cd09bcbce8884bdda4ac9286bf94854dd29cd511a9103a7"), }, KeyPair { sk: hex!("1914beab355b0a86a7bcd37f2e036a9c2c6bff7f16d8bf3e23e42b7131b44701"), pk: hex!("1872237fb7ceccc1a6e85f83988c226cc47db75496e41cf20e8a4b93e8fd5e91d0cdcc3b2946a352223ec2b7817a2aae0dc4e6bb7b97c855828670362fcbd0ad6453f28e4fa4b7a075ac8bb1d69a4a1bb8c6723900fead307239f04a9bcec0ad"), }, KeyPair { sk: hex!("46b19b928638068780ba82e76dfeaeaf5c37790cdf37f580e206dc6599c72dc7"), pk: hex!("0fd1a6b1e46b83a197bbf1dc2a854d024caa5ead5a54893c9767392c837d7c070e86a9206ddba1801332f9d74e0f78e9175419ccc40a966bf4c12a7f8500519e2b83cebd61e32121379911925bf7ae6d2c0d8ec4dcc411d4bbcd14763c1a9d31"), }, KeyPair { sk: hex!("0ce3cd1dcaecf002715228aeb0645c6a7fd9990ace3d79515c547dac120bb9f7"), pk: hex!("19f7e9dcd4ce2bef92180b60d0c7c7b48b1924a36f9fbb93e9ecb8acb3219e26033b83facd4dc6d2e3f9fa0fffafeca8168bd4824e31dc9dfd977fbf037210508bc807c1a6d20f98a044911f6b689328f3f25dd35a6c05e8c6ac3ac6ef0def91"), }, KeyPair { sk: hex!("6b4b27ba3ffc953eff3b974142cdac75f98c8c4ab26f93d5adfd49da5d462c3f"), pk: hex!("15f55ec5572026d6c3c7c62b3ce3c5d7539045e9f492f2b1b0860c0af5f5f6b34531dfe4626a92d5c23ac6ad44330cf40e63a8a7234edbb41539c5484eff2cd23b2f0d502a7fd74501b1a05ffee29b24e79cb1ee9fb9b804d84f486283101ee0"), }, ]; ``` We aggregate the signer public keys, by first converting the bytes into `G1Affine`, then add them all up. ```rust fn aggregate_pk_bytes(env: &Env) -> BytesN<96> { let bls = env.crypto().bls12_381(); let mut agg_pk = G1Affine::from_bytes(BytesN::from_array(env, &KEY_PAIRS[0].pk)); for i in 1..KEY_PAIRS.len() { let pk = G1Affine::from_bytes(BytesN::from_array(env, &KEY_PAIRS[i].pk)); agg_pk = bls.g1_add(&agg_pk, &pk); } agg_pk.to_bytes() } ``` To produce the signature, the message will first be hashed into G2 via `bls.hash_to_g2`. Here we use our own defined DST. To aggregate the signatures, we first produce individual signatures by having each signer sign the message. This means multiplying the secret key by the message's G2 point. Then we add them all up. Here we use `g2_msm`, by an array of message (`Vec`) points and an the array of secret keys (`Vec`) and it computes their inner-product which is the aggregate signature we want. ```rust const DST: &str = "BLSSIG-V01-CS01-with-BLS12381G2_XMD:SHA-256_SSWU_RO_"; fn sign_and_aggregate(env: &Env, msg: &Bytes) -> BytesN<192> { let bls = env.crypto().bls12_381(); let mut vec_sk: Vec = vec![env]; for kp in KEY_PAIRS { vec_sk.push_back(Fr::from_bytes(BytesN::from_array(env, &kp.sk))); } let dst = Bytes::from_slice(env, DST.as_bytes()); let msg_g2 = bls.hash_to_g2(&msg, &dst); let vec_msg: Vec = vec![ env, msg_g2.clone(), msg_g2.clone(), msg_g2.clone(), msg_g2.clone(), msg_g2.clone(), msg_g2.clone(), msg_g2.clone(), msg_g2.clone(), msg_g2.clone(), msg_g2.clone(), ]; bls.g2_msm(vec_msg, vec_sk).to_bytes() } ``` Running this test will produce the following budget output (some portion of the output omitted for brevity), the total CPU consumption for signature verification is around 31M. And you can add as many additional public keys as you like. In general `pairing_check` is a function with linear cost, so the more unique messages that needs to be signed, the higher cost. Here because all signers sign the same content (hash of the call stack of this contract), we can do this in constant time. ``` ---- test::test stdout ---- ================================================================= Cpu limit: 100000000; used: 31143102 Mem limit: 41943040; used: 159903 ================================================================= CostType cpu_insns mem_bytes WasmInsnExec 0 0 MemAlloc 23516 5000 [... previous output omitted for brevity ...] VerifyEcdsaSecp256r1Sig 0 0 Bls12381EncodeFp 2644 0 Bls12381DecodeFp 11820 0 Bls12381G1CheckPointOnCurve 3868 0 Bls12381G1CheckPointInSubgroup 1461020 0 Bls12381G2CheckPointOnCurve 11842 0 Bls12381G2CheckPointInSubgroup 2115644 0 Bls12381G1ProjectiveToAffine 0 0 Bls12381G2ProjectiveToAffine 0 0 Bls12381G1Add 0 0 Bls12381G1Mul 0 0 Bls12381G1Msm 0 0 Bls12381MapFpToG1 0 0 Bls12381HashToG1 0 0 Bls12381G2Add 0 0 Bls12381G2Mul 0 0 Bls12381G2Msm 0 0 Bls12381MapFp2ToG2 0 0 Bls12381HashToG2 7052263 6816 Bls12381Pairing 20447400 148148 Bls12381FrFromU256 0 0 Bls12381FrToU256 0 0 Bls12381FrAddSub 0 0 Bls12381FrMul 0 0 Bls12381FrPow 0 0 Bls12381FrInv 0 0 ================================================================= ``` ## Build the Contract To build the contract into a `.wasm` file, use the `stellar contract build` command. ```sh stellar contract build ``` The `.wasm` file should be found in the `target` directory after building: ``` target/wasm32v1-none/release/soroban_bls_signature_contract.wasm ``` --- ## Complex Account {`Implement a contract account with multisig and custom authorization policies.`} Start with the [Simple Account example](./simple-account.mdx) to learn the single-signer basics. This Complex Account extends that baseline with multisig and customizable authorization policies. Any time an `Address` pointing at this contract instance is used, the logic defined here runs through the Soroban auth framework. Contract accounts are exclusive to Soroban and can't be used to perform other Stellar operations. :::danger Implementing a contract account requires a very good understanding of authentication and authorization and requires rigorous testing and review. The example here is _not_ a full-fledged account contract - use it as an API reference only. ::: :::caution While contract accounts are supported by the Stellar protocol and Soroban SDK, the full client support (such as transaction simulation) is still under development. ::: [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [contract account example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/account ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example use `cargo test`. ```sh cargo test -p soroban-account-contract ``` You should see the output: ``` running 1 test test test::test_token_auth ... ok ``` ## How it Works Open the `account/src/lib.rs` file to follow along. Account contracts implement a special function `__check_auth` that takes the signature payload, signatures and authorization context. The function should error if auth is declined, otherwise auth will be approved. This example contract uses ed25519 keys for signature verification and supports multiple equally weighted signers. It also implements a policy that allows setting per-token limits on transfers. The token can be spent beyond the limit only if every signature is provided. For example, the user may initialize this contract with 2 keys and introduce 100 USDC spend limit. This way they can use a single key to sign their contract invocations and be sure that even if they sign a malicious transaction they won't spend more than 100 USDC. ### Initialization ```rust title="account/src/lib.rs" #[contract] struct AccountContract; #[contracttype] #[derive(Clone)] enum DataKey { SignerCnt, Signer(BytesN<32>), SpendLimit(Address), } ... #[contractimpl] impl AccountContract { // Initialize the contract with a list of ed25519 public key ('signers'). pub fn __constructor(env: Env, signers: Vec>) { // In reality this would need some additional validation on signers // (deduplication etc.). for signer in signers.iter() { env.storage().instance().set(&DataKey::Signer(signer), &()); } env.storage() .instance() .set(&DataKey::SignerCnt, &signers.len()); } ... } ``` This account contract needs to work with the public keys explicitly. Here we initialize the contract with ed25519 keys. We use constructor in order to ensure that the contract instance is created and initialized atomically (without constructor there is a risk that someone frontruns the initialization of the contract and sets their own public keys). ### Policy modification ```rust #[contractimpl] impl AccountContract { ... // Adds a limit on any token transfers that aren't signed by every signer. // For the sake of simplicity of the example the limit is only applied on // a per-authorization basis; the 'real' limits should likely be time-based // instead. pub fn add_limit(env: Env, token: Address, limit: i128) { // The current contract address is the account contract address and has // the same semantics for `require_auth` call as any other account // contract address. // Note, that if a contract *invokes* another contract, then it would // authorize the call on its own behalf and that wouldn't require any // user-side verification. env.current_contract_address().require_auth(); env.storage() .instance() .set(&DataKey::SpendLimit(token), &limit); } } ``` This function allows users to set and modify the per-token spend limit described above. The neat trick here is that `require_auth` can be used for the `current_contract_address()`, i.e. the account contract may be used to verify authorization for its own administrative functions. This way there is no need to write duplicate authorization and authentication logic. ### `__check_auth` ```rust #[contracttype] #[derive(Clone)] pub struct AccSignature { pub public_key: BytesN<32>, pub signature: BytesN<64>, } #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum AccError { NotEnoughSigners = 1, NegativeAmount = 2, BadSignatureOrder = 3, UnknownSigner = 4, } ... #[contractimpl] impl CustomAccountInterface for AccountContract { type Signature = Vec; type Error = AccError; // This is the 'entry point' of the account contract and every account // contract has to implement it. `require_auth` calls for the Address of // this contract will result in calling this `__check_auth` function with // the appropriate arguments. // // This should return `()` if authentication and authorization checks have // been passed and return an error (or panic) otherwise. // // `__check_auth` takes the payload that needed to be signed, arbitrarily // typed signatures (`Vec` contract type here) and authorization // context that contains all the invocations that this call tries to verify. // // `__check_auth` has to authenticate the signatures. It also may use // `auth_context` to implement additional authorization policies (like token // spend limits here). // // Soroban host guarantees that `__check_auth` is only being called during // `require_auth` verification and hence this may mutate its own state // without the need for additional authorization (for example, this could // store per-time-period token spend limits instead of just enforcing the // limit per contract call). // // Note, that `__check_auth` function shouldn't call `require_auth` on the // contract's own address in order to avoid infinite recursion. #[allow(non_snake_case)] fn __check_auth( env: Env, signature_payload: Hash<32>, signatures: Self::Signature, auth_context: Vec, ) -> Result<(), AccError> { // Perform authentication. authenticate(&env, &signature_payload, &signatures)?; let tot_signers: u32 = env .storage() .instance() .get::<_, u32>(&DataKey::SignerCnt) .unwrap(); let all_signed = tot_signers == signatures.len(); let curr_contract = env.current_contract_address(); // This is a map for tracking the token spend limits per token. This // makes sure that if e.g. multiple `transfer` calls are being authorized // for the same token we still respect the limit for the total // transferred amount (and not the 'per-call' limits). let mut spend_left_per_token = Map::::new(&env); // Verify the authorization policy. for context in auth_context.iter() { verify_authorization_policy( &env, &context, &curr_contract, all_signed, &mut spend_left_per_token, )?; } Ok(()) } } ``` `__check_auth` is a special function that account contracts implement. It will be called by the Soroban environment every time `require_auth` or `require_auth_for_args` is called for the address of the account contract. Here it is implemented in two steps. First, authentication is performed using the signature payload and a vector of signatures. Second, authorization policy is enforced using the `auth_context` vector. This vector contains all the contract calls that are being authorized by the provided signatures. `__check_auth` is a reserved function and can only be called by the Soroban environment in response to a call to `require_auth`. Any direct call to `__check_auth` will fail. This makes it safe to write to the account contract storage from `__check_auth`, as it's guaranteed to not be called in unexpected context. In this example it's possible to persist the spend limits without worrying that they'll be exhausted via a bad actor calling `__check_auth` directly. ### Authentication ```rust fn authenticate( env: &Env, signature_payload: &Hash<32>, signatures: &Vec, ) -> Result<(), AccError> { for i in 0..signatures.len() { let signature = signatures.get_unchecked(i); if i > 0 { let prev_signature = signatures.get_unchecked(i - 1); if prev_signature.public_key >= signature.public_key { return Err(AccError::BadSignatureOrder); } } if !env .storage() .instance() .has(&DataKey::Signer(signature.public_key.clone())) { return Err(AccError::UnknownSigner); } env.crypto().ed25519_verify( &signature.public_key, &signature_payload.clone().into(), &signature.signature, ); } Ok(()) } ``` Authentication here simply checks that the provided signatures are valid given the payload and also that they belong to the signers of this account contract. ### Authorization policy ```rust fn verify_authorization_policy( env: &Env, context: &Context, curr_contract: &Address, all_signed: bool, spend_left_per_token: &mut Map, ) -> Result<(), AccError> { // There are no limitations when every signers signs the transaction. if all_signed { return Ok(()); } let contract_context = match context { Context::Contract(c) => { // Allow modifying this contract only if every signer has signed for it. if &c.contract == curr_contract { return Err(AccError::NotEnoughSigners); } c } // Allow creating new contracts only if every signer has signed for it. Context::CreateContractHostFn(_) | Context::CreateContractWithCtorHostFn(_) => { return Err(AccError::NotEnoughSigners); } }; ... } ``` We verify the policy per `Context`. i.e. per one `require_auth` call for the address of this account. The policy for the account contract itself enforces every signer to have signed the method call. ```rust fn verify_authorization_policy( env: &Env, context: &Context, curr_contract: &Address, all_signed: bool, spend_left_per_token: &mut Map, ) -> Result<(), AccError> { ... // Besides the checks above we're only interested in functions that spend tokens. if contract_context.fn_name != TRANSFER_FN && contract_context.fn_name != APPROVE_FN && contract_context.fn_name != BURN_FN { return Ok(()); } let spend_left: Option = if let Some(spend_left) = spend_left_per_token.get(contract_context.contract.clone()) { Some(spend_left) } else if let Some(limit_left) = env .storage() .instance() .get::<_, i128>(&DataKey::SpendLimit(contract_context.contract.clone())) { Some(limit_left) } else { None }; // 'None' means that the contract is outside of the policy. if let Some(spend_left) = spend_left { // 'amount' is the third argument in both `approve` and `transfer`. // If the contract has a different signature, it's safer to panic // here, as it's expected to have the standard interface. let spent: i128 = contract_context .args .get(2) .unwrap() .try_into_val(env) .unwrap(); if spent < 0 { return Err(AccError::NegativeAmount); } if !all_signed && spent > spend_left { return Err(AccError::NotEnoughSigners); } spend_left_per_token.set(contract_context.contract.clone(), spend_left - spent); } Ok(()) } ``` Then we check for the standard token function names and verify that for these function we don't exceed the spending limits. ### Tests Open the [`account/src/test.rs`] file to follow along. [`account/src/test.rs`]: https://github.com/stellar/soroban-examples/tree/v23.0.0/account/src/test.rs Refer to another examples for the general information on the test setup. Here we only look at some points specific to the account contracts. ```rust fn sign(e: &Env, signer: &Keypair, payload: &BytesN<32>) -> Val { AccSignature { public_key: signer_public_key(e, signer), signature: signer .sign(payload.to_array().as_slice()) .to_bytes() .into_val(e), } .into_val(e) } ``` Unlike most of the contracts that may simply use `Address`, account contracts deal with the signature verification and hence need to actually sign the payloads. ```rust let payload = BytesN::random(&env); let token = Address::generate(&env); // `__check_auth` can't be called directly, hence we need to use // `try_invoke_contract_check_auth` testing utility that emulates being // called by the Soroban host during a `require_auth` call. env.try_invoke_contract_check_auth::( &account_contract.address, &payload, vec![&env, sign(&env, &signers[0], &payload)].into(), &vec![ &env, token_auth_context(&env, &token, Symbol::new(&env, "transfer"), 1000), ], ) .unwrap(); ``` `__check_auth` can't be called directly as regular contract functions, hence we need to use `try_invoke_contract_check_auth` testing utility that emulates being called by the Soroban host during a `require_auth` call. ```rust // Add a spend limit of 1000 per 1 signer. account_contract.add_limit(&token, &1000); // Verify that this call needs to be authorized. assert_eq!( env.auths(), std::vec![( account_contract.address.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( account_contract.address.clone(), symbol_short!("add_limit"), (token.clone(), 1000_i128).into_val(&env), )), sub_invocations: std::vec![] } )] ); ``` Asserting the contract-specific error to `try_invoke_contract_check_auth` allows verifying the exact error code and makes sure that the verification has failed due to not having enough signers and not for any other reason. It's a good idea for the account contract to have detailed error codes and verify that they are returned when they are expected. ```rust // 1 signer no longer can perform the token operation that transfers more // than 1000 units. assert_eq!( env.try_invoke_contract_check_auth::( &account_contract.address, &payload, vec![&env, sign(&env, &signers[0], &payload)].into(), &vec![ &env, token_auth_context(&env, &token, Symbol::new(&env, "transfer"), 1001) ], ) .err() .unwrap() .unwrap(), AccError::NotEnoughSigners ); ``` ## Further Reading - [Delegate Auth example](./delegate-auth.mdx) — extends this example with CAP-71 auth delegation to registered delegate signers. --- ## Cross Contract Calls Call a smart contract from another smart contract. The [cross contract call example] demonstrates how to call a contract from another contract. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] :::info In this example there are two contracts that are compiled separately, deployed separately, and then tested together. There are a variety of ways to develop and test contracts with dependencies on other contracts, and the Soroban SDK and tooling is still building out the tools to support these workflows. Feedback appreciated [here](https://github.com/stellar/rs-soroban-sdk/issues/new/choose). ::: [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [cross contract call example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/cross_contract ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, first build Contract A (the contract to be called) and then run `cargo test` from Contract B's directory. Build Contract A by navigating to the `cross_contract/contract_a` directory and use the `stellar contract build` build command: ```sh cd cross_contract/contract_a stellar contract build ``` When Contract A has been built, navigate to the `cross_contract/contract_b` directory, and use `cargo test`. ```sh cd ../contract_b cargo test ``` You should see the output: ``` running 1 test test test::test ... ok ``` ## Code ```rust title="cross_contract/contract_a/src/lib.rs" #[contract] pub struct ContractA; #[contractimpl] impl ContractA { pub fn add(x: u32, y: u32) -> u32 { x.checked_add(y).expect("no overflow") } } ``` ```rust title="cross_contract/contract_b/src/lib.rs" mod contract_a { soroban_sdk::contractimport!( file = "../contract_a/target/wasm32v1-none/release/soroban_cross_contract_a_contract.wasm" ); } #[contract] pub struct ContractB; #[contractimpl] impl ContractB { pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 { let client = contract_a::Client::new(&env, &contract); client.add(&x, &y) } } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/cross_contract ## How it Works Cross contract calls are made by invoking another contract by its contract ID. Contracts to invoke can be imported into your contract with the use of `contractimport!(file = "...")`. The import will code generate: - A `ContractClient` type that can be used to invoke functions on the contract. - Any types in the contract that were annotated with `#[contracttype]`. :::tip The `contractimport!` macro will generate the types in the module it is used, so it's a good idea to use the macro inside a `mod { ... }` block, or inside its own file, so that the names of generated types don't collide with names of types in your own contract. ::: Open the files above to follow along. ### Contract A: The Contract to be Called The contract to be called is Contract A. It is a simple contract that accepts `x` and `y` parameters, adds them together and returns the result. ```rust title="cross_contract/contract_a/src/lib.rs" #[contract] pub struct ContractA; #[contractimpl] impl ContractA { pub fn add(x: u32, y: u32) -> u32 { x.checked_add(y).expect("no overflow") } } ``` :::tip The contract uses the `checked_add` method to ensure that there is no overflow, and if there is overflow, panics rather than returning an overflowed value. Rust's primitive integer types all have checked operations available as functions with the prefix `checked_`. ::: ### Contract B: The Contract doing the Calling The contract that does the calling is Contract B. It accepts a contract ID that it will call, as well as the same parameters to pass through. In many contracts the contract to call might have been stored as contract data and be retrieved, but in this simple example it is being passed in as a parameter each time. The contract imports Contract A into the `contract_a` module. The `contract_a::Client` is constructed pointing at the contract ID passed in. The client is used to execute the `add` function with the `x` and `y` parameters on Contract A. ```rust title="cross_contract/contract_b/src/lib.rs" mod contract_a { soroban_sdk::contractimport!( file = "../contract_a/target/wasm32v1-none/release/soroban_cross_contract_a_contract.wasm" ); } #[contract] pub struct ContractB; #[contractimpl] impl ContractB { pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 { let client = contract_a::Client::new(&env, &contract); client.add(&x, &y) } } ``` ### Tests Open the `cross_contract/contract_b/src/test.rs` file to follow along. ```rust title="cross_contract/contract_b/src/test.rs" #[test] fn test() { let env = Env::default(); // Register contract A using the imported WASM. let contract_a_id = env.register(contract_a::WASM, ()); // Register contract B defined in this crate. let contract_b_id = env.register(ContractB, ()); // Create a client for calling contract B. let client = ContractBClient::new(&env, &contract_b_id); // Invoke contract B via its client. Contract B will invoke contract A. let sum = client.add_with(&contract_a_id, &5, &7); assert_eq!(sum, 12); } ``` In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. ```rust let env = Env::default(); ``` Contract A is registered with the environment using the imported Wasm. ```rust let contract_a_id = env.register(contract_a::WASM, ()); ``` Contract B is registered with the environment using the contract type and the contract instance is compiled into the Rust binary. ```rust let contract_b_id = env.register(ContractB, ()); ``` All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `ContractB`, and the client is named `ContractBClient`. The client can be constructed and used in the same way that client generated for Contract A can be. ```rust let client = ContractBClient::new(&env, &contract_b_id); ``` The client is used to invoke the `add_with` function on Contract B. Contract B will invoke Contract A, and the result will be returned. ```rust let sum = client.add_with(&contract_a_id, &5, &7); ``` The test asserts that the result that is returned is as we expect. ```rust assert_eq!(sum, 12); ``` ## Build the Contracts To build the contract into a `.wasm` file, use the `stellar contract build` command. Both `contract_call/contract_a` and `contract_call/contract_b` must be built, with `contract_a` being built first. ```sh stellar contract build ``` Both `.wasm` files should be found in both contract `target` directories after building both contracts: ``` target/wasm32v1-none/release/soroban_cross_contract_a_contract.wasm ``` ``` target/wasm32v1-none/release/soroban_cross_contract_b_contract.wasm ``` ## Run the Contract If you have [`stellar-cli`] installed, you can invoke contract functions. Both contracts must be deployed. ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_cross_contract_a_contract.wasm \ --alias cross_contract_a_example \ --source-account alice \ --network testnet ``` ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_cross_contract_b_contract.wasm \ --alias cross_contract_b_example \ --source-account alice \ --network testnet ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/soroban_cross_contract_a_contract.wasm ` --alias cross_contract_a_example ` --source-account alice ` --network testnet ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/soroban_cross_contract_b_contract.wasm ` --alias cross_contract_b_example ` --source-account alice ` --network testnet ``` Invoke Contract B's `add_with` function, passing in values for `x` and `y` (e.g. as `5` and `7`), and then pass in the contract ID or alias of Contract A. ```sh stellar contract invoke \ --id cross_contract_b_example \ --source-account alice \ --network testnet \ -- \ add_with \ --contract cross_contract_a_example \ --x 5 \ --y 7 ``` ```powershell stellar contract invoke ` --id cross_contract_b_example ` --source-account alice ` --network testnet ` -- ` add_with ` --contract cross_contract_a_example ` --x 5 ` --y 7 ``` The following output should occur using the code above. ``` 12 ``` Contract B's `add_with` function invoked Contract A's `add` function to do the addition. [`stellar-cli`]: ../getting-started/setup.mdx#install-the-stellar-cli --- ## Custom Types Define your own data structures in a smart contract. The [custom types example] demonstrates how to define your own data structures that can be stored on the ledger, or used as inputs and outputs to contract invocations. This example is an extension of the [storing data example]. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [custom types example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/custom_types [storing data example]: ../getting-started/storing-data.mdx ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, navigate to the `custom_types` directory, and use `cargo test`. ```sh cd custom_types cargo test ``` You should see the output: ``` running 1 test test test::test ... ok ``` ## Code ```rust title="custom_types/src/lib.rs" #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct State { pub count: u32, pub last_incr: u32, } const STATE: Symbol = symbol_short!("STATE"); #[contract] pub struct IncrementContract; #[contractimpl] impl IncrementContract { /// Increment increments an internal counter, and returns the value. pub fn increment(env: Env, incr: u32) -> u32 { // Get the current count. let mut state = Self::get_state(env.clone()); // Increment the count. state.count += incr; state.last_incr = incr; // Save the count. env.storage().instance().set(&STATE, &state); // Return the count to the caller. state.count } /// Return the current state. pub fn get_state(env: Env) -> State { env.storage().instance().get(&STATE).unwrap_or(State { count: 0, last_incr: 0, }) // If no value set, assume 0. } } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/custom_types ## How it Works Custom types are defined using the `#[contracttype]` attribute on either a `struct` or an `enum`. Open the `custom_types/src/lib.rs` file to follow along. ### Custom Type: Struct Structs are stored on ledger as a map of key-value pairs, where the key is up to a 32 character string representing the field name, and the value is the value encoded. Field names must be no more than 32 characters. ```rust #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct State { pub count: u32, pub last_incr: u32, } ``` ### Custom Type: Enum The example does not contain enums, but enums may also be contract types. Enums containing unit and tuple variants are stored on ledger as a two element vector, where the first element is the name of the enum variant as a string up to 32 characters in length, and the value is the value if the variant has one. Only unit variants and single value variants, like `A` and `B` below, are supported. ```rust #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub enum Enum { A, B(...), } ``` Enums containing integer values are stored on ledger as the `u32` value. ```rust #[contracttype] #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(u32)] pub enum Enum { A = 1, B = 2, } ``` ### Using Types in Functions Types that have been annotated with `#[contracttype]` can be stored as contract data and retrieved later. Types can also be used as inputs and outputs on contract functions. ```rust /// Increment increments an internal counter, and returns the value. pub fn increment(env: Env, incr: u32) -> u32 { // Get the current count. let mut state = Self::get_state(env.clone()); // Increment the count. state.count += incr; state.last_incr = incr; // Save the count. env.storage().instance().set(&STATE, &state); // Return the count to the caller. state.count } /// Return the current state. pub fn get_state(env: Env) -> State { env.storage().instance().get(&STATE).unwrap_or(State { count: 0, last_incr: 0, }) // If no value set, assume 0. } ``` ## Tests Open the `custom_types/src/test.rs` file to follow along. ```rust title="custom_types/src/test.rs" #[test] fn test() { let env = Env::default(); let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.increment(&1), 1); assert_eq!(client.increment(&10), 11); assert_eq!( client.get_state(), State { count: 11, last_incr: 10 } ); } ``` In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. ```rust let env = Env::default(); ``` The contract is registered with the environment using the contract type. ```rust let contract_id = env.register(IncrementContract, ()); ``` All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `IncrementContract`, and the client is named `IncrementContractClient`. ```rust let client = IncrementContractClient::new(&env, &contract_id); ``` The test invokes the `increment` function on the registered contract that causes the `State` type to be stored and updated a couple times. ```rust assert_eq!(client.increment(&1), 1); assert_eq!(client.increment(&10), 11); ``` The test then invokes the `get_state` function to get the `State` value that was stored, and can assert on its values. ```rust assert_eq!( client.get_state(), State { count: 11, last_incr: 10 } ); ``` ## Build the Contract To build the contract, use the `stellar contract build` command. ```sh stellar contract build ``` A `.wasm` file should be outputted in the `target` directory: ``` target/wasm32v1-none/release/soroban_custom_types_contract.wasm ``` ## Run the Contract If you have [`stellar-cli`] installed, you can [deploy] and invoke contract function. ```sh stellar contract invoke \ --id CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN \ --source-account alice \ --network testnet \ -- \ increment \ --incr 5 ``` ```powershell stellar contract invoke ` --id CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN ` --source-account alice ` --network testnet ` -- ` increment ` --incr 5 ``` The following output should occur using the code above. ``` 5 ``` Run it a few more times with different increment amounts to watch the count change. Use the `stellar-cli` to inspect what the counter is after a few runs. ```sh stellar contract read --id 1 --key STATE ``` ``` STATE,"{""count"":25,""last_incr"":15}" ``` [`stellar-cli`]: ../getting-started/setup.mdx#install-the-stellar-cli [deploy]: ../getting-started/deploy-to-testnet --- ## Delegate Auth Delegate Auth This example shows **auth delegation**: a `ModularAccount` contract performs no signature verification itself. Instead, it stores a set of registered signer addresses and, when `__check_auth` is called, forwards the authorization context to whichever of those signers the user attached to the transaction. Each delegate runs its own `__check_auth` independently. Auth delegation was introduced in soroban-sdk v27 via [CAP-71](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0071.md). For a single-key account see the [Simple Account example](./simple-account.mdx), and for a multi-sig account with spend-limit policies see the [Complex Account example](./complex-account.mdx). :::danger Implementing a contract account requires a very good understanding of authentication and authorization and requires rigorous testing and review. The example here is _not_ a full-fledged account contract — use it as an API reference only. ::: :::caution While contract accounts are supported by the Stellar protocol and Soroban SDK, the full client support (such as transaction simulation) is still under development. ::: [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples ## Run the Example 1. Finish the [Setup] checklist to install the Stellar CLI, Rust target, and required environment variables. 2. Clone the `soroban-examples` repository: ```sh git clone https://github.com/stellar/soroban-examples ``` 3. Run the tests from the `modular_account` directory: ```sh cd modular_account make test ``` Expected output: ``` running 3 tests test test::test_empty_delegates_is_rejected ... ok test test::test_unknown_delegate_is_rejected ... ok test test::test ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` [setup]: ../getting-started/setup.mdx ## Code ```rust title="modular_account/src/lib.rs" #![no_std] use soroban_sdk::{ auth::{Context, CustomAccountInterface}, contract, contracterror, contractimpl, contracttype, crypto::Hash, Address, Env, Vec, }; #[contracterror] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] #[repr(u32)] pub enum Error { UnknownDelegate = 1, InsufficientDelegates = 2, } #[contracttype] enum ModularAccountDataKey { // Marks an address as a signer allowed to authenticate for the // modular account. Signer(Address), } #[contract] pub struct ModularAccount; #[contractimpl] impl ModularAccount { // Registers the addresses allowed to authenticate for this account. pub fn __constructor(env: Env, signers: Vec
) { for signer in signers.iter() { env.storage() .persistent() .set(&ModularAccountDataKey::Signer(signer), &()); } } } #[contractimpl] impl CustomAccountInterface for ModularAccount { // The account verifies no signature of its own, so it carries no // signature to check. type Signature = (); type Error = Error; fn __check_auth( env: Env, _signature_payload: Hash<32>, _signatures: (), _auth_contexts: Vec, ) -> Result<(), Error> { // The signers the user attached to the auth entry for this // account's authorization. let delegates = env.custom_account().get_delegated_signers(); // With no delegates to forward to, the account would authenticate // nothing and be effectively unauthenticated, so reject it. A real // account might require more than one delegate to meet a threshold. if delegates.is_empty() { return Err(Error::InsufficientDelegates); } // Check if the delegates are accepted by the modular account. for delegate in delegates.iter() { if !env .storage() .persistent() .has(&ModularAccountDataKey::Signer(delegate.clone())) { return Err(Error::UnknownDelegate); } } // Forward the current authorization to each delegate. for delegate in delegates.iter() { env.custom_account().delegate_auth(&delegate); } Ok(()) } } mod test; ``` ## How it Works ### Storage layout `ModularAccountDataKey::Signer(Address)` uses one persistent storage entry per allowed signer. Using a per-key entry makes registration and revocation O(1) lookups rather than scanning a list. The `()` value signals presence. ### Constructor ```rust pub fn __constructor(env: Env, signers: Vec
) { for signer in signers.iter() { env.storage() .persistent() .set(&ModularAccountDataKey::Signer(signer), &()); } } ``` Each allowed delegate address is persisted individually at deployment. A production account would also expose an admin function to add or remove signers after deployment. ### `type Signature = ()` ```rust type Signature = (); ``` `ModularAccount` verifies no signature of its own — it relies entirely on its delegates. Setting `Signature = ()` tells the Soroban host that no signature data needs to be passed to `__check_auth`. The user's wallet still builds a full `SorobanAddressCredentialsWithDelegates` auth entry (see the test), but the account-level signature field is empty. ### `__check_auth`: get → verify → forward ```rust let delegates = env.custom_account().get_delegated_signers(); if delegates.is_empty() { return Err(Error::InsufficientDelegates); } for delegate in delegates.iter() { if !env.storage().persistent().has(&ModularAccountDataKey::Signer(delegate.clone())) { return Err(Error::UnknownDelegate); } } for delegate in delegates.iter() { env.custom_account().delegate_auth(&delegate); } ``` Two methods on `env.custom_account()` drive delegation. Both may only be called from within `__check_auth`; calling either outside of it panics. - **`get_delegated_signers() -> Vec
`** returns the delegate addresses the user attached to the transaction's auth entry. These are unsanitized user input — the contract must verify each one is registered before forwarding. - **`delegate_auth(&address)`** forwards the current `__check_auth` authorization context to `address`. Unlike `require_auth`, this does not start a new contract invocation and does not require a separate auth entry for the delegate in the transaction. Delegation is nestable: a delegate may further delegate. The example first rejects an empty delegate list — since the account verifies no signature of its own, forwarding to nobody would leave it effectively unauthenticated. It then makes two passes: first it validates all delegates, then it forwards to each. This ensures that an invalid delegate causes the whole check to fail before any forwarding occurs. ## Tests Open [`modular_account/src/test.rs`][test-rs]. The test defines two helper contracts: [test-rs]: https://github.com/stellar/soroban-examples/tree/main/modular_account/src/test.rs - **`DelegateAccount`** — a simple custom account with `type Signature = ()` that always approves, and stores the received `auth_contexts` in instance storage for later assertion. - **`Protected`** — a contract with one function that calls `account.require_auth()`. The test registers one `ModularAccount` with one `DelegateAccount` as its allowed signer, builds an `AddressWithDelegates` auth entry by hand, and calls `Protected::protected`: ```rust env.set_auths(&[SorobanAuthorizationEntry { credentials: SorobanCredentials::AddressWithDelegates( SorobanAddressCredentialsWithDelegates { address_credentials: SorobanAddressCredentials { address: account_addr.clone(), nonce: 1, signature_expiration_ledger: 100, // The account verifies no signature of its own. signature: ScVal::Void, }, delegates: std::vec![SorobanDelegateSignature { address: delegate_addr, signature: ScVal::Void, nested_delegates: VecM::default(), }] .try_into() .unwrap(), }, ), root_invocation: SorobanAuthorizedInvocation { /* ... */ }, }]); ProtectedClient::new(&env, &protected).protected(&account); ``` After the call, the test asserts two things: 1. `env.auths()` shows only the account authorizing `protected` — delegating to `DelegateAccount` is not recorded as a separate top-level authorization. 2. `DelegateAccount`'s `ApprovedContexts` storage contains the same contract context, confirming the delegation actually reached it. Two further tests cover the rejection paths: `test_unknown_delegate_is_rejected` attaches a delegate the account never registered, and `test_empty_delegates_is_rejected` attaches none at all. In both, `Protected::protected` fails. The caller only sees a generic `Error(Auth, InvalidAction)`, because the host escalates any failed `__check_auth` to that — the account's own error code does not propagate to the return value. To assert the account rejected the call for its own reason, the tests read the host's diagnostic events with `env.host().get_diagnostic_events()` and match the recorded `Error(Contract, #n)` — `UnknownDelegate` and `InsufficientDelegates` respectively. :::note Testing delegated auth via `env.try_invoke_contract_check_auth` is not supported. Use `env.set_auths` with `SorobanAddressCredentialsWithDelegates` and a wrapper contract call instead, as shown in the test. ::: ## Build the Contract ```sh stellar contract build ``` A `.wasm` file will be output in the `target` directory: ``` target/wasm32v1-none/release/soroban_modular_account_contract.wasm ``` ## Further Reading - [Simple Account example](./simple-account.mdx) — the minimal single-key baseline. - [Complex Account example](./complex-account.mdx) — multisig and spend-limit policies. - [CAP-71 specification](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0071.md) — the protocol change that introduced auth delegation. - [`CustomAccount` API docs](https://docs.rs/soroban-sdk/latest/soroban_sdk/custom_account/struct.CustomAccount.html) — reference for `get_delegated_signers` and `delegate_auth`. --- ## Deployer {`Deploy and initialize a smart contract using another smart contract.`} The [deployer example] demonstrates how to deploy contracts using a contract. Here we deploy a contract on behalf of any address and initialize it atomically. :::info In this example there are two contracts that are compiled separately, and the tests deploy one with the other. ::: [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [deployer example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/deployer ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, navigate to the `deployer/deployer` directory, and use `cargo test`. ```sh cd deployer/deployer cargo test ``` You should see the output: ``` running 1 test test test::test ... ok ``` ## Code ```rust title="deployer/deployer/src/lib.rs" #[contract] pub struct Deployer; const ADMIN: Symbol = symbol_short!("admin"); #[contractimpl] impl Deployer { /// Construct the deployer with a provided administrator. pub fn __constructor(env: Env, admin: Address) { env.storage().instance().set(&ADMIN, &admin); } /// Deploys the contract on behalf of the `Deployer` contract. /// /// This has to be authorized by the `Deployer`s administrator. pub fn deploy( env: Env, wasm_hash: BytesN<32>, salt: BytesN<32>, constructor_args: Vec, ) -> Address { let admin: Address = env.storage().instance().get(&ADMIN).unwrap(); admin.require_auth(); // Deploy the contract using the uploaded Wasm with given hash on behalf // of the current contract. // Note, that not deploying on behalf of the admin provides more // consistent address space for the deployer contracts - the admin could // change or it could be a completely separate contract with complex // authorization rules, but all the contracts will still be deployed // by the same `Deployer` contract address. let deployed_address = env .deployer() .with_address(env.current_contract_address(), salt) .deploy_v2(wasm_hash, constructor_args); deployed_address } } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/deployer ## How it Works Contracts can deploy other contracts using the SDK `deployer()` method. The contract address of the deployed contract is deterministic and is derived from the address of the deployer. The deployment also has to be authorized by the deployer. Open the `deployer/deployer/src/lib.rs` file to follow along. ### Contract Wasm Upload Before deploying the new contract instances, the Wasm code needs to be uploaded on-chain. Then it can be used to deploy an arbitrary number of contract instances. The upload should typically happen outside of the deployer contract, as it needs to happen just once. However, it is possible to use `env.deployer().upload_contract_wasm()` function to upload Wasm from a contract as well. See the [tests](#tests) for an example of uploading the contract code programmatically. For the actual on-chain installation see the general deployment [tutorial](../../smart-contracts/getting-started/deploy-to-testnet.mdx). ### Authorization :::info For introduction to Soroban authorization see the [auth tutorial](./auth.mdx). ::: We start with verifying authorization of the deployer contract's admin. Without that anyone would be able to call the `deploy` function with any arguments, which may not always be desirable (however, there are contracts where it's perfectly fine to have permissionless deployments). ```rust let admin: Address = env.storage().instance().get(&ADMIN).unwrap(); admin.require_auth(); ``` `deployer().with_address()` performs authorization as well. However, as we deploy on behalf of the current contract, the call is considered to have been implicitly authorized. See more details on the actual authorization payloads in [tests](#tests). ### `env.deployer()` The `env.deployer()` SDK function comes with a few deployment-related utilities. Here we use the most generic deployer kind, `with_address(env.current_contract_address(), salt)`. ```rust let deployed_address = env .deployer() .with_address(env.current_contract_address(), salt) .deploy_v2(wasm_hash, constructor_args); ``` `with_address()` accepts the `deployer` address and `salt`. Both are used to derive the address of the deployed contract deterministically. It is not possible to re-deploy an already existing contract. :::tip The `env.deployer().with_address(env.current_contract_address(), salt)` call may be replaced with the `env.deployer().with_current_contract(salt)` function for brevity. ::: The `deploy_v2()` function performs the actual deployment using the provided `wasm_hash`. The implementation of the new contract is defined by the Wasm file uploaded under `wasm_hash`. `constructor_args` are the arguments that will be passed to the constructor of the contract that is being deployed. If the deployed contract has no constructor, empty argument vector should be passed. :::tip Only the `wasm_hash` itself is stored per contract ID thus saving the ledger space and fees. ::: ### Tests Open the [`deployer/deployer/src/test.rs`](https://github.com/stellar/soroban-examples/tree/v23.0.0/deployer/deployer/src/test.rs) file to follow along. #### Contract to deploy Start by importing the test contract Wasm to be deployed. ```rust title="deployer/deployer/src/test.rs" // The contract that will be deployed by the deployer contract. mod contract { soroban_sdk::contractimport!( file = "../contract/target/wasm32v1-none/release/soroban_deployer_test_contract.wasm" ); } ``` That contract contains the following code that exports two functions: constructor function that takes a value and a getter function for the stored value. ```rust title="deployer/contract/src/lib.rs" #[contract] pub struct Contract; const KEY: Symbol = symbol_short!("value"); #[contractimpl] impl Contract { pub fn __constructor(env: Env, value: u32) { env.storage().instance().set(&KEY, &value); } pub fn value(env: Env) -> u32 { env.storage().instance().get(&KEY).unwrap() } } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/deployer This test contract will be used when testing the deployer. The deployer contract will deploy the test contract and invoke its constructor. #### Test code ```rust title="deployer/deployer/src/test.rs" #[test] fn test() { let env = Env::default(); let admin = Address::generate(&env); let deployer_client = DeployerClient::new(&env, &env.register(Deployer, (&admin,))); // Upload the Wasm to be deployed from the deployer contract. // This can also be called from within a contract if needed. let wasm_hash = env.deployer().upload_contract_wasm(contract::WASM); // Deploy contract using deployer, and include an init function to call. let salt = BytesN::from_array(&env, &[0; 32]); let constructor_args: Vec = (5u32,).into_val(&env); env.mock_all_auths(); let contract_id = deployer_client.deploy(&wasm_hash, &salt, &constructor_args); // An authorization from the admin is required. let expected_auth = AuthorizedInvocation { // Top-level authorized function is `deploy` with all the arguments. function: AuthorizedFunction::Contract(( deployer_client.address, symbol_short!("deploy"), (wasm_hash.clone(), salt, constructor_args).into_val(&env), )), sub_invocations: vec![], }; assert_eq!(env.auths(), vec![(admin, expected_auth)]); // Invoke contract to check that it is initialized. let client = contract::Client::new(&env, &contract_id); let sum = client.value(); assert_eq!(sum, 5); } ``` In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. ```rust let env = Env::default(); ``` Register the deployer contract with the environment and create a client to for it. The contract is initialized with the admin address during the registration. ```rust let admin = Address::generate(&env); let deployer_client = DeployerClient::new(&env, &env.register(Deployer, (&admin,))); ``` Upload the code of the test contract that we have imported above via `contractimport!` and get the hash of the uploaded Wasm code. ```rust let wasm_hash = env.deployer().upload_contract_wasm(contract::WASM); ``` The client is used to invoke the `deploy` function. The contract will deploy the test contract using the hash of its Wasm code and pass a single `5u32` argument to its constructor. We also need the `salt` to pass into the call in order to generate a unique identifier of the output contract. ```rust let salt = BytesN::from_array(&env, &[0; 32]); let constructor_args: Vec = (5u32,).into_val(&env); ``` Before invoking the contract we need to enable mock authorization in order to get the recorded authorization payload that we can verify. ```rust env.mock_all_auths(); ``` After the preparations above we can actually call the `deploy` function. ```rust let contract_id = deployer_client.deploy(&wasm_hash, &salt, &constructor_args); ``` The deployment requires authorization from the admin. As mentioned above, the authorization necessary for `deploy_v2` function is performed on behalf of the deployer contract and is implicit. This can be verified in the test by examining `env.auths()`. ```rust // An authorization from the admin is required. let expected_auth = AuthorizedInvocation { // Top-level authorized function is `deploy` with all the arguments. function: AuthorizedFunction::Contract(( deployer_client.address, symbol_short!("deploy"), (wasm_hash.clone(), salt, constructor_args).into_val(&env), )), sub_invocations: vec![], }; assert_eq!(env.auths(), vec![(admin, expected_auth)]); ``` The test checks that the test contract was deployed by using its client to invoke it and get back the value set during initialization. ```rust // Invoke contract to check that it is initialized. let client = contract::Client::new(&env, &contract_id); let sum = client.value(); assert_eq!(sum, 5); ``` ## Build the Contracts To build the contract into a `.wasm` file, use the `stellar contract build` command. Build both the deployer contract and the test contract. ```sh stellar contract build ``` Both `.wasm` files should be found in both contract `target` directories after building both contracts: ``` target/wasm32v1-none/release/soroban_deployer_contract.wasm ``` ``` target/wasm32v1-none/release/soroban_deployer_test_contract.wasm ``` ## Run the Contract If you have [`stellar-cli`] installed, you can invoke the contract function to deploy the test contract. Before deploying the test contract with the deployer, install the test contract Wasm using the `install` command. The `install` command will print out the hash derived from the Wasm file (it's not just the hash of the Wasm file itself though) which should be used by the deployer. ```sh stellar contract upload --wasm contract/target/wasm32v1-none/release/soroban_deployer_test_contract.wasm ``` The command prints out the hash as hex. It will look something like `7792a624b562b3d9414792f5fb5d72f53b9838fef2ed9a901471253970bc3b15`. We also need to deploy the `Deployer` contract: ```sh stellar contract deploy --wasm deployer/target/wasm32v1-none/release/soroban_deployer_contract.wasm --alias 1 ``` This will return the deployer address: `CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM`. Then the deployer contract may be invoked with the Wasm hash value above. ```sh stellar contract invoke --id 1 -- deploy \ --deployer CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM --salt 123 \ --wasm_hash 7792a624b562b3d9414792f5fb5d72f53b9838fef2ed9a901471253970bc3b15 \ --constructor_args '[{"u32":5}]' ``` ```powershell stellar contract invoke --id 1 -- deploy ` --deployer CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM --salt 123 ` --wasm_hash 7792a624b562b3d9414792f5fb5d72f53b9838fef2ed9a901471253970bc3b15 ` --constructor_args '[{"u32":5}]' ``` And then invoke the deployed test contract using the identifier returned from the previous command. ```sh stellar contract invoke \ --id ead19f55aec09bfcb555e09f230149ba7f72744a5fd639804ce1e934e8fe9c5d \ -- \ value ``` ```powershell stellar contract invoke ` --id ead19f55aec09bfcb555e09f230149ba7f72744a5fd639804ce1e934e8fe9c5d ` -- ` value ``` The following output should occur using the code above. ``` 5 ``` [`stellar-cli`]: ../getting-started/setup.mdx#install-the-stellar-cli --- ## Errors Define and generate errors in a smart contract. The [errors example] demonstrates how to define and generate errors in a contract that invokers of the contract can understand and handle. This example is an extension of the [storing data example]. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [errors example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/errors [storing data example]: ../getting-started/storing-data.mdx ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, navigate to the `errors` directory, and use `cargo test`. ```sh cd errors cargo test ``` You should see output that begins like this: ``` running 2 tests test test::test ... ok test test::test_panic - should panic ... ok [Diagnostic Event] contract:CAAA..., topics:[log], data:["count: {}", 0] [Diagnostic Event] contract:CAAA..., topics:[log], data:["count: {}", 1] [Diagnostic Event] contract:CAAA..., topics:[log], data:["count: {}", 2] [Diagnostic Event] contract:CAAA..., topics:[log], data:["count: {}", 3] [Diagnostic Event] contract:CAAA..., topics:[log], data:["count: {}", 4] [Diagnostic Event] contract:CAAA..., topics:[log], data:["count: {}", 5] [Failed Diagnostic Event (not emitted)] contract:CAAA..., topics:[log], data:["count: {}", 5] thread 'test::test_panic' panicked at .../src/host.rs: HostError: Error(Contract, #1) Event log (newest first): 0: [Diagnostic Event] topics:[error, Error(Contract, #1)], data:"escalating error to panic" 1: [Diagnostic Event] topics:[error, Error(Contract, #1)], data:["contract call failed", increment, []] 2: [Failed Diagnostic Event (not emitted)] contract:CAAA..., topics:[error, Error(Contract, #1)], data:"escalating Ok(ScErrorType::Contract) frame-exit to Err" 3: [Failed Diagnostic Event (not emitted)] contract:CAAA..., topics:[fn_return, increment], data:Error(Contract, #1) 4: [Failed Diagnostic Event (not emitted)] contract:CAAA..., topics:[log], data:["count: {}", 5] 5: [Diagnostic Event] topics:[fn_call, CAAA..., increment], data:Void ... successes: test::test test::test_panic test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s ``` ## Code ```rust title="errors/src/lib.rs" #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum Error { LimitReached = 1, } const COUNTER: Symbol = symbol_short!("COUNTER"); const MAX: u32 = 5; #[contract] pub struct IncrementContract; #[contractimpl] impl IncrementContract { /// Increment increments an internal counter, and returns the value. Errors /// if the value is attempted to be incremented past 5. pub fn increment(env: Env) -> Result { // Get the current count. let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0. log!(&env, "count: {}", count); // Increment the count. count += 1; // Check if the count exceeds the max. if count <= MAX { // Save the count. env.storage().instance().set(&COUNTER, &count); // Return the count to the caller. Ok(count) } else { // Return an error if the max is exceeded. Err(Error::LimitReached) } } } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/errors ## How it Works Open the [`errors/src/lib.rs`](https://github.com/stellar/soroban-examples/tree/v23.0.0/errors/src/lib.rs) file to follow along. ### Defining an Error Contract errors are Rust u32 enums where every variant of the enum is assigned an integer. The `#[contracterror]` attribute is used to set the error up so it can be used in the return value of contract functions. The enum has some constraints: - It must have the `#[repr(u32)]` attribute. - It must have the `#[derive(Copy)]` attribute. - Every variant must have an explicit integer value assigned. ```rust #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum Error { LimitReached = 1, } ``` Contract errors cannot be stored as contract data, and therefore cannot be used as types on fields of contract types. :::tip If an error is returned from a function anything the function has done is rolled back. If ledger entries have been altered, or contract data stored, all those changes are reverted and will not be persisted. ::: ### Returning an Error Errors can be returned from contract functions by returning `Result<_, E>`. The increment function returns a `Result`, which means it returns `Ok(u32)` in the successful case, and `Err(Error)` in the error case. ```rust pub fn increment(env: Env) -> Result { // ... if count <= MAX { // ... Ok(count) } else { // ... Err(Error::LimitReached) } } ``` ### Panicking with an Error Errors can also be panicked instead of being returned from the function. The increment function could also be written as follows with a `u32` return value. The error can be passed to the environment using the `panic_with_error!` macro. ```rust pub fn increment(env: Env) -> u32 { // ... if count <= MAX { // ... count } else { // ... panic_with_error!(&env, Error::LimitReached) } } ``` :::caution Functions that do not return a `Result<_, E>` type do not include in their specification what the possible error values are. This makes it more difficult for other contracts and clients to integrate with the contract. However, this might be ideal if the errors are diagnostic and debugging, and not intended to be handled. ::: ## Tests Open the [`errors/src/test.rs`](https://github.com/stellar/soroban-examples/tree/v23.0.0/errors/src/test.rs) file to follow along. ```rust title="errors/src/test.rs" #[test] fn test() { let env = Env::default(); let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.try_increment(), Ok(Ok(1))); assert_eq!(client.try_increment(), Ok(Ok(2))); assert_eq!(client.try_increment(), Ok(Ok(3))); assert_eq!(client.try_increment(), Ok(Ok(4))); assert_eq!(client.try_increment(), Ok(Ok(5))); assert_eq!(client.try_increment(), Err(Ok(Error::LimitReached))); std::println!("{}", env.logs().all().join("\n")); } #[test] #[should_panic(expected = "HostError: Error(Contract, #1)")] fn test_panic() { let env = Env::default(); let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.increment(), 1); assert_eq!(client.increment(), 2); assert_eq!(client.increment(), 3); assert_eq!(client.increment(), 4); assert_eq!(client.increment(), 5); client.increment(); } ``` In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. ```rust let env = Env::default(); ``` The contract is registered with the environment using the contract type. ```rust let contract_id = env.register(IncrementContract, ()); ``` All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `IncrementContract`, and the client is named `IncrementContractClient`. ```rust let client = IncrementContractClient::new(&env, &contract_id); ``` Two functions are generated for every contract function, one that returns a `Result<>`, and the other that does not handle errors and panics if an error occurs. ### `try_increment` In the first test the `try_increment` function is called and returns `Result, Result>`. ```rust assert_eq!(client.try_increment(), Ok(Ok(5))); assert_eq!(client.try_increment(), Err(Ok(Error::LimitReached))); ``` - If the function call is successful, `Ok(Ok(u32))` is returned. - If the function call is successful but returns a value that is not a `u32`, `Ok(Err(_))` is returned. - If the function call is unsuccessful and fails with an error in the `Error` enum, `Err(Ok(Error))` is returned. - If the function call is unsuccessful but returns an error code not in the `Error` enum, or returns a system error code, `Err(Err(InvokeError))` is returned and the `InvokeError` can be inspected. ### `increment` In the second test the `increment` function is called and returns `u32`. When the last call is made the function panics. ```rust assert_eq!(client.increment(), 5); client.increment(); ``` - If the function call is successful, `u32` is returned. - If the function call is successful but returns a value that is not a `u32`, a panic occurs. - If the function call is unsuccessful, a panic occurs. ## Build the Contract To build the contract, use the `stellar contract build` command. ```sh stellar contract build ``` A `.wasm` file should be outputted in the `target` directory: ``` target/wasm32v1-none/release/soroban_errors_contract.wasm ``` ## Run the Contract Let's deploy the contract to Testnet so we can run it. The value provided as `--source-account` was set up in our Getting Started guide; please change accordingly if you created a different identity. ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_errors_contract.wasm \ --source-account alice \ --network testnet ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/soroban_errors_contract.wasm ` --source-account alice ` --network testnet ``` The command above will output the contract id, which in our case is `CA4KWO3HL6M5F5MZ5ITVFLCQ6ZM2GDSP2NOTNXOT4GIRECQOVX3I6CXL`. Now that we've deployed the contract, we can invoke it. ```sh stellar contract invoke \ --id CA4KWO3HL6M5F5MZ5ITVFLCQ6ZM2GDSP2NOTNXOT4GIRECQOVX3I6CXL \ --network testnet \ --source-account alice \ -- \ increment ``` ```powershell stellar contract invoke ` --id CA4KWO3HL6M5F5MZ5ITVFLCQ6ZM2GDSP2NOTNXOT4GIRECQOVX3I6CXL ` --network testnet ` --source-account alice ` -- ` increment ``` Run the command a few times and on the 6th invocation you should see an error like this: ``` ❌ error: transaction simulation failed: HostError: Error(Contract, #1) Event log (newest first): 0: [Diagnostic Event] contract:CA4KWO3HL6M5F5MZ5ITVFLCQ6ZM2GDSP2NOTNXOT4GIRECQOVX3I6CXL, topics:[error, Error(Contract, #1)], data:"escalating Ok(ScErrorType::Contract) frame-exit to Err" 1: [Diagnostic Event] topics:[fn_call, CA4KWO3HL6M5F5MZ5ITVFLCQ6ZM2GDSP2NOTNXOT4GIRECQOVX3I6CXL, increment], data:Void ``` To retrieve the current counter value, use the command `stellar contract read`. ```sh stellar contract read \ --id CA4KWO3HL6M5F5MZ5ITVFLCQ6ZM2GDSP2NOTNXOT4GIRECQOVX3I6CXL \ --network testnet \ --source-account alice \ --durability persistent \ --output json ``` ```powershell stellar contract read ` --id CA4KWO3HL6M5F5MZ5ITVFLCQ6ZM2GDSP2NOTNXOT4GIRECQOVX3I6CXL ` --network testnet ` --source-account alice ` --durability persistent ` --output json ``` [`stellar-cli`]: ../getting-started/setup.mdx#install-the-stellar-cli --- ## Events Publish events from a smart contract. The [events example] demonstrates how to publish events from a contract. This example is an extension of the [storing data example]. :::tip[Whisk Changes] With the release of Whisk, Protocol 23, the syntax for publishing smart contract events has changed. In order to provide the most up-to-date information, this example has been updated to include the new patterns. Find more detailed information in the [Rust SDK documentation]. ::: [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [events example]: https://github.com/stellar/soroban-examples/tree/main/events [storing data example]: ../getting-started/storing-data.mdx [Rust SDK documentation]: https://docs.rs/soroban-sdk/latest/soroban_sdk/_migrating/v23_contractevent/index.html ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `main` branch of the `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ```sh git clone -b main https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, navigate to the `events` directory, and use `cargo test`. ```sh cd events cargo test ``` You should see the output: ``` running 1 test test test::test ... ok ``` ## Code ```rust title="events/src/lib.rs" const COUNTER: Symbol = symbol_short!("COUNTER"); // Define two static topics for the event: "COUNTER" and "increment". // Also set the data format to "single-value", which means that the event data // payload will contain a single value not nested into any data structure. #[contractevent(topics = ["COUNTER", "increment"], data_format = "single-value")] struct IncrementEvent { count: u32, } #[contract] pub struct IncrementContract; #[contractimpl] impl IncrementContract { /// Increment increments an internal counter, and returns the value. pub fn increment(env: Env) -> u32 { // Get the current count. let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0. // Increment the count. count += 1; // Save the count. env.storage().instance().set(&COUNTER, &count); // Publish an event about the increment occuring. // The event has two static topics ("COUNTER", "increment") and actual // count as the data payload. IncrementEvent { count }.publish(&env); // Return the count to the caller. count } } ``` Ref: https://github.com/stellar/soroban-examples/tree/main/events ## How it Works This example contract extends the increment example by publishing an event each time the counter is incremented. Contract events let contracts emit information about what their contract is doing. Contracts can publish events creating a defined `struct` and `publish`ing it to the smart contract environments. First, the `#[contractevent]` struct must be defined ```rust #[contractevent(topics = ["COUNTER", "increment"], data_format = "single-value")] struct IncrementEvent { count: u32, } ``` Then, inside the contract's function, we can create and publish the event with the relevant data. ```rust IncrementEvent { count }.publish(&env); ``` ### Event Topics Topics can be defined either statically or dynamically. In the sample code two static topics are used, which will be of the `Symbol` type: `COUNTER` and `increment`. ```rust #[contractevent(topics = ["COUNTER", "increment"], ...)] ``` :::tip The topics don't have to be made of the same type. ::: Topics can also be defined dynamically, inside the struct. In this case, the struct's `snake_case` name will be the first topic. For example, the following event will have two topics: the `Symbol` "increment", followed by an `Address`. ```rust #[contractevent] pub struct Increment { #[topic] addr: Address, count: u32, } ``` ### Event Data An event also contains a data object of any value or type including types defined by contracts using `#[contracttype]`. In the example the data is the `u32` count. The `data_format = "single-value"` tells the event to publish the data alone, with no surrounding data structure. ```rust #[contractevent(..., data_format = "single-value")] ``` Event data will, by default, conform to the data structure in the defined `struct`. The `data_format` can also be specified as `vec` or `single-value`. Again, please refer to the [Rust SDK documentation] for more details. ### Publishing Publishing an event is done by calling the `publish` function on the created event struct. The function returns nothing on success, and panics on failure. Possible failure reasons can include malformed inputs (e.g. topic count exceeds limit) and running over the resource budget (TBD). Once successfully published, the new event will be available to applications consuming the events. ```rust IncrementEvent { count }.publish(&env); ``` :::caution Published events are discarded if a contract invocation fails due to a panic, budget exhaustion, or when the contract returns an error. ::: ## Tests Open the [`events/src/test.rs`](https://github.com/stellar/soroban-examples/tree/main/events/src/test.rs) file to follow along. ```rust title="events/src/test.rs" #[test] fn test() { let env = Env::default(); let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); // Assert on the events emitted by the last contract invocation using // the contract's event struct defined with #[contractevent] macro to // construct the expected event in xdr form for comparison. assert_eq!(client.increment(), 1); assert_eq!( env.events().all(), [IncrementEvent { count: 1 }.to_xdr(&env, &contract_id)] ); // Assert on the events emitted by the last contract invocation that // were emitted by a contract with a specific contract id. This is // useful when your contract might call other contracts that also emit events. assert_eq!(client.increment(), 2); assert_eq!( env.events().all().filter_by_contract(&contract_id), [IncrementEvent { count: 2 }.to_xdr(&env, &contract_id)] ); // Assert on the events emitted by the last contract invocation by // building a tuple form of the event manually. This is useful // when the contract does not define its events using the #[contractevent] macro. // // Tuple Format: (contract_id: Address, topics: Val, data: Val) assert_eq!(client.increment(), 3); assert_eq!( env.events().all(), soroban_sdk::vec![ &env, ( contract_id, (symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env), 3u32.into_val(&env) ), ] ); } ``` In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. ```rust let env = Env::default(); ``` The contract is registered with the environment using the contract type. ```rust let contract_id = env.register(IncrementContract, ()); ``` All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `IncrementContract`, and the client is named `IncrementContractClient`. ```rust let client = IncrementContractClient::new(&env, &contract_id); ``` The example invokes the contract several times. ```rust assert_eq!(client.increment(), 1); ``` The example asserts that the event was published. `env.events().all()` returns a `ContractEvents` value containing all the events published by the last contract invocation, which can be compared in a few different ways. The event struct defined with `#[contractevent]` can be converted to its XDR form with `to_xdr` and compared directly, which is the most convenient way to assert on events when the contract defines them using the macro. ```rust assert_eq!( env.events().all(), [IncrementEvent { count: 1 }.to_xdr(&env, &contract_id)] ); ``` `filter_by_contract` narrows the events down to those emitted by a specific contract address, which is useful when a contract invocation calls into other contracts that also emit events. ```rust assert_eq!( env.events().all().filter_by_contract(&contract_id), [IncrementEvent { count: 2 }.to_xdr(&env, &contract_id)] ); ``` The event can also be compared by building its tuple form manually, as `(contract_id, topics, data)`, which is useful when the contract does not define its events using the `#[contractevent]` macro. ```rust assert_eq!( env.events().all(), soroban_sdk::vec![ &env, ( contract_id, (symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env), 3u32.into_val(&env) ), ] ); ``` ## Build the Contract To build the contract, use the `stellar contract build` command. ```sh stellar contract build ``` A `.wasm` file should be outputted in the `target` directory: ``` target/wasm32v1-none/release/soroban_events_contract.wasm ``` ## Run the Contract If you have [`stellar-cli`] installed, you can deploy the contract and invoke its functions. ### Deploy ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_events_contract.wasm \ --alias events_example \ --source-account alice \ --network testnet ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/soroban_events_contract.wasm ` --alias events_example ` --source-account alice ` --network testnet ``` ### Invoke ```sh stellar contract invoke \ --id events_example \ --source-account alice \ --network testnet \ -- \ increment ``` ```powershell stellar contract invoke ` --id events_example ` --source-account alice ` --network testnet ` -- ` increment ``` The following output should occur using the code above. ``` 📅 CAAA... - Success - Event: [{"symbol":"COUNTER"},{"symbol":"increment"}] = {"u32":1} 1 ``` A single event is outputted, which is the contract event the contract published. The event contains the two topics, each a `Symbol`, and the data object containing the `u32`. [`stellar-cli`]: ../getting-started/setup.mdx#install-the-stellar-cli --- ## Fuzz Testing {`Increase confidence in a contract's correctness with fuzz testing.`} The [fuzzing example] demonstrates how to fuzz test Soroban contracts with [`cargo-fuzz`] and customize the input to fuzz tests with the [`arbitrary`] crate. It also demonstrates how to adapt fuzz tests into reusable property tests with the [`proptest`] and [`proptest-arbitrary-interop`] crates. It builds on the [timelock example]. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [fuzzing example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/fuzzing [`cargo-fuzz`]: https://docs.rs/cargo-fuzz [`arbitrary`]: https://docs.rs/arbitrary [`proptest`]: https://docs.rs/proptest [`proptest-arbitrary-interop`]: https://docs.rs/proptest-arbitrary-interop [timelock example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/timelock ## Run the Example First go through the [setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [setup]: ../../smart-contracts/getting-started/setup.mdx ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` You will also need the `cargo-fuzz` tool, and to run `cargo-fuzz` you will need a nightly Rust toolchain: ```sh cargo install cargo-fuzz rustup install nightly ``` To run one of the fuzz tests, navigate to the `fuzzing` directory and run the `cargo fuzz` subcommand with the `nightly` toolchain: ```sh cd fuzzing cargo +nightly fuzz run fuzz_target_1 ``` :::info If you're developing on MacOS you may need to add the `--sanitizer=thread` flag in order to fix some [known linking errors](https://github.com/stellar/rs-soroban-sdk/issues/1056). ::: You should see output that begins like this: ```sh $ cargo +nightly fuzz run fuzz_target_1 Compiling soroban-fuzzing-contract v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing) Compiling soroban-fuzzing-contract-fuzzer v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz) Finished release [optimized + debuginfo] target(s) in 23.74s Finished release [optimized + debuginfo] target(s) in 0.07s Running `fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1 ...` INFO: Running with entropic power schedule (0xFF, 100). INFO: Seed: 886588732 INFO: Loaded 1 modules (1093478 inline 8-bit counters): 1093478 [0x55eb8e2c7620, 0x55eb8e3d2586), INFO: Loaded 1 PC tables (1093478 PCs): 1093478 [0x55eb8e3d2588,0x55eb8f481be8), INFO: 105 files found in /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/corpus/fuzz_target_1 INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes INFO: seed corpus: files: 105 min: 32b max: 61b total: 3558b rss: 86Mb #2 pulse ft: 8355 exec/s: 1 rss: 307Mb #4 pulse cov: 8354 ft: 11014 corp: 1/32b exec/s: 2 rss: 313Mb #8 pulse cov: 8495 ft: 12420 corp: 4/128b exec/s: 4 rss: 315Mb ``` The rest of this tutorial will explain how to set up this fuzz test, interpret this output, and remedy fuzzing failures. ## Background: Fuzz Testing and Rust Fuzzing is a kind of testing where new inputs are repeatedly fed into a program in hopes of finding unexpected bugs. This style of testing is commonly employed to increase confidence in the correctness of security-sensitive software. In Rust, fuzzing is most often performed with the [`cargo-fuzz`] tool, which drives LLVM's [`libfuzzer`], though other fuzzing tools are available. [`libfuzzer`]: https://llvm.org/docs/LibFuzzer.html Soroban has built-in support for fuzzing Soroban contracts with `cargo-fuzz`. `cargo-fuzz` is a mutation-based fuzzer: it runs a test program, passing it generated input; while the program is executing, the fuzzer monitors which branches the program takes, and which functions it executes; after execution the fuzzer uses this information to make decisions about how to _mutate_ the previously-used input to create new input that might discover more branches and functions; it then runs the test again with new input, repeating this process for potentially millions of iterations. In this way `cargo-fuzz` is able to automatically explore execution paths through the program that may never be seen by other types of tests. If a fuzz tests panics or hard-crashes, `cargo-fuzz` considers it a failure and provides instructions for repeating the test with the failing inputs. Fuzz testing is typically an exploratory and interactive process, with the programmer devising schemes for producing input that will stress the program in interesting ways, observing the behavior of the fuzz test, and iterating on the test itself. Resolving a fuzz testing failure typically involves capturing the problematic input in a unit test. The fuzz test itself may or may not be kept, depending on determinations about the cost of maintaining the fuzzer vs the likelihood of it continuing to find bugs in the future. While fuzzing non-memory-safe software tends to be more lucrative than fuzzing Rust software, it is still relatively common to find panics and other logic errors in Rust through fuzzing. In Rust, multiple fuzzers are maintained by the [`rust-fuzz`] GitHub organization, which also maintains a "trophy case" of Rust bugs found through fuzzing. [`rust-fuzz`]: https://github.com/rust-fuzz ## About the Example The example used for this tutorial is based on the [`timelock`] example program, with some changes to demonstrate fuzzing. [`timelock`]: https://github.com/stellar/soroban-examples/tree/v23.0.0/timelock The contract, `ClaimableBalanceContract`, allows one party to deposit an arbitrary quantity of a token to the contract, specifying additionally: the `claimants`, addresses that may withdraw from the contract; and the `time_bound`, a specification of when those claimants may withdraw from the account. The `TimeBound` type looks like ```rust #[derive(Clone, Debug)] #[contracttype] pub enum TimeBoundKind { Before, After, } #[derive(Clone, Debug)] #[contracttype] pub struct TimeBound { pub kind: TimeBoundKind, pub timestamp: u64, } ``` `ClaimableBalanceContract` has two methods, `deposit` and `claim`: ```rust pub fn deposit( env: Env, from: Address, token: Address, amount: i128, claimants: Vec
, time_bound: TimeBound, ); pub fn claim( env: Env, claimant: Address, amount: i128, ); ``` `deposit` may only be successfully called once, after which `claim` may be called multiple times until the balance is completely drained, at which point the contract becomes dormant and may no longer be used. ## Fuzz Testing Setup For these examples, the fuzz tests have been created for you, but normally you would use the `cargo fuzz init` command to create a fuzzing project as a subdirectory of the contract under test. To do that you would navigate to the contract directory, in this case, `soroban-examples/fuzzing`, and execute ```sh cargo fuzz init ``` A `cargo-fuzz` project is its own crate, which lives in the `fuzz` subdirectory of the crate being tested. This crate has its own `Cargo.toml` and `Cargo.lock`, and another subdirectory, `fuzz_targets`, which contains Rust programs, each its own fuzz test. Our `soroban-examples/fuzzing` directory looks like - `Cargo.toml` - this is the contract's manifest - `Cargo.lock` - `src` - `lib.rs` - this is the contract code - `fuzz` - this is the fuzzing crate - `Cargo.toml` - this is fuzzing crate's manifest - `Cargo.lock` - `fuzz_targets` - `fuzz_target_1.rs` - this is a single fuzz test - `fuzz_target_2.rs` There are special considerations to note in the configuration of both the [contract's manifest] and the [fuzzing crate's manifest]. [contract's manifest]: https://github.com/stellar/soroban-examples/tree/v23.0.0/fuzzing/Cargo.toml [fuzzing crate's manifest]: https://github.com/stellar/soroban-examples/tree/v23.0.0/fuzzing/fuzz/Cargo.toml Within the contract's manifest one must specificy the crate type as both "cdylib" and "rlib": ```toml title="fuzzing/Cargo.toml" [package] name = "soroban-fuzzing-contract" version = "0.0.0" edition = "2021" publish = false rust-version = "1.89.0" [lib] crate-type = ["cdylib", "rlib"] doctest = false [features] testutils = [] ``` In most examples, a Soroban contract will only be a "cdylib", a Rust crate that is compiled to a dynamically loadable Wasm module. For fuzzing though, the fuzzing crate needs to be able to link to the contract crate as a Rust library, an "rlib". :::note Note that cargo has a [feature/bug that inhibits LTO][lto] of cdylibs when a crate is both a "cdylib" and "rlib". This can be worked around by building the contract with either `soroban contract build` or `cargo rustc --crate-type cdylib` instead of the typical `cargo build`. ::: [lto]: https://github.com/stellar/soroban-docs/pull/476 The contract crate must also provide the "testutils" feature. When "testutils" is activated, the Soroban SDK's [`contracttype`] macro emits additional code needed for running fuzz tests. [`contracttype`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/attr.contracttype.html Within the fuzzing crate's manifest one must turn on the "testutils" features in both the contract crate and the `soroban-sdk` crate: ```toml title="fuzzing/fuzz/Cargo.toml" [package] name = "soroban-fuzzing-contract-fuzzer" version = "0.0.0" publish = false edition = "2021" rust-version = "1.79.0" [package.metadata] cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" soroban-sdk = { version = "23.0.1", features = ["testutils"] } soroban-env-host = { version = "23.0.1" } soroban-ledger-snapshot = { version = "23.0.1" } [dependencies.soroban-fuzzing-contract] path = ".." features = ["testutils"] ``` ## A Simple Fuzz Test First let's look at [`fuzz_target_1.rs`]. This fuzz test does two things: it first deposits an arbitrary amount, then it claims an arbitrary amount. [`fuzz_target_1.rs`]: https://github.com/stellar/soroban-examples/tree/v23.0.0/fuzzing/fuzz/fuzz_targets/fuzz_target_1.rs Again, you can run this fuzzer from the `soroban-examples/fuzzing` directory with the following command: ```sh cargo +nightly fuzz run fuzz_target_1 ``` The entry point and setup code for Soroban contract fuzz tests will typically look like: ```rust title="fuzzing/fuzz/fuzz_targets/fuzz_target_1.rs" #[derive(Arbitrary, Debug)] struct Input { deposit_amount: i128, claim_amount: i128, } fuzz_target!(|input: Input| { let env = Env::default(); env.mock_all_auths(); env.ledger().set(LedgerInfo { timestamp: 12345, protocol_version: 1, sequence_number: 10, network_id: Default::default(), base_reserve: 10, min_temp_entry_ttl: u32::MAX, min_persistent_entry_ttl: u32::MAX, max_entry_ttl: u32::MAX, }); // Turn off the CPU/memory budget for testing. env.cost_estimate().budget().reset_unlimited(); // ... do fuzzing here ... } ``` Instead of a `main` function, `cargo-fuzz` uses a special entry point defined by the [`fuzz_target!`] macro. This macro accepts a Rust closure that accepts `input`, any Rust type that implements the [`Arbitrary`] trait. Here we have defined a struct, `Input`, that derives `Arbitrary`. [`fuzz_target!`]: https://docs.rs/libfuzzer-sys/latest/libfuzzer_sys/macro.fuzz_target.html [`arbitrary`]: https://docs.rs/arbitrary/latest/arbitrary/trait.Arbitrary.html `cargo-fuzz` will be responsible for generating `input` and repeatedly calling this closure. To test a Soroban contract, we must set up an [`Env`]. Note that we have disabled the CPU and memory budget: this will allow us to fuzz arbitrarily complex code paths without worrying about running out of budget; we can assume that running out of budget during a transaction always correctly fails, canceling the transaction; it is not something we need to fuzz. [`env`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Env.html Refer to the [`fuzz_target_1.rs`] source code for additional setup for this contract. This fuzzer performs two steps: deposit, then claim: ```rust title="fuzzing/fuzz/fuzz_targets/fuzz_target_1.rs" // Deposit, then assert invariants. { let _ = timelock_client.try_deposit( &depositor_address, &token_contract_id, &input.deposit_amount, &vec![&env, claimant_address.clone()], &TimeBound { kind: TimeBoundKind::Before, timestamp: 123456, }, ); assert_invariants(&env, &timelock_contract_id, &token_client, &input); } // Claim, then assert invariants. { let _ = timelock_client.try_claim(&claimant_address, &input.claim_amount); assert_invariants(&env, &timelock_contract_id, &token_client, &input); } ``` There are a number of potential strategies for writing fuzz tests. The strategy in this test is to make arbitrary, possibly weird and unrealistic, calls to the contract, disregarding whether those calls succeed or fail, and then to make assertions about the state of the contract. Because there are many potential failure cases for any given contract call, we don't want to write a fuzz test by attempting to interpret the success or failure of any given call: that path leads to duplicating the contract's logic within the fuzz test. Instead we just want to ensure that, regardless of what happened during execution, the contract is never left in an invalid state. Notice the use of the `try_` client function to invoke the contract. Each contract function can be invoked with a `try_` variant, which captures any errors, including panics and crashes, and returns the value on success or an error otherwise. Without using the `try_` variant, a panic from within a contract will immediately cause the fuzz test to fail, but in most cases a panic within a contract does not indicate a bug - it is simply how a Soroban contract cancels a transaction. `try_` returns a `Result`, but here we discard it. Finally, the `assert_invariants` function is where we make any assertions we can about the state of the contract: ```rust title="fuzzing/fuzz/fuzz_targets/fuzz_target_1.rs" /// Directly inspect the contract state and make assertions about it. fn assert_invariants( env: &Env, timelock_contract_id: &Address, token_client: &TokenClient, input: &Input, ) { // Configure the environment to access the timelock contract's storage. env.as_contract(timelock_contract_id, || { let storage = env.storage().persistent(); // Get the two datums owned by the timelock contract. let is_initialized = storage.has(&DataKey::Init); let claimable_balance = storage.get::<_, ClaimableBalance>(&DataKey::Balance); // Call the token client to get the balance held in the timelock contract. // This consumes contract execution budget. let actual_token_balance = token_client.balance(timelock_contract_id); // There can only be a claimaible balance after the contract is initialized, // but once the balance is claimed there is no balance, // but the contract remains initialized. // This is a truth table of valid states. assert!(match (is_initialized, claimable_balance.is_some()) { (false, false) => true, (false, true) => false, (true, true) => true, (true, false) => true, }); assert!(actual_token_balance >= 0); if let Some(claimable_balance) = claimable_balance { assert!(claimable_balance.amount > 0); assert!(claimable_balance.amount <= input.deposit_amount); assert_eq!(claimable_balance.amount, actual_token_balance); assert!(claimable_balance.claimants.len() > 0); } }); } ``` ## Interpreting `cargo-fuzz` Output If you run `cargo-fuzz` with `fuzz_target_1`, from inside the `soroban-examples/fuzzing` directory, you will see output similar to: ```text $ cargo +nightly fuzz run fuzz_target_1 Compiling soroban-fuzzing-contract v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing) Compiling soroban-fuzzing-contract-fuzzer v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz) Finished release [optimized + debuginfo] target(s) in 25.18s Finished release [optimized + debuginfo] target(s) in 0.08s Running `fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1 -artifact_prefix=/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/artifacts/fuzz_target_1/ /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/corpus/fuzz_target_1` INFO: Running with entropic power schedule (0xFF, 100). INFO: Seed: 1384064486 INFO: Loaded 1 modules (1122058 inline 8-bit counters): 1122058 [0x561f6ecd4fc0, 0x561f6ede6eca), INFO: Loaded 1 PC tables (1122058 PCs): 1122058 [0x561f6ede6ed0,0x561f6ff05f70), INFO: 173 files found in /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/corpus/fuzz_target_1 INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes INFO: seed corpus: files: 173 min: 32b max: 61b total: 6039b rss: 83Mb #4 pulse cov: 4848 ft: 10214 corp: 1/32b exec/s: 2 rss: 313Mb #8 pulse cov: 8507 ft: 11743 corp: 4/128b exec/s: 4 rss: 315Mb #16 pulse cov: 8512 ft: 12393 corp: 10/320b exec/s: 8 rss: 319Mb thread '' panicked at 'assertion failed: claimable_balance.amount > 0', fuzz_targets/fuzz_target_1.rs:130:13 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ==6102== ERROR: libFuzzer: deadly signal #0 0x561f6ae3a431 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1c80431) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) #1 0x561f6e3855b0 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x51cb5b0) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) #2 0x561f6e35c08a (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x51a208a) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) #3 0x7fce05f5e08f (/lib/x86_64-linux-gnu/libc.so.6+0x4308f) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) #4 0x7fce05f5e00a (/lib/x86_64-linux-gnu/libc.so.6+0x4300a) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) #5 0x7fce05f3d858 (/lib/x86_64-linux-gnu/libc.so.6+0x22858) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) ... #27 0x561f6e3847b9 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x51ca7b9) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) #28 0x561f6ad98346 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1bde346) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) #29 0x7fce05f3f082 (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) #30 0x561f6ad9837d (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1bde37d) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) NOTE: libFuzzer has rudimentary signal handlers. Combine libFuzzer with AddressSanitizer or similar for better crash reports. SUMMARY: libFuzzer: deadly signal MS: 0 ; base unit: 0000000000000000000000000000000000000000 0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x5d,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0x5f,0x5f,0x52,0xff, \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000]\000\000\000\000\000\000\000\000\377__R\377 artifact_prefix='/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/artifacts/fuzz_target_1/'; Test unit written to /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 Base64: AAAAAAAAAAAAAAAAAAAAAAAAXQAAAAAAAAAA/19fUv8= ──────────────────────────────────────────────────────────────────────────────── Failing input: fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 Output of `std::fmt::Debug`: Input { deposit_amount: 0, claim_amount: -901525218878596739118967460911579136, } Reproduce with: cargo fuzz run fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 Minimize test case with: cargo fuzz tmin fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 ──────────────────────────────────────────────────────────────────────────────── Error: Fuzz target exited with exit status: 77 ``` This is a fuzzing failure, indicating a bug in either the fuzzer or the program. The details will be different. Here is the same output, with less important lines trimmed: ```text thread '' panicked at 'assertion failed: claimable_balance.amount > 0', fuzz_targets/fuzz_target_1.rs:130:13 ... Failing input: fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 Output of `std::fmt::Debug`: Input { deposit_amount: 0, claim_amount: -901525218878596739118967460911579136, } Reproduce with: cargo fuzz run fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 Minimize test case with: cargo fuzz tmin fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 ``` The first line here is printed by our Rust program, and indicates exactly where the fuzzer panicked. The later lines indicate how to reproduce this failing case. The first thing to do when you get a fuzzing failure is copy the command to reproduce the failure, so that you can use it to debug: ```sh cargo +nightly fuzz run fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 ``` Notice though that we need to tell `cargo` to use the nightly toolchain with the `+nightly` flag, something that `cargo-fuzz` doesn't print in its version of the command. Another thing to notice is that by default, `cargo-fuzz` / `libfuzzer` does not print names of functions in its output, as in the stack trace: ```text ==6102== ERROR: libFuzzer: deadly signal #0 0x561f6ae3a431 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1c80431) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) ... #28 0x561f6ad98346 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1bde346) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) #29 0x7fce05f3f082 (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) #30 0x561f6ad9837d (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1bde37d) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) ``` Depending on how your system is set up, you may or may not have this problem. In order to print stack traces, `libfuzzer` needs the `llvm-symbolizer` program. On Ubuntu-based systems this can be installed with the `llvm-dev` package: ```sh sudo apt install llvm-dev ``` After which `libfuzzer` will print demangled function names instead of addresses: ```text ==6323== ERROR: libFuzzer: deadly signal #0 0x557c9da6a431 in __sanitizer_print_stack_trace /rustc/llvm/src/llvm-project/compiler-rt/lib/asan/asan_stack.cpp:87:3 #1 0x557ca0fb55b0 in fuzzer::PrintStackTrace() /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libfuzzer-sys-0.4.5/libfuzzer/FuzzerUtil.cpp:210:38 #2 0x557ca0f8c08a in fuzzer::Fuzzer::CrashCallback() /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:233:18 #3 0x557ca0f8c08a in fuzzer::Fuzzer::CrashCallback() /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:228:6 #4 0x7ff19e84d08f (/lib/x86_64-linux-gnu/libc.so.6+0x4308f) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) #5 0x7ff19e84d00a in __libc_signal_restore_set /build/glibc-SzIz7B/glibc-2.31/signal/../sysdeps/unix/sysv/linux/internal-signals.h:86:3 #6 0x7ff19e84d00a in raise /build/glibc-SzIz7B/glibc-2.31/signal/../sysdeps/unix/sysv/linux/raise.c:48:3 #7 0x7ff19e82c858 in abort /build/glibc-SzIz7B/glibc-2.31/stdlib/abort.c:79:7 ... #23 0x557c9daee89a in fuzz_target_1::assert_invariants::hd6d4f9549b01c31c /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/fuzz_targets/fuzz_target_1.rs:103:5 #24 0x557c9daee89a in fuzz_target_1::_::run::hac1117cb3dfecb2b /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/fuzz_targets/fuzz_target_1.rs:69:9 #25 0x557c9daecea6 in rust_fuzzer_test_input /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libfuzzer-sys-0.4.5/src/lib.rs:297:60 ... #37 0x557c9d9c8346 in main /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libfuzzer-sys-0.4.5/libfuzzer/FuzzerMain.cpp:20:30 #38 0x7ff19e82e082 in __libc_start_main /build/glibc-SzIz7B/glibc-2.31/csu/../csu/libc-start.c:308:16 #39 0x557c9d9c837d in _start (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1bde37d) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) ``` To continue, our program has a bug that should be easy to fix by inspecting the error and making a slight modification to the source. Once the bug is fixed, the fuzzer will run continuously, producing output that looks like ```sh $ cargo +nightly fuzz run fuzz_target_1 Compiling soroban-fuzzing-contract v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing) Compiling soroban-fuzzing-contract-fuzzer v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz) Finished release [optimized + debuginfo] target(s) in 24.91s Finished release [optimized + debuginfo] target(s) in 0.08s Running `fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1 -artifact_prefix=/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/artifacts/fuzz_target_1/ /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/corpus/fuzz_target_1` INFO: Running with entropic power schedule (0xFF, 100). INFO: Seed: 1619748028 INFO: Loaded 1 modules (1122061 inline 8-bit counters): 1122061 [0x5647a55b9080, 0x5647a56caf8d), INFO: Loaded 1 PC tables (1122061 PCs): 1122061 [0x5647a56caf90,0x5647a67ea060), INFO: 173 files found in /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/corpus/fuzz_target_1 INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes INFO: seed corpus: files: 173 min: 32b max: 61b total: 6039b rss: 85Mb #2 pulse ft: 8067 exec/s: 1 rss: 312Mb #4 pulse cov: 8068 ft: 10709 corp: 1/32b exec/s: 2 rss: 315Mb #8 pulse cov: 8476 ft: 11498 corp: 5/160b exec/s: 4 rss: 317Mb #16 pulse cov: 8512 ft: 12362 corp: 9/288b exec/s: 8 rss: 320Mb #32 pulse cov: 8516 ft: 13290 corp: 19/608b exec/s: 10 rss: 326Mb #64 pulse cov: 8516 ft: 13311 corp: 27/864b exec/s: 21 rss: 340Mb #128 pulse cov: 8540 ft: 13536 corp: 37/1196b exec/s: 25 rss: 365Mb #175 INITED cov: 8540 ft: 13580 corp: 42/1387b exec/s: 29 rss: 382Mb #177 NEW cov: 8545 ft: 13821 corp: 43/1419b lim: 48 exec/s: 29 rss: 384Mb L: 32/48 MS: 1 ChangeASCIIInt- #178 NEW cov: 8545 ft: 13824 corp: 44/1451b lim: 48 exec/s: 29 rss: 384Mb L: 32/48 MS: 1 ChangeBinInt- #229 NEW cov: 8545 ft: 13826 corp: 45/1483b lim: 48 exec/s: 38 rss: 401Mb L: 32/48 MS: 1 ChangeByte- #256 pulse cov: 8545 ft: 13826 corp: 45/1483b lim: 48 exec/s: 36 rss: 410Mb #361 NEW cov: 8545 ft: 13830 corp: 46/1521b lim: 48 exec/s: 40 rss: 451Mb L: 38/48 MS: 5 ShuffleBytes-CMP-EraseBytes-CopyPart-ChangeBinInt- DE: "\005\000\000\000"- NEW_FUNC[1/1]: 0x5647a2964640 in rand::rngs::adapter::reseeding::ReseedingCore$LT$R$C$Rsdr$GT$::reseed_and_generate::ha760ded93293681c /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/rand-0.7.3/src/rngs/adapter/reseeding.rs:235 #368 NEW cov: 8557 ft: 13842 corp: 47/1566b lim: 48 exec/s: 40 rss: 454Mb L: 45/48 MS: 2 CrossOver-InsertRepeatedBytes- #512 pulse cov: 8557 ft: 13842 corp: 47/1566b lim: 48 exec/s: 46 rss: 502Mb #850 NEW cov: 8557 ft: 13843 corp: 48/1610b lim: 48 exec/s: 53 rss: 591Mb L: 44/48 MS: 2 CopyPart-ChangeBit- #1024 pulse cov: 8557 ft: 13843 corp: 48/1610b lim: 48 exec/s: 56 rss: 645Mb #1796 NEW cov: 8557 ft: 13863 corp: 49/1642b lim: 53 exec/s: 71 rss: 669Mb L: 32/48 MS: 1 ChangeBinInt- #1913 NEW cov: 8557 ft: 13864 corp: 50/1675b lim: 53 exec/s: 73 rss: 669Mb L: 33/48 MS: 2 ShuffleBytes-InsertByte- #3749 REDUCE cov: 8557 ft: 13864 corp: 50/1670b lim: 68 exec/s: 98 rss: 669Mb L: 39/48 MS: 1 EraseBytes- ... ``` And this output will continue until the fuzzer is killed with `Ctrl-C`. Next, let's look at a single line of fuzzer output: ```text #177 NEW cov: 8545 ft: 13821 corp: 43/1419b lim: 48 exec/s: 29 rss: 384Mb L: 32/48 MS: 1 ChangeASCIIInt- ``` The most important column here is `cov`. This is a cumulative measure of branches covered by the fuzzer. When this number stops increasing the fuzzer has probably explored as much of the program as it can. The other columns are described in the [`libfuzzer` documentation][lfout]. [lfout]: https://llvm.org/docs/LibFuzzer.html#output Finally, lets look at this warning: ```text INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes. ``` By default, `libfuzzer` only generates input up to 4096 bytes. In a lot of cases, this is probably reasonable, but `cargo-fuzz` can increase the `max_len` by appending the argument after `--`: ```sh cargo +nightly fuzz run fuzz_target_1 -- -max_len=20000 ``` All the options to libfuzzer can be listed with ```sh cargo +nightly fuzz run fuzz_target_1 -- -help=1 ``` See the [`libfuzzer` documentation] for more. [`libfuzzer` documentation]: https://llvm.org/docs/LibFuzzer.html#output ## Accepting Soroban Types as Input with the `SorobanArbitrary` Trait Inputs to the `fuzz_target!` macro must implement the [`Arbitrary`] trait, which accepts bytes from the fuzzer driver and converts them to Rust values. Soroban types though are managed by the host environment, and so must be created from an [`Env`] value, which is not available to the fuzzer driver. The [`SorobanArbitrary`] trait, implemented for all Soroban contract types, exists to bridge this gap: it defines a _prototype_ pattern whereby the `fuzz_target` macro creates prototype values that the fuzz program can convert to contract values with the standard soroban conversion traits, [`FromVal`] or [`IntoVal`]. [`sorobanarbitrary`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/testutils/arbitrary/trait.SorobanArbitrary.html [`fromval`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/trait.FromVal.html [`intoval`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/trait.IntoVal.html The types of prototypes are identified by the associated type, `SorobanArbitrary::Prototype`: ```rust pub trait SorobanArbitrary: TryFromVal + IntoVal + TryFromVal { type Prototype: for<'a> Arbitrary<'a>; } ``` Types that implement `SorobanArbitrary` include: - `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, [`I256`], [`U256`], `()`, and `bool`, - [`Error`], - [`Bytes`], [`BytesN`], [`Vec`], [`Map`], - [`Address`], [`Symbol`], - [`Val`], [`i256`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.I256.html [`u256`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.U256.html [`error`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/xdr/enum.Error.html [`bytes`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Bytes.html [`bytesn`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.BytesN.html [`vec`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Vec.html [`map`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Map.html [`address`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Address.html [`symbol`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Symbol.html [`val`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Val.html All user-defined contract types, those with the [`contracttype`] attribute, automatically derive `SorobanArbitrary`. Note that `SorobanArbitrary` is only derived when the "testutils" Cargo feature is active. This implies that, in general, to make a Soroban contract fuzzable, the contract crate must define a "testutils" Cargo feature, that feature should turn on the "soroban-sdk/testutils" feature, and the fuzz test, which is its own crate, must turn that feature on. ## A More Complex Fuzz Test The [`fuzz_target_2.rs`] example, demonstrates the use of `SorobanArbitrary`, the advancement of time, and more advanced fuzzing techniques. [`fuzz_target_2.rs`]: https://github.com/stellar/soroban-examples/tree/v23.0.0/fuzzing/fuzz/fuzz_targets/fuzz_target_2.rs This fuzz test takes a much more complex input, where some of the values are user-defined types exported from the contract under test. This test is structured as a simple interpreter, where the fuzzing harness provides arbitrarily-generated "steps", where each step is either a `deposit` command or a `claim` command. The test then treats each of these steps as a separate transaction: it maintains a snapshot of the blockchain state, and for each step creates a fresh environment in which to execute the contract call, simulating the advancement of time between each step. As in the previous example, assertions are made after each step. The input to the fuzzer looks, in part, like: ```rust title="fuzzing/fuzz/fuzz_targets/fuzz_target_2.rs" #[derive(Arbitrary, Debug)] struct Input { addresses: [
::Prototype; NUM_ADDRESSES], #[arbitrary(with = |u: &mut Unstructured| u.int_in_range(0..=i128::MAX))] token_mint: i128, steps: RustVec, } #[derive(Arbitrary, Debug)] struct Step { #[arbitrary(with = |u: &mut Unstructured| u.int_in_range(1..=u64::MAX))] advance_time: u64, command: Command, // `Command` not shown here - see the full source. } ``` This shows how to use the `SorobanArbitrary::Prototype` associated type to define inputs to the fuzzer. A Soroban [`Address`] can only be created with an [`Env`], so cannot be generated directly by the `Arbitrary` trait. Instead we use the fully-qualified name of the `Address` prototype, `
::Prototype`, to ask for `Address`'s prototype instead. Then when our fuzzer needs the `Address` we instantiate it with the [`FromVal`] trait: ```rust title="fuzzing/fuzz/fuzz_targets/fuzz_target_2.rs" let depositor_address = Address::from_val(&env, &input.addresses[cmd.depositor_index]); ``` --- The contract we are fuzzing is a _timelock_ contract, where calculation of time is crucial for correctness. So our testing must account for the advancement of time. The contract defines a `TimeBound` type and accepts it in the `deposit` method: ```rust title="fuzzing/src/lib.rs" #[derive(Clone, Debug)] #[contracttype] pub struct TimeBound { pub kind: TimeBoundKind, pub timestamp: u64, } #[contractimpl] impl ClaimableBalanceContract { pub fn deposit( env: Env, from: Address, token: Address, amount: i128, claimants: Vec
, time_bound: TimeBound, ) { ... } } ``` In our fuzzer, one of the possible commands issued each step is a `DepositCommand`: ```rust title="fuzzing/fuzz/fuzz_targets/fuzz_target_2.rs" #[derive(Arbitrary, Debug)] struct DepositCommand { #[arbitrary(with = |u: &mut Unstructured| u.int_in_range(0..=NUM_ADDRESSES - 1))] depositor_index: usize, amount: i128, // This is an ugly way to get a vector of integers in range #[arbitrary(with = |u: &mut Unstructured| { u.arbitrary_len::().map(|len| { (0..len).map(|_| { u.int_in_range(0..=NUM_ADDRESSES - 1) }).collect::, _>>() }).and_then(|inner_result| inner_result) })] claimant_indexes: RustVec, time_bound: ::Prototype, } ``` Notice that this command again uses the `SorobanArbitrary::Prototype` associated type to accept a `TimeBound` as input. To advance time we maintain a [`LedgerSnapshot`], defined in the [`soroban-ledger-snapshot`] crate. For each step we call [`Env::from_snapshot`] to create a fresh environment to execute the step, then [`Env::to_snapshot`] to create a new snapshot to use in the following step. [`ledgersnapshot`]: https://docs.rs/soroban-ledger-snapshot/latest/soroban_ledger_snapshot/struct.LedgerSnapshot.html [`soroban-ledger-snapshot`]: https://docs.rs/soroban-ledger-snapshot [`env::from_snapshot`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Env.html#method.from_snapshot [`env::to_snapshot`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Env.html#method.to_snapshot Here is a simplified outline of how this works. See the full source code for details. ```rust title="fuzzing/fuzz/fuzz_targets/fuzz_target_2.rs" let snapshot = { let init_ledger = LedgerInfo { timestamp: 12345, protocol_version: 1, sequence_number: 10, network_id: Default::default(), base_reserve: 10, min_temp_entry_ttl: u32::MAX, min_persistent_entry_ttl: u32::MAX, max_entry_ttl: u32::MAX, }; LedgerSnapshot::from(init_ledger, None) }; let mut prev_env = Env::from_snapshot(init_snapshot); for step in &config.input.steps { // Advance time and create a new env from snapshot. let curr_env = { let mut snapshot = prev_env.to_snapshot(); snapshot.ledger.sequence_number += 1; snapshot.ledger.timestamp = snapshot.ledger.timestamp.saturating_add(step.advance_time); let env = Env::from_snapshot(snapshot); env.cost_estimate().budget().reset_unlimited(); env }; step.command.exec(&config, &curr_env); assert_invariants(&config, &prev_env, &curr_env); prev_env = curr_env; } ``` ## Converting a Fuzz Test to a Property Test In addition to fuzz testing, Soroban supports property testing in the style of quickcheck, by using the [`proptest`] and [`proptest-arbitrary-interop`] crates in conjunction with the `SorobanArbitrary` trait. Property tests are similar to fuzz tests in that they generate randomized input. Property tests though do not instrument their test cases or mutate their input based on feedback from previous tests. Thus they are a weaker form of test. The great benefit of property tests though is that they can be included in standard Rust test suites and require no extra tooling to execute. One might take advantage of this by interactively fuzzing to discover deep bugs, then convert fuzz tests to property tests to help prevent regressions. The [`proptest.rs`] file is a translation of `fuzz_target_1.rs` to a property test. [`proptest.rs`]: https://github.com/stellar/soroban-examples/tree/v23.0.0/fuzzing/src/proptest.rs --- ## Liquidity Pool Write a constant-product liquidity pool contract. The [liquidity pool example] demonstrates how to write a constant product liquidity pool contract. A liquidity pool is an automated way to add liquidity for a set of tokens that will facilitate asset conversion between them. Users can deposit some amount of each token into the pool, receiving a proportional number of "token shares." The user will then receive a portion of the accrued conversion fees when they ultimately "trade in" their token shares to receive their original tokens back. Soroban liquidity pools are exclusive to Soroban and cannot interact with built-in Stellar AMM liquidity pools. :::caution Implementing a liquidity pool contract should be done cautiously. User funds are involved, so great care should be taken to ensure safety and transparency. The example here should _not_ be considered a ready-to-go contract. Please use it as a reference only. The Stellar network already has liquidity pool functionality built right in to the core protocol. [Learn more here](../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx). ::: [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [liquidity pool example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/liquidity_pool [source code]: https://github.com/stellar/soroban-examples/tree/v23.0.0/liquidity_pool/src/lib.rs#L143 ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, navigate to the `liquidity_pool` directory, and use `cargo test`. ```sh cd liquidity_pool cargo test ``` You should see the output: ```text running 3 tests test test::deposit_amount_zero_should_panic - should panic ... ok test test::swap_reserve_one_nonzero_other_zero - should panic ... ok test test::test ... ok ``` [setup]: ../getting-started/setup.mdx ## Code ```rust title="liquidity_pool/src/lib.rs" #![no_std] mod test; use num_integer::Roots; use soroban_sdk::{contract, contractimpl, contractmeta, contracttype, token, Address, Env}; #[derive(Clone)] #[contracttype] pub enum DataKey { TokenA, TokenB, TotalShares, ReserveA, ReserveB, Shares(Address), } fn get_token_a(e: &Env) -> Address { e.storage().instance().get(&DataKey::TokenA).unwrap() } fn get_token_b(e: &Env) -> Address { e.storage().instance().get(&DataKey::TokenB).unwrap() } fn get_total_shares(e: &Env) -> i128 { e.storage().instance().get(&DataKey::TotalShares).unwrap() } fn get_reserve_a(e: &Env) -> i128 { e.storage().instance().get(&DataKey::ReserveA).unwrap() } fn get_reserve_b(e: &Env) -> i128 { e.storage().instance().get(&DataKey::ReserveB).unwrap() } fn get_balance(e: &Env, contract: Address) -> i128 { token::Client::new(e, &contract).balance(&e.current_contract_address()) } fn get_balance_a(e: &Env) -> i128 { get_balance(e, get_token_a(e)) } fn get_balance_b(e: &Env) -> i128 { get_balance(e, get_token_b(e)) } fn get_shares(e: &Env, user: &Address) -> i128 { e.storage() .persistent() .get(&DataKey::Shares(user.clone())) .unwrap_or(0) } fn put_shares(e: &Env, user: &Address, amount: i128) { e.storage() .persistent() .set(&DataKey::Shares(user.clone()), &amount); } fn put_token_a(e: &Env, contract: Address) { e.storage().instance().set(&DataKey::TokenA, &contract); } fn put_token_b(e: &Env, contract: Address) { e.storage().instance().set(&DataKey::TokenB, &contract); } fn put_total_shares(e: &Env, amount: i128) { e.storage().instance().set(&DataKey::TotalShares, &amount) } fn put_reserve_a(e: &Env, amount: i128) { e.storage().instance().set(&DataKey::ReserveA, &amount) } fn put_reserve_b(e: &Env, amount: i128) { e.storage().instance().set(&DataKey::ReserveB, &amount) } fn burn_shares(e: &Env, from: &Address, amount: i128) { let current_shares = get_shares(e, from); if current_shares < amount { panic!("insufficient shares"); } let total = get_total_shares(e); put_shares(e, from, current_shares - amount); put_total_shares(e, total - amount); } fn mint_shares(e: &Env, to: &Address, amount: i128) { let current_shares = get_shares(e, to); let total = get_total_shares(e); put_shares(e, to, current_shares + amount); put_total_shares(e, total + amount); } fn transfer(e: &Env, token: Address, to: Address, amount: i128) { token::Client::new(e, &token).transfer(&e.current_contract_address(), &to, &amount); } fn transfer_a(e: &Env, to: Address, amount: i128) { transfer(e, get_token_a(e), to, amount); } fn transfer_b(e: &Env, to: Address, amount: i128) { transfer(e, get_token_b(e), to, amount); } fn get_deposit_amounts( desired_a: i128, min_a: i128, desired_b: i128, min_b: i128, reserve_a: i128, reserve_b: i128, ) -> (i128, i128) { if reserve_a == 0 && reserve_b == 0 { return (desired_a, desired_b); } let amount_b = desired_a * reserve_b / reserve_a; if amount_b <= desired_b { if amount_b < min_b { panic!("amount_b less than min") } (desired_a, amount_b) } else { let amount_a = desired_b * reserve_a / reserve_b; if amount_a > desired_a || amount_a < min_a { panic!("amount_a invalid") } (amount_a, desired_b) } } // Metadata that is added on to the WASM custom section contractmeta!( key = "Description", val = "Constant product AMM with a .3% swap fee" ); #[contract] struct LiquidityPool; #[contractimpl] impl LiquidityPool { pub fn __constructor(e: Env, token_a: Address, token_b: Address) { if token_a >= token_b { panic!("token_a must be less than token_b"); } put_token_a(&e, token_a); put_token_b(&e, token_b); put_total_shares(&e, 0); put_reserve_a(&e, 0); put_reserve_b(&e, 0); } pub fn balance_shares(e: Env, user: Address) -> i128 { get_shares(&e, &user) } pub fn deposit( e: Env, to: Address, desired_a: i128, min_a: i128, desired_b: i128, min_b: i128, ) { // Depositor needs to authorize the deposit to.require_auth(); let (reserve_a, reserve_b) = (get_reserve_a(&e), get_reserve_b(&e)); // Calculate deposit amounts let (amount_a, amount_b) = get_deposit_amounts(desired_a, min_a, desired_b, min_b, reserve_a, reserve_b); if amount_a <= 0 || amount_b <= 0 { // If one of the amounts can be zero, we can get into a situation // where one of the reserves is 0, which leads to a divide by zero. panic!("both amounts must be strictly positive"); } let token_a_client = token::Client::new(&e, &get_token_a(&e)); let token_b_client = token::Client::new(&e, &get_token_b(&e)); token_a_client.transfer(&to, &e.current_contract_address(), &amount_a); token_b_client.transfer(&to, &e.current_contract_address(), &amount_b); // Now calculate how many new pool shares to mint let (balance_a, balance_b) = (get_balance_a(&e), get_balance_b(&e)); let total_shares = get_total_shares(&e); let zero = 0; let new_total_shares = if reserve_a > zero && reserve_b > zero { let shares_a = (balance_a * total_shares) / reserve_a; let shares_b = (balance_b * total_shares) / reserve_b; shares_a.min(shares_b) } else { (balance_a * balance_b).sqrt() }; mint_shares(&e, &to, new_total_shares - total_shares); put_reserve_a(&e, balance_a); put_reserve_b(&e, balance_b); } // If "buy_a" is true, the swap will buy token_a and sell token_b. This is flipped if "buy_a" is false. // "out" is the amount being bought, with in_max being a safety to make sure you receive at least that amount. // swap will transfer the selling token "to" to this contract, and then the contract will transfer the buying token to "to". pub fn swap(e: Env, to: Address, buy_a: bool, out: i128, in_max: i128) { to.require_auth(); let (reserve_a, reserve_b) = (get_reserve_a(&e), get_reserve_b(&e)); let (reserve_sell, reserve_buy) = if buy_a { (reserve_b, reserve_a) } else { (reserve_a, reserve_b) }; if reserve_buy < out { panic!("not enough token to buy"); } // First calculate how much needs to be sold to buy amount out from the pool let n = reserve_sell * out * 1000; let d = (reserve_buy - out) * 997; let sell_amount = (n / d) + 1; if sell_amount > in_max { panic!("in amount is over max") } // Transfer the amount being sold to the contract let sell_token = if buy_a { get_token_b(&e) } else { get_token_a(&e) }; let sell_token_client = token::Client::new(&e, &sell_token); sell_token_client.transfer(&to, &e.current_contract_address(), &sell_amount); let (balance_a, balance_b) = (get_balance_a(&e), get_balance_b(&e)); // residue_numerator and residue_denominator are the amount that the invariant considers after // deducting the fee, scaled up by 1000 to avoid fractions let residue_numerator = 997; let residue_denominator = 1000; let zero = 0; let new_invariant_factor = |balance: i128, reserve: i128, out: i128| { let delta = balance - reserve - out; let adj_delta = if delta > zero { residue_numerator * delta } else { residue_denominator * delta }; residue_denominator * reserve + adj_delta }; let (out_a, out_b) = if buy_a { (out, 0) } else { (0, out) }; let new_inv_a = new_invariant_factor(balance_a, reserve_a, out_a); let new_inv_b = new_invariant_factor(balance_b, reserve_b, out_b); let old_inv_a = residue_denominator * reserve_a; let old_inv_b = residue_denominator * reserve_b; if new_inv_a * new_inv_b < old_inv_a * old_inv_b { panic!("constant product invariant does not hold"); } if buy_a { transfer_a(&e, to, out_a); } else { transfer_b(&e, to, out_b); } let new_reserve_a = balance_a - out_a; let new_reserve_b = balance_b - out_b; if new_reserve_a <= 0 || new_reserve_b <= 0 { panic!("new reserves must be strictly positive"); } put_reserve_a(&e, new_reserve_a); put_reserve_b(&e, new_reserve_b); } // transfers share_amount of pool share tokens to this contract, burns all pools share tokens in this contracts, and sends the // corresponding amount of token_a and token_b to "to". // Returns amount of both tokens withdrawn pub fn withdraw( e: Env, to: Address, share_amount: i128, min_a: i128, min_b: i128, ) -> (i128, i128) { to.require_auth(); let current_shares = get_shares(&e, &to); if current_shares < share_amount { panic!("insufficient shares"); } let (balance_a, balance_b) = (get_balance_a(&e), get_balance_b(&e)); let total_shares = get_total_shares(&e); // Calculate withdrawal amounts let out_a = (balance_a * share_amount) / total_shares; let out_b = (balance_b * share_amount) / total_shares; if out_a < min_a || out_b < min_b { panic!("min not satisfied"); } burn_shares(&e, &to, share_amount); transfer_a(&e, to.clone(), out_a); transfer_b(&e, to, out_b); put_reserve_a(&e, balance_a - out_a); put_reserve_b(&e, balance_b - out_b); (out_a, out_b) } pub fn get_rsrvs(e: Env) -> (i128, i128) { (get_reserve_a(&e), get_reserve_b(&e)) } } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/liquidity_pool ## How it Works Every asset created on Stellar starts with zero liquidity. The same is true of tokens created on Soroban (unless a Stellar asset with existing liquidity token has its [Stellar Asset Contract (SAC)](../../../tokens/stellar-asset-contract.mdx) deployed for use in Soroban). In simple terms, "liquidity" means how much of an asset in a market is available to be bough or sold. In the "old days," you could generate liquidity in a market by creating buy/sell orders on an order book. Liquidity pools automate this process by substituting the orders with math. Depositors into the liquidity pool earn fees from `swap` transactions. No orders required! Open the [`liquidity_pool/src/lib.rs`](https://github.com/stellar/soroban-examples/blob/v23.0.0/liquidity_pool/src/lib.rs) file or see the code above to follow along. ### Initialize the Contract When this contract is deployed, the `__constructor` function will automatically and atomically be invoked, so the following arguments must be passed in: - **`token_a`:** The contract `Address` for an **already deployed** (or wrapped) token that will be held in reserve by the liquidity pool. - **`token_b`:** The contract `Address` for an **already deployed** (or wrapped) token that will be held in reserve by the liquidity pool. Bear in mind that which token is `token_a` and which is `token_b` is **not** an arbitrary distinction. In line with the Built-in Stellar liquidity pools, this contract can only make a single liquidity pool for a given set of tokens. So, the token addresses must be provided in [lexicographical order] at the time of initialization. ```rust title="liquidity_pool/src/lib.rs" pub fn __constructor(e: Env, token_a: Address, token_b: Address) { if token_a >= token_b { panic!("token_a must be less than token_b"); } put_token_a(&e, token_a); put_token_b(&e, token_b); put_total_shares(&e, 0); put_reserve_a(&e, 0); put_reserve_b(&e, 0); } ``` [creating its own `pool` token]: #minting-and-burning-lp-shares [interacting with contracts for `token_a` and `token_b`]: #token-transfers-tofrom-the-lp-contract [lexicographical order]: ../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#liquidity-pool-participation ### A "Constant Product" Liquidity Pool The _type_ of liquidity pool this example contract implements is called a "constant product" liquidity pool. While this isn't the only type of liquidity pool out there, it is the most common variety. These liquidity pools are designed to keep the _total_ value of each asset in _relative_ equilibrium. The "product" in the constant product (also called an "invariant") will change every time the liquidity pool is interacted with (deposit, withdraw, or token swaps). However, the invariant **must** only increase with every interaction. During a swap, what must be kept in mind is that for every withdrawal from the `token_a` side, you must "refill" the `token_b` side with a sufficient amount to keep the liquidity pool's price balanced. The math is predictable, but it is not linear. The more you take from one side, the more you must give on the opposite site _exponentially_. Inside the `swap` function, the math is done like this (this is a simplified version, however): ```rust title="liquidity_pool/src/lib.rs" pub fn swap(e: Env, to: Address, buy_a: bool, out: i128, in_max: i128) { // Get the current balances of both tokens in the liquidity pool let (reserve_sell, reserve_buy) = (get_reserve_a(&e), get_reserve_b(&e)); // Calculate how much needs to be let n = reserve_sell * out * 1000; let d = (reserve_buy - out) * 997; let sell_amount = (n / d) + 1; } ``` We have much more in-depth information about how this kind of liquidity pool works is available in [Stellar Quest: Series 3, Quest 5]. This is a really useful, interactive way to learn more about how the built-in Stellar liquidity pools work. Much of the knowledge you might gain from there will easily translate to this example contract. [stellar quest: series 3, quest 5]: https://quest.stellar.org/learn/series/3/quest/5 ### Interacting with Token Contracts in Another Contract This liquidity pool contract will operate with a total of three different Soroban tokens: - **Pool Shares:** This example uses a very simple share token given to asset depositors in exchange for their deposit. These tokens are "traded in" by the user when they withdraw some amount of their original deposit (plus any earned swap fees). In this simplified system, shares are just added/subtracted whenever a user deposits or withdraws the underlying assets. No distinct _token contract_ will be used for these shares. - **`token_a`** and **`token_b`**: Will be the two "reserve tokens" that users will deposit into the pool. These could be "wrapped" tokens from pre-existing Stellar assets, or they could be Soroban-native tokens. This contract doesn't really care, as long as the functions it needs from the common [Token Interface] are available in the token contract. [token interface]: ../../../tokens/token-interface.mdx #### Minting and Burning LP Shares We are minting and burning LP shares within the logic of the main contract, instead of utilizing a distinct token contract. There are some "helper" functions created to facilitate this functionality. These functions are used when a user takes any kind of deposit or withdraw action. ```rust title="liquidity_pool/src/lib.rs" fn burn_shares(e: &Env, from: &Address, amount: i128) { let current_shares = get_shares(e, from); if current_shares < amount { panic!("insufficient shares"); } let total = get_total_shares(e); put_shares(e, from, current_shares - amount); put_total_shares(e, total - amount); } fn mint_shares(e: &Env, to: &Address, amount: i128) { let current_shares = get_shares(e, to); let total = get_total_shares(e); put_shares(e, to, current_shares + amount); put_total_shares(e, total + amount); } ``` How is that number of shares calculated, you ask? Excellent question! If it's the very first deposit (see above), it's just the square root of the product of the quantities of `token_a` and `token_b` deposited. Very simple. However, if there have already been deposits into the liquidity pool, and the user is just adding more tokens into the pool, there's a bit more math. However, the main point is that each depositor receives the same ratio of `POOL` tokens for their deposit as every other depositor. ```rust title=liquidity_pool/src/lib.rs fn deposit(e: Env, to: Address, desired_a: i128, min_a: i128, desired_b: i128, min_b: i128) { let zero = 0; let new_total_shares = if reserve_a > zero && reserve_b > zero { // Note balance_a and balance_b at this point in the function include // the tokens the user is currently depositing, whereas reserve_a and // reserve_b do not yet. let shares_a = (balance_a * total_shares) / reserve_a; let shares_b = (balance_b * total_shares) / reserve_b; shares_a.min(shares_b) } else { (balance_a * balance_b).sqrt() }; } ``` #### Token Transfers to/from the LP Contract As we've already discussed, the liquidity pool contract will make use of the [Token Interface] available in the token contracts that were supplied as `token_a` and `token_b` arguments at the time of initialization. Throughout the rest of the contract, the liquidity pool will make use of that interface to make transfers of those tokens to/from itself. What's happening is that as a user deposits tokens into the pool, and the contract invokes the `transfer` function to move the tokens from the `to` address (the depositor) to be held by the contract address. `POOL` tokens are then minted to depositor (see previous section). Pretty simple, right!? ```rust title="liquidity_pool/src/lib.rs" fn deposit(e: Env, to: Address, desired_a: i128, min_a: i128, desired_b: i128, min_b: i128) { // Depositor needs to authorize the deposit to.require_auth(); let token_a_client = token::Client::new(&e, &get_token_a(&e)); let token_b_client = token::Client::new(&e, &get_token_b(&e)); token_a_client.transfer(&to, &e.current_contract_address(), &amount_a); token_b_client.transfer(&to, &e.current_contract_address(), &amount_b); mint_shares(&e, to, new_total_shares - total_shares); } ``` In contrast, when a user withdraws their deposited tokens, it's a bit more involved, and the following procedure happens. 1. The number of shares being "redeemed" by the user are checked against the _actual_ amount of shares the user holds. 2. The withdraw amounts for the reserve tokens are calculated based on the amount of share tokens being redeemed. 3. The share tokens are burned now the withdraw amounts have been calculated, and they are no longer needed. 4. The respective amounts of `token_a` and `token_b` are transferred _from_ the contract address into the `to` address (the depositor). ```rust title="liquidity_pool/src/lib.rs" fn withdraw(e: Env, to: Address, share_amount: i128, min_a: i128, min_b: i128) -> (i128, i128) { to.require_auth(); // First calculate the specified pool shares are available to the user let current_shares = get_shares(&e, &to); if current_shares < share_amount { panic!("insufficient shares"); } // ... balances of pool shares and underlying assets are retrieved // Now calculate the withdraw amounts let out_a = (balance_a * balance_shares) / total_shares; let out_b = (balance_b * balance_shares) / total_shares; burn_shares(&e, balance_shares); transfer_a(&e, to.clone(), out_a); transfer_b(&e, to, out_b); } ``` You'll notice that by holding the balance of `token_a` and `token_b` on the liquidity pool contract itself it makes, it very easy for us to perform any of the [Token Interface] actions inside the contract. As a bonus, any outside observer could query the balances of `token_a` or `token_b` held by the contract to verify the reserves are actually in line with the values the contract reports when its own `get_rsvs` function is invoked. ## Tests Open the [`liquidity_pool/src/test.rs`] file to follow along. ```rust title="liquidity_pool/src/test.rs" #![cfg(test)] extern crate std; use crate::LiquidityPoolClient; use soroban_sdk::{ symbol_short, testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, token, Address, Env, IntoVal, }; fn create_token_contract<'a>( e: &Env, admin: &Address, ) -> (token::Client<'a>, token::StellarAssetClient<'a>) { let sac = e.register_stellar_asset_contract_v2(admin.clone()); ( token::Client::new(e, &sac.address()), token::StellarAssetClient::new(e, &sac.address()), ) } fn create_liqpool_contract<'a>( e: &Env, token_a: &Address, token_b: &Address, ) -> LiquidityPoolClient<'a> { LiquidityPoolClient::new(e, &e.register(crate::LiquidityPool {}, (token_a, token_b))) } #[test] fn test() { let e = Env::default(); e.mock_all_auths(); let admin1 = Address::generate(&e); let admin2 = Address::generate(&e); let (token1, token1_admin) = create_token_contract(&e, &admin1); let (token2, token2_admin) = create_token_contract(&e, &admin2); let user1 = Address::generate(&e); let liqpool = create_liqpool_contract(&e, &token1.address, &token2.address); token1_admin.mint(&user1, &1000); assert_eq!(token1.balance(&user1), 1000); token2_admin.mint(&user1, &1000); assert_eq!(token2.balance(&user1), 1000); liqpool.deposit(&user1, &100, &100, &100, &100); assert_eq!( e.auths(), std::vec![( user1.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( liqpool.address.clone(), symbol_short!("deposit"), (&user1, 100_i128, 100_i128, 100_i128, 100_i128).into_val(&e) )), sub_invocations: std::vec![ AuthorizedInvocation { function: AuthorizedFunction::Contract(( token1.address.clone(), symbol_short!("transfer"), (&user1, &liqpool.address, 100_i128).into_val(&e) )), sub_invocations: std::vec![] }, AuthorizedInvocation { function: AuthorizedFunction::Contract(( token2.address.clone(), symbol_short!("transfer"), (&user1, &liqpool.address, 100_i128).into_val(&e) )), sub_invocations: std::vec![] } ] } )] ); assert_eq!(liqpool.balance_shares(&user1), 100); assert_eq!(token1.balance(&user1), 900); assert_eq!(token1.balance(&liqpool.address), 100); assert_eq!(token2.balance(&user1), 900); assert_eq!(token2.balance(&liqpool.address), 100); liqpool.swap(&user1, &false, &49, &100); assert_eq!( e.auths(), std::vec![( user1.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( liqpool.address.clone(), symbol_short!("swap"), (&user1, false, 49_i128, 100_i128).into_val(&e) )), sub_invocations: std::vec![AuthorizedInvocation { function: AuthorizedFunction::Contract(( token1.address.clone(), symbol_short!("transfer"), (&user1, &liqpool.address, 97_i128).into_val(&e) )), sub_invocations: std::vec![] }] } )] ); assert_eq!(token1.balance(&user1), 803); assert_eq!(token1.balance(&liqpool.address), 197); assert_eq!(token2.balance(&user1), 949); assert_eq!(token2.balance(&liqpool.address), 51); e.cost_estimate().budget().reset_unlimited(); liqpool.withdraw(&user1, &100, &197, &51); assert_eq!( e.auths(), std::vec![( user1.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( liqpool.address.clone(), symbol_short!("withdraw"), (&user1, 100_i128, 197_i128, 51_i128).into_val(&e) )), sub_invocations: std::vec![] } )] ); assert_eq!(token1.balance(&user1), 1000); assert_eq!(token2.balance(&user1), 1000); assert_eq!(liqpool.balance_shares(&user1), 0); assert_eq!(token1.balance(&liqpool.address), 0); assert_eq!(token2.balance(&liqpool.address), 0); } #[test] #[should_panic] fn deposit_amount_zero_should_panic() { let e = Env::default(); e.mock_all_auths(); // Create contracts let admin1 = Address::generate(&e); let admin2 = Address::generate(&e); let (token1, token1_admin) = create_token_contract(&e, &admin1); let (token2, token2_admin) = create_token_contract(&e, &admin2); let liqpool = create_liqpool_contract(&e, &token1.address, &token2.address); // Create a user let user1 = Address::generate(&e); token1_admin.mint(&user1, &1000); assert_eq!(token1.balance(&user1), 1000); token2_admin.mint(&user1, &1000); assert_eq!(token2.balance(&user1), 1000); liqpool.deposit(&user1, &1, &0, &0, &0); } #[test] #[should_panic] fn swap_reserve_one_nonzero_other_zero() { let e = Env::default(); e.mock_all_auths(); // Create contracts let admin1 = Address::generate(&e); let admin2 = Address::generate(&e); let (token1, token1_admin) = create_token_contract(&e, &admin1); let (token2, token2_admin) = create_token_contract(&e, &admin2); let liqpool = create_liqpool_contract(&e, &token1.address, &token2.address); // Create a user let user1 = Address::generate(&e); token1_admin.mint(&user1, &1000); assert_eq!(token1.balance(&user1), 1000); token2_admin.mint(&user1, &1000); assert_eq!(token2.balance(&user1), 1000); // Try to get to a situation where the reserves are 1 and 0. // It shouldn't be possible. token2.transfer(&user1, &liqpool.address, &1); liqpool.swap(&user1, &false, &1, &1); } ``` [`liquidity_pool/src/test.rs`]: https://github.com/stellar/soroban-examples/tree/v23.0.0/liquidity_pool/src/test.rs In any test, the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. ```rust title="liquidity_pool/src/test.rs" let e = Env::default(); ``` We mock authentication checks in the tests, which allows the tests to proceed as if all users/addresses/contracts/etc. had successfully authenticated. ```rust title="liquidity_pool/src/test.rs" e.mock_all_auths(); ``` We have abstracted into a couple functions the tasks of creating token contracts and deploying a liquidity pool contract. Each are then used within the test. ```rust title="liquidity_pool/src/test.rs" fn create_token_contract<'a>( e: &Env, admin: &Address, ) -> (token::Client<'a>, token::StellarAssetClient<'a>) { let sac = e.register_stellar_asset_contract_v2(admin.clone()); ( token::Client::new(e, &sac.address()), token::StellarAssetClient::new(e, &sac.address()), ) } fn create_liqpool_contract<'a>( e: &Env, token_a: &Address, token_b: &Address, ) -> LiquidityPoolClient<'a> { LiquidityPoolClient::new(e, &e.register(crate::LiquidityPool {}, (token_a, token_b))) } ``` All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `LiquidityPool`, and the client is named `LiquidityPoolClient`. These tests examine the "typical" use-case of a liquidity pool, ensuring that the balances, returns, etc. are appropriate at various points during the test. 1. First, the test sets everything up with an `Env`, two admin addresses, two reserve tokens, a randomly generated address to act as the user of the liquidity pool, the liquidity pool itself, a pool token shares contract, and mints the reserve assets to the user address. 2. The user then deposits some of each asset into the liquidity pool. At this time, the following checks are done: - appropriate authorizations for deposits and transfers exist, - balances are checked for each token (`token_a`, `token_b`, and `POOL`) from both the user's perspective and the `liqpool` contract's perspective 3. The user performs a swap, buying `token_b` in exchange for `token_a`. The same checks as the previous step are made now, excepting the balances of `POOL`, since a swap has no effect on `POOL` tokens. 4. The user then withdraws all of the deposits it made, trading all of its `POOL` tokens in the process. The same checks are made here as were made in the `deposit` step. ## Build the Contract To build the contract, use the `stellar contract build` command. ```sh stellar contract build ``` A `.wasm` file should be outputted in the `target` directory: ``` target/wasm32v1-none/release/soroban_liquidity_pool_contract.wasm ``` ## Run the Contract If you have [`stellar-cli`] installed, you can deploy the contract and invoke its functions. ### Deploy ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_liquidity_pool_contract.wasm \ --alias liquidity_pool_example \ --source-account alice \ --network testnet ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/soroban_liquidity_pool_contract.wasm ` --alias liquidity_pool_example ` --source-account alice ` --network testnet ``` ### Invoke ```sh stellar contract invoke \ --id liquidity_pool_example \ --source-account alice \ --network testnet \ -- \ deposit \ --to GBZV3NONYSUDVTEHATQO4BCJVFXJO3XQU5K32X3XREVZKSMMOZFO4ZXR \ --desired_a 100 \ --min_a 98 \ --desired_be 200 \ --min_b 196 ``` ```powershell stellar contract invoke ` --id liquidity_pool_example ` --source-account alice ` --network testnet ` -- ` deposit ` --to GBZV3NONYSUDVTEHATQO4BCJVFXJO3XQU5K32X3XREVZKSMMOZFO4ZXR ` --desired_a 100 ` --min_a 98 ` --desired_be 200 ` --min_b 196 ``` [`stellar-cli`]: ../../../tools/cli/stellar-cli.mdx --- ## Logging Debug a smart contract with logs. The [logging example] demonstrates how to log for the purpose of debugging. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] Logs in contracts are only visible in tests, or when executing contracts using [`stellar-cli`]. Logs are only compiled into the contract if the `debug-assertions` Rust compiler option is enabled. [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [logging example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/hello_world :::tip Logs are not a substitute for step-through debugging. Rust tests for Soroban can be step-through debugged in your Rust-enabled IDE. See [testing] for more details. ::: :::caution Logs are not accessible by dapps and other applications. See the [events example] for how to produce structured events. ::: [testing]: ../getting-started/hello-world.mdx#run-the-tests [events example]: ./events.mdx ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, navigate to the `logging` directory, and use `cargo test`. ```sh cd logging cargo test -- --nocapture ``` You should see the output: ``` running 1 test [Diagnostic Event] contract:CAAA..., topics:[log], data:["Hello {}", Dev] [Diagnostic Event] contract:CAAA..., topics:[log], data:["Hello {}", Dev] Writing test snapshot file for test "test::test" to "test_snapshots/test/test.1.json". test test::test ... ok ``` ## Code ```toml title="logging/Cargo.toml" [profile.release-with-logs] inherits = "release" debug-assertions = true ``` ```rust title="logging/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl, log, Env, Symbol}; #[contract] pub struct Contract; #[contractimpl] impl Contract { pub fn hello(env: Env, value: Symbol) { log!(&env, "Hello {}", value); } } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/logging ## How it Works The [`log!`] macro logs a string. Any logs that occur during execution are outputted to stdout in [`stellar-cli`] and available for tests to assert on or print. Logs are only outputted if the contract is built with the `debug-assertions` compiler option enabled. This makes them efficient to leave in code permanently since a regular `release` build will omit them. Logs are only recorded in Soroban environments that have logging enabled. The only Soroban environments where logging is enabled is in Rust tests, and in the [`stellar-cli`]. Open the files above to follow along. ### `Cargo.toml` Profile Logs are only outputted if the contract is built with the `debug-assertions` compiler option enabled. The `test` profile that is activated when running `cargo test` has `debug-assertions` enabled, so when running tests logs are enabled by default. A new `release-with-logs` profile is added to `Cargo.toml` that inherits from the `release` profile, and enables `debug-assertions`. It can be used to build a `.wasm` file that has logs enabled. ```toml [profile.release-with-logs] inherits = "release" debug-assertions = true ``` To build without logs use the `--release` or `--profile release` option. To build with logs use the `--profile release-with-logs` option. ### Using the `log!` Macro The [`log!`] macro builds a string from the format string, and a list of arguments. Arguments are substituted wherever the `{}` value appears in the format string. ```rust log!(&env, "Hello {}", value); ``` The above log will render as follows if `value` is a `Symbol` containing `"Dev"`. ``` Hello Symbol(Dev) ``` :::caution The values outputted are currently relatively limited. While primitive values like `u32`, `u64`, `bool`, and `Symbol`s will render clearly in the log output, `Bytes`, `Vec`, `Map`, and custom types will render only their handle number. Logging capabilities are in early development. ::: ## Tests Open the [`logging/src/test.rs`](https://github.com/stellar/soroban-examples/tree/v23.0.0/logging/src/test.rs) file to follow along. ```rust title="logging/src/test.rs" extern crate std; #[test] fn test() { let env = Env::default(); let addr = Address::from_str( &env, "CAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQMCJ", ); let contract_id = env.register_at(&addr, Contract, ()); let client = ContractClient::new(&env, &contract_id); client.hello(&symbol_short!("Dev")); let logs = env.logs().all(); assert_eq!(logs, std::vec!["[Diagnostic Event] contract:CAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQMCJ, topics:[log], data:[\"Hello {}\", Dev]"]); std::println!("{}", logs.join("\n")); } ``` The `std` crate, which contains the Rust standard library, is imported so that the test can use the `std::vec!` and `std::println!` macros. Since contracts are required to use `#![no_std]`, tests in contracts must manually import `std` to use `std` functionality like printing to stdout. ```rust extern crate std; ``` In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. ```rust let env = Env::default(); ``` The contract is registered with the environment using the contract type. We're specifying precisely which contract address the contract should be deployed to in the test, using the `env.register_at` function. This makes it easier to ensure the logging output is coming from the relevant contract address. ```rust let addr = Address::from_str( &env, "CAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQMCJ", ); let contract_id = env.register_at(&addr, Contract, ()); ``` All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `HelloContract`, and the client is named `HelloContractClient`. ```rust let client = ContractClient::new(&env, &contract_id); client.hello(&symbol_short!("Dev")); ``` Logs are available in tests via the environment. ```rust let logs = env.logs().all(); ``` They can be asserted on like any other value. ```rust assert_eq!(logs, std::vec!["[Diagnostic Event] contract:CAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQMCJ, topics:[log], data:[\"Hello {}\", Dev]"]); ``` They can also be printed to stdout. ```rust std::println!("{}", logs.join("\n")); ``` ## Build the Contract To build the contract, use the `stellar contract build` command. ### Without Logs To build the contract without logs, use the `--release` option. ```sh stellar contract build ``` A `.wasm` file should be outputted in the `target` directory, in the `release` subdirectory: ``` target/wasm32v1-none/release/soroban_logging_contract.wasm ``` ### With Logs To build the contract with logs, use the `--profile release-with-logs` option. ```sh stellar contract build --profile release-with-logs ``` A `.wasm` file should be outputted in the `target` directory, in the `release-with-logs` subdirectory: ``` target/wasm32v1-none/release-with-logs/soroban_logging_contract.wasm ``` ## Run the Contract If you have [`stellar-cli`] installed, you can deploy the contract and invoke its functions. Specify the `-v` option to enable verbose logs. ### Deploy ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release-with-logs/soroban_logging_contract.wasm \ --alias logging_example \ --source-account alice \ --network testnet ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release-with-logs/soroban_logging_contract.wasm ` --alias logging_example ` --source-account alice ` --network testnet ``` ### Invoke ```sh stellar -v contract invoke \ --id logging_example \ --source-account alice \ --network testnet \ -- \ hello \ --value friend ``` ```powershell stellar -v contract invoke ` --id logging_example ` --source-account alice ` --network testnet ` -- ` hello ` --value friend ``` The output should include the following line. ``` 📔 CAAA... - Success - Log: {"vec":[{"string":"Hello {}"},{"symbol":"friend"}]} ``` [`log!`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/macro.log.html [`stellar-cli`]: ../getting-started/setup.mdx#install-the-stellar-cli --- ## Mint Lock {`Implement contract that can delegate minting with limits.`} The [mint-lock example] demonstrates how to write a contract that can delegate minting tokens from another address with limits on how much those addresses can mint across a specified time period. The admin of the token contracts used must be the mint-lock contract. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [mint-lock example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/mint-lock --- ## Simple Account Simple Account This example implements the smallest possible [contract account](../../../learn/glossary.mdx#contract-account): each `require_auth` call delegates to one ed25519 public key. It shows how to store that key, run `__check_auth`, and surface authorization failures. Use this as the baseline before moving to the [Complex Account example](./complex-account.mdx) for multisig or policy enforcement. :::danger Implementing a contract account requires a very good understanding of authentication and authorization and requires rigorous testing and review. The example here is _not_ a full-fledged account contract - use it as an API reference only. ::: :::caution While contract accounts are supported by the Stellar protocol and Soroban SDK, the full client support (such as transaction simulation) is still under development. ::: [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples ## Run the Example 1. Finish the [Setup] checklist to install the Stellar CLI, Rust target, and required environment variables. 2. Clone the `soroban-examples` repository at the `main` branch: ```sh git clone -b main https://github.com/stellar/soroban-examples ``` 3. If you prefer not to install anything locally, launch the repo in [GitHub Codespaces][open-in-github-codespaces] or [Codeanywhere][open-in-code-anywhere]. Run the tests from the `simple_account` directory: ```sh cd simple_account cargo test ``` Expected output: ``` running 1 test test test::test_account ... ok ``` [setup]: ../getting-started/setup.mdx ## How it Works Open `simple_account/src/lib.rs`. The contract keeps one piece of state: the owner's ed25519 public key. ### Initialize the owner ```rust title="simple_account/src/lib.rs" #[contracttype] #[derive(Clone)] pub enum DataKey { Owner, } #[contractimpl] impl SimpleAccount { pub fn __constructor(env: Env, public_key: BytesN<32>) { if env.storage().instance().has(&DataKey::Owner) { panic!("owner is already set"); } env.storage().instance().set(&DataKey::Owner, &public_key); } ``` `__constructor` runs once, at deployment, to persist the owner's public key. ### Implement `__check_auth` ```rust title="simple_account/src/lib.rs" #[allow(non_snake_case)] pub fn __check_auth( env: Env, signature_payload: BytesN<32>, signature: BytesN<64>, _auth_context: Vec, ) { let public_key: BytesN<32> = env .storage() .instance() .get(&DataKey::Owner) .unwrap(); env.crypto() .ed25519_verify(&public_key, &signature_payload.into(), &signature); } } ``` `__check_auth` runs whenever another contract invokes `require_auth` on this contract address. The implementation loads the stored key, verifies the signature, and panics on failure so the upstream `require_auth` call rejects. Once you need multiple keys or policy logic, follow the same pattern shown in Complex Account. ## Tests Open `simple_account/src/test.rs`. `__check_auth` is not exposed as a regular entry point, so tests call `env.try_invoke_contract_check_auth` to emulate the Soroban host and exercise the same path Soroban runs during `require_auth`. ```rust title="simple_account/src/test.rs" #[test] fn test_account() { let env = Env::default(); let signer = Keypair::generate(&mut thread_rng()); let public_key: BytesN<32> = signer.public.to_bytes().into_val(&env); let contract_id = env.register(SimpleAccount, SimpleAccountArgs::__constructor(&public_key)); let account_contract = SimpleAccountClient::new(&env, &contract_id); let payload = BytesN::random(&env); env.try_invoke_contract_check_auth::( &account_contract.address, &payload, sign(&env, &signer, &payload), &vec![&env], ) .unwrap(); assert!(env .try_invoke_contract_check_auth::( &account_contract.address, &payload, BytesN::<64>::random(&env).into(), &vec![&env], ) .is_err()); } ``` `try_invoke_contract_check_auth` mimics the host path for `require_auth`, so the test proves both the success case and a failure case with random bytes. Follow the same structure for any account: - create a keypair and store the expected signer (for example, via `__constructor`) - call `try_invoke_contract_check_auth` with a valid signature and assert it succeeds - call it again with an invalid signature or payload and assert it fails ## Build the Contract To produce the Wasm executable, run: ```sh stellar contract build # add --package soroban-simple-account-contract when building inside the soroban-examples workspace ``` The compiled file appears at `target/wasm32v1-none/release/simple_account.wasm` (the exact filename depends on your crate name). ## Further Reading - [Complex account example] – adds multisig support and spend limits. - [BLS signature contract](./bls-signature.mdx) – demonstrates custom signature schemes inside `__check_auth`. [Complex account example]: ./complex-account.mdx [simple account example]: https://github.com/stellar/soroban-examples/tree/main/simple_account --- ## Single Offer Sale {`Make a standing offer to sell a token in exchange for another token.`} The [single offer sale example] demonstrates how to write a contract that allows a seller to set up an offer to sell token A for token B to multiple buyers. The comments in the [source code] explain how the contract should be used. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [single offer sale example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/single_offer [source code]: https://github.com/stellar/soroban-examples/tree/v23.0.0/single_offer/src/lib.rs --- ## Storage Increment a counter and store the incremented value. The [increment example] demonstrates how to call a contract from another contract. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [increment example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/increment ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [setup]: ../getting-started/setup.mdx ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, navigate to the `increment` directory, and use `cargo test`. ```sh cd increment cargo test ``` You should see the output: ``` running 1 test test test::test ... ok ``` ## Code ```rust title="increment/src/lib.rs" const COUNTER: Symbol = symbol_short!("COUNTER"); #[contract] pub struct IncrementContract; #[contractimpl] impl IncrementContract { pub fn increment(env: Env) -> u32 { let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); log!(&env, "count: {}", count); count += 1; env.storage().instance().set(&COUNTER, &count); env.storage().instance().extend_ttl(50, 100); count } } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/increment ## How it Works This contract will get a counter value from storage (or use the value 0 if no value has been stored yet), and increment this counter every time it's called. Open the [`increment/src/lib.rs`](https://github.com/stellar/soroban-examples/tree/v23.0.0/increment/src/lib.rs) file or see the code above to follow along. ### Contract Data Key Contract data is associated with a key, which can be used at a later time to look up the value. ```rust const COUNTER: Symbol = symbol_short!("COUNTER"); ``` `Symbol` is a short (up to 32 characters long) string type with limited character space (only `a-zA-Z0-9_` characters are allowed). Identifiers like contract function names and contract data keys are represented by `Symbols`. The `symbol_short!()` macro is a convenient way to pre-compute short symbols up to 9 characters in length at compile time using `Symbol::short`. It generates a compile-time constant that adheres to the valid character set of letters (a-zA-Z), numbers (0-9), and underscores (\_). If a symbol exceeds the 9-character limit, `Symbol::new` should be utilized for creating symbols at runtime. ### Contract Data Access ```rust let mut count: u32 = env .storage() .instance() .get(&COUNTER) .unwrap_or(0); // If no value set, assume 0. ``` The `env.storage()` function is used to access and update contract data. The executing contract is the only contract that can query or modify contract data that it has stored. The data stored is viewable on ledger anywhere the ledger is viewable, but contracts executing within the Soroban environment are restricted to their own data. The `get(`) function gets the current value associated with the counter key. If no value is currently stored, the value given to `unwrap_or(...)` is returned instead. Values stored as contract data and retrieved are transmitted from [the environment] and expanded into the type specified. In this case a `u32`. If the value can be expanded, the type returned will be a `u32`. Otherwise, if a developer cast it to be some other type, a panic would occur at the unwrap. [the environment]: ../../../learn/fundamentals/contract-development/environment-concepts.mdx ```rust env.storage() .instance() .set(&COUNTER, &count); ``` The `set()` function stores the new count value against the key, replacing the existing value. ### Managing Contract Data TTLs with `extend_ttl()` ```rust env.storage().instance().extend_ttl(50, 100); ``` All contract data has a Time To Live (TTL), measured in ledgers, that must be periodically extended. If an entry's TTL is not periodically extended, the entry will eventually become "archived." You can learn more about this in the [State Archival](../../../learn/fundamentals/contract-development/storage/state-archival.mdx) document. For now, it's worth knowing that there are three kinds of storage: `Persistent`, `Temporary`, and `Instance`. This contract only uses `Instance` storage: `env.storage().instance()`. Every time the counter is incremented, this storage's TTL gets extended by 100 [ledgers](../../../learn/fundamentals/stellar-data-structures/ledgers.mdx), or about 500 seconds. ## Tests Open the [`increment/src/test.rs`](https://github.com/stellar/soroban-examples/tree/v23.0.0/increment/src/test.rs) file to follow along. ```rust title="increment/src/test.rs" #[test] fn test() { let env = Env::default(); let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.increment(), 1); assert_eq!(client.increment(), 2); assert_eq!(client.increment(), 3); } ``` In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. ```rust let env = Env::default(); ``` The contract is registered with the environment using the contract type. ```rust let contract_id = env.register(IncrementContract, ()); ``` All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. ```rust let client = IncrementContractClient::new(&env, &contract_id); ``` The test asserts that the result that is returned is as we expect. ```rust assert_eq!(client.increment(), 1); assert_eq!(client.increment(), 2); assert_eq!(client.increment(), 3); ``` ## Build the Contracts To build the contract into a `.wasm` file, use the `stellar contract build` command. ```sh stellar contract build ``` The `.wasm` file should be found in the contract `target` directory after building the contract: ``` target/wasm32v1-none/release/soroban_increment_contract.wasm ``` ## Run the Contract If you have [`stellar-cli`] installed, you can deploy the contract, and invoke the contract function. ### Deploy ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_increment_contract.wasm \ --alias increment_example \ --source-account alice \ --network testnet ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/soroban_increment_contract.wasm ` --alias increment_example ` --source-account alice ` --network testnet ``` ### Invoke ```sh stellar contract invoke \ --id increment_example \ --source-account alice \ --network testnet \ -- \ increment ``` ```powershell stellar contract invoke ` --id increment_example ` --source-account alice ` --network testnet ` -- ` increment ``` ### Result The following output should occur the first time the code above is used. ``` 1 ``` The value will be incremented every time the contract function is invoked. [`stellar-cli`]: ../getting-started/setup.mdx#install-the-stellar-cli --- ## Timelock {`Lockup some token to be claimed by another user under set conditions`} The [timelock example] demonstrates how to write a timelock and implements a greatly simplified claimable balance similar to the [claimable balance] feature available on Stellar. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] The contract accepts deposits of an amount of a token, and allows other users to claim it before or after a time point. [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [timelock example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/timelock [claimable balance]: ../../guides/transactions/claimable-balances.mdx --- ## Tokens Write a CAP-46-6 compliant token contract The [token example] demonstrates how to write a token contract that implements the [Token Interface]. :::tip This example shows how to build a token from scratch using only the `soroban-sdk`. For many use cases, issuing a Stellar asset and using its built-in [Stellar Asset Contract (SAC)](../../../tokens/stellar-asset-contract.mdx) is all you need. If you want a custom token contract, the fastest path is the audited [OpenZeppelin Stellar Contracts](https://github.com/OpenZeppelin/stellar-contracts) library, which has easy-to-follow examples in its docs for [fungible tokens](https://docs.openzeppelin.com/stellar-contracts/tokens/fungible/fungible) and [non-fungible tokens (NFTs)](https://docs.openzeppelin.com/stellar-contracts/tokens/non-fungible/non-fungible). ::: [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [token example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/token [token interface]: ../../../tokens/token-interface.mdx :::info[Whisk Changes] With the release of Whisk, Protocol 23, the [token interface] has seen some changes to incorporate the `MuxedAddress` type into the `transfer` function. Please see the [Rust SDK documentation](https://docs.rs/soroban-token-sdk/latest/soroban_token_sdk/_migrating/v23_token_transfer/index.html) for more details. ::: ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: ```sh git clone -b v23.0.0 https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example, navigate to the `hello_world` directory, and use `cargo test`. ```sh cd token cargo test ``` You should see the output: ``` running 6 tests test test::decimal_is_over_eighteen - should panic ... ok test test::test_zero_allowance ... ok test test::transfer_insufficient_balance - should panic ... ok test test::transfer_from_insufficient_allowance - should panic ... ok test test::test_burn ... ok test test::test ... ok ``` [setup]: ../getting-started/setup.mdx ## Code :::note The source code for this [token example] is broken into several smaller modules. This is a common design pattern for more complex smart contracts. ::: ```rust title="token/src/lib.rs" #![no_std] mod admin; mod allowance; mod balance; mod contract; mod metadata; mod storage_types; mod test; pub use crate::contract::TokenClient; ``` ```rust title="token/src/admin.rs" use soroban_sdk::{Address, Env}; use crate::storage_types::DataKey; pub fn read_administrator(e: &Env) -> Address { let key = DataKey::Admin; e.storage().instance().get(&key).unwrap() } pub fn write_administrator(e: &Env, id: &Address) { let key = DataKey::Admin; e.storage().instance().set(&key, id); } ``` ```rust title="token/src/allowance.rs" use crate::storage_types::{AllowanceDataKey, AllowanceValue, DataKey}; use soroban_sdk::{Address, Env}; pub fn read_allowance(e: &Env, from: Address, spender: Address) -> AllowanceValue { let key = DataKey::Allowance(AllowanceDataKey { from, spender }); if let Some(allowance) = e.storage().temporary().get::<_, AllowanceValue>(&key) { if allowance.expiration_ledger < e.ledger().sequence() { AllowanceValue { amount: 0, expiration_ledger: allowance.expiration_ledger, } } else { allowance } } else { AllowanceValue { amount: 0, expiration_ledger: 0, } } } pub fn write_allowance( e: &Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32, ) { let allowance = AllowanceValue { amount, expiration_ledger, }; if amount > 0 && expiration_ledger < e.ledger().sequence() { panic!("expiration_ledger is less than ledger seq when amount > 0") } let key = DataKey::Allowance(AllowanceDataKey { from, spender }); e.storage().temporary().set(&key.clone(), &allowance); if amount > 0 { let live_for = expiration_ledger .checked_sub(e.ledger().sequence()) .unwrap(); e.storage().temporary().extend_ttl(&key, live_for, live_for) } } pub fn spend_allowance(e: &Env, from: Address, spender: Address, amount: i128) { let allowance = read_allowance(e, from.clone(), spender.clone()); if allowance.amount < amount { panic!("insufficient allowance"); } if amount > 0 { write_allowance( e, from, spender, allowance.amount - amount, allowance.expiration_ledger, ); } } ``` ```rust title="token/src/balance.rs" use crate::storage_types::{DataKey, BALANCE_BUMP_AMOUNT, BALANCE_LIFETIME_THRESHOLD}; use soroban_sdk::{Address, Env}; pub fn read_balance(e: &Env, addr: Address) -> i128 { let key = DataKey::Balance(addr); if let Some(balance) = e.storage().persistent().get::(&key) { e.storage() .persistent() .extend_ttl(&key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT); balance } else { 0 } } fn write_balance(e: &Env, addr: Address, amount: i128) { let key = DataKey::Balance(addr); e.storage().persistent().set(&key, &amount); e.storage() .persistent() .extend_ttl(&key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT); } pub fn receive_balance(e: &Env, addr: Address, amount: i128) { let balance = read_balance(e, addr.clone()); write_balance(e, addr, balance + amount); } pub fn spend_balance(e: &Env, addr: Address, amount: i128) { let balance = read_balance(e, addr.clone()); if balance < amount { panic!("insufficient balance"); } write_balance(e, addr, balance - amount); } ``` ```rust title="token/src/contract.rs" //! This contract demonstrates a sample implementation of the Soroban token //! interface. use crate::admin::{read_administrator, write_administrator}; use crate::allowance::{read_allowance, spend_allowance, write_allowance}; use crate::balance::{read_balance, receive_balance, spend_balance}; use crate::metadata::{read_decimal, read_name, read_symbol, write_metadata}; #[cfg(test)] use crate::storage_types::{AllowanceDataKey, AllowanceValue, DataKey}; use crate::storage_types::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; use soroban_sdk::{ contract, contractevent, contractimpl, token::TokenInterface, Address, Env, MuxedAddress, String, }; use soroban_token_sdk::events; use soroban_token_sdk::metadata::TokenMetadata; fn check_nonnegative_amount(amount: i128) { if amount < 0 { panic!("negative amount is not allowed: {}", amount) } } #[contract] pub struct Token; // SetAdmin is not a standardized token event, so we just define a custom event // for our token. #[contractevent(data_format = "single-value")] pub struct SetAdmin { #[topic] admin: Address, new_admin: Address, } #[contractimpl] impl Token { pub fn __constructor(e: Env, admin: Address, decimal: u32, name: String, symbol: String) { if decimal > 18 { panic!("Decimal must not be greater than 18"); } write_administrator(&e, &admin); write_metadata( &e, TokenMetadata { decimal, name, symbol, }, ) } pub fn mint(e: Env, to: Address, amount: i128) { check_nonnegative_amount(amount); let admin = read_administrator(&e); admin.require_auth(); e.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); receive_balance(&e, to.clone(), amount); events::MintWithAmountOnly { to, amount }.publish(&e); } pub fn set_admin(e: Env, new_admin: Address) { let admin = read_administrator(&e); admin.require_auth(); e.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); write_administrator(&e, &new_admin); SetAdmin { admin, new_admin }.publish(&e); } #[cfg(test)] pub fn get_allowance(e: Env, from: Address, spender: Address) -> Option { let key = DataKey::Allowance(AllowanceDataKey { from, spender }); let allowance = e.storage().temporary().get::<_, AllowanceValue>(&key); allowance } } #[contractimpl] impl TokenInterface for Token { fn allowance(e: Env, from: Address, spender: Address) -> i128 { e.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); read_allowance(&e, from, spender).amount } fn approve(e: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32) { from.require_auth(); check_nonnegative_amount(amount); e.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); write_allowance(&e, from.clone(), spender.clone(), amount, expiration_ledger); events::Approve { from, spender, amount, expiration_ledger, } .publish(&e); } fn balance(e: Env, id: Address) -> i128 { e.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); read_balance(&e, id) } fn transfer(e: Env, from: Address, to_muxed: MuxedAddress, amount: i128) { from.require_auth(); check_nonnegative_amount(amount); e.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); spend_balance(&e, from.clone(), amount); let to: Address = to_muxed.address(); receive_balance(&e, to.clone(), amount); events::Transfer { from, to, to_muxed_id: to_muxed.id(), amount, } .publish(&e); } fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128) { spender.require_auth(); check_nonnegative_amount(amount); e.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); spend_allowance(&e, from.clone(), spender, amount); spend_balance(&e, from.clone(), amount); receive_balance(&e, to.clone(), amount); events::Transfer { from, to, // `transfer_from` does not support muxed destination. to_muxed_id: None, amount, } .publish(&e); } fn burn(e: Env, from: Address, amount: i128) { from.require_auth(); check_nonnegative_amount(amount); e.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); spend_balance(&e, from.clone(), amount); events::Burn { from, amount }.publish(&e); } fn burn_from(e: Env, spender: Address, from: Address, amount: i128) { spender.require_auth(); check_nonnegative_amount(amount); e.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); spend_allowance(&e, from.clone(), spender, amount); spend_balance(&e, from.clone(), amount); events::Burn { from, amount }.publish(&e); } fn decimals(e: Env) -> u32 { read_decimal(&e) } fn name(e: Env) -> String { read_name(&e) } fn symbol(e: Env) -> String { read_symbol(&e) } } ``` ```rust title="token/src/metadata.rs" use soroban_sdk::{Env, String}; use soroban_token_sdk::{metadata::TokenMetadata, TokenUtils}; pub fn read_decimal(e: &Env) -> u32 { let util = TokenUtils::new(e); util.metadata().get_metadata().decimal } pub fn read_name(e: &Env) -> String { let util = TokenUtils::new(e); util.metadata().get_metadata().name } pub fn read_symbol(e: &Env) -> String { let util = TokenUtils::new(e); util.metadata().get_metadata().symbol } pub fn write_metadata(e: &Env, metadata: TokenMetadata) { let util = TokenUtils::new(e); util.metadata().set_metadata(&metadata); } ``` ```rust title="token/src/storage_types.rs" use soroban_sdk::{contracttype, Address}; pub(crate) const DAY_IN_LEDGERS: u32 = 17280; pub(crate) const INSTANCE_BUMP_AMOUNT: u32 = 7 * DAY_IN_LEDGERS; pub(crate) const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; pub(crate) const BALANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; pub(crate) const BALANCE_LIFETIME_THRESHOLD: u32 = BALANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; #[derive(Clone)] #[contracttype] pub struct AllowanceDataKey { pub from: Address, pub spender: Address, } #[contracttype] pub struct AllowanceValue { pub amount: i128, pub expiration_ledger: u32, } #[derive(Clone)] #[contracttype] pub enum DataKey { Allowance(AllowanceDataKey), Balance(Address), State(Address), Admin, } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/token ## How it Works Tokens created on a smart contract platform can take many different forms, include a variety of different functionalities, and meet very different needs or use-cases. While each token can fulfill a unique niche, there are some "normal" features that almost all tokens will need to make use of (e.g., payments, transfers, balance queries, etc.). In an effort to minimize repetition and streamline token deployments, Soroban implements the [Token Interface], which provides a uniform, predictable interface for developers and users. Creating a Soroban token compatible contract from an existing Stellar asset is very easy, it requires deploying the built-in [Stellar Asset Contract]. This example contract, however, demonstrates how a smart contract token might be constructed that doesn't take advantage of the Stellar Asset Contract, but does still satisfy the commonly used Token Interface to maximize interoperability. [stellar asset contract]: ../../../tokens/stellar-asset-contract.mdx ### Separation of Functionality You have likely noticed that this example contract is broken into discrete modules, with each one responsible for a siloed set of functionality. This common practice helps to organize the code and make it more maintainable. For example, most of the token logic exists in the `contract.rs` module. Functions like `mint`, `burn`, `transfer`, etc. are written and programmed in that file. The Token Interface describes how some of these functions should emit events when they occur. However, keeping all that event-emitting logic bundled in with the rest of the contract code could make it harder to track what is happening in the code, and that confusion could ultimately lead to errors. Instead, we have a separate `soroban_token_sdk::events` module that takes away all the headache of emitting events when other functions run. Here is the event emitted when a token is minted: ```rust events::MintWithAmountOnly { to, amount }.publish(&e); ``` Admittedly, this is a simple example, but constructing the contract this way makes it very clear to the developer what is happening and where. This function is then used by the `contract.rs` module whenever the `mint` function is invoked: ```rust // earlier in `contract.rs` use soroban_token_sdk::events; pub fn mint(e: Env, to: Address, amount: i128) { check_nonnegative_amount(amount); let admin = read_administrator(&e); admin.require_auth(); e.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); receive_balance(&e, to.clone(), amount); // highlight-next-line events::MintWithAmountOnly { to, amount }.publish(&e); } ``` This same convention is used to separate from the "main" contract code the metadata for the token, the storage type definitions, etc. ### Standardized Interface, Customized Behavior This example contract follows the standardized [Token Interface], implementing all of the same functions as the [Stellar Asset Contract] (SAC). This gives wallets, users, developers, etc. a predictable interface to interact with the token. Even though we are implementing the same _interface_ of functions, that doesn't mean we have to implement the same _behavior_ inside those functions. While this example contract doesn't actually modify any of the functions that would be present in a deployed instance of the SAC, that possibility remains open to the contract developer. By way of example, perhaps you have an NFT project, and the artist wants to have a small royalty paid every time their token transfers hands: ```rust // This is mainly the `transfer` function from `token/src/contract.rs` fn transfer(e: Env, from: Address, to_muxed: MuxedAddress, amount: i128) { from.require_auth(); check_nonnegative_amount(amount); spend_balance(&e, from.clone(), amount); let to: Address = to_muxed.address(); // highlight-start // We calculate some new amounts for payment and royalty let payment = (amount * 997) / 1000; let royalty = amount - payment let artist = read_artist(&e); receive_balance(&e, artist.clone(), royalty); events::TransferWithAmountOnly { to: artist.clone(), amount: royalty, }.publish(&e); // highlight-end receive_balance(&e, to.clone(), payment); events::Transfer { from, to, to_muxed_id: to_muxed.id(), amount, } .publish(&e); } ``` The `transfer` interface is still in use, and is still the same as other tokens, but we've customized the behavior to address a specific need. Another use-case might be a tightly controlled token that requires authentication from an admin before any `transfer`, `allowance`, etc. function could be invoked. :::tip Of course, you will want your token to behave in an _intuitive_ and _transparent_ manner. If a user is invoking a `transfer`, they will expect tokens to move. If an asset issuer needs to invoke a `clawback` they will likely _require_ the right kind of behavior to take place. ::: ## Tests Open the [`token/src/test.rs`](https://github.com/stellar/soroban-examples/tree/v23.0.0/token/src/test.rs) file to follow along. ```rust title="token/src/test.rs" #![cfg(test)] extern crate std; use crate::{contract::Token, TokenClient}; use soroban_sdk::{ symbol_short, testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, Address, Env, FromVal, IntoVal, String, Symbol, }; fn create_token<'a>(e: &Env, admin: &Address) -> TokenClient<'a> { let token_contract = e.register( Token, ( admin, 7_u32, String::from_val(e, &"name"), String::from_val(e, &"symbol"), ), ); TokenClient::new(e, &token_contract) } #[test] fn test() { let e = Env::default(); e.mock_all_auths(); let admin1 = Address::generate(&e); let admin2 = Address::generate(&e); let user1 = Address::generate(&e); let user2 = Address::generate(&e); let user3 = Address::generate(&e); let token = create_token(&e, &admin1); token.mint(&user1, &1000); assert_eq!( e.auths(), std::vec![( admin1.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( token.address.clone(), symbol_short!("mint"), (&user1, 1000_i128).into_val(&e), )), sub_invocations: std::vec![] } )] ); assert_eq!(token.balance(&user1), 1000); token.approve(&user2, &user3, &500, &200); assert_eq!( e.auths(), std::vec![( user2.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( token.address.clone(), symbol_short!("approve"), (&user2, &user3, 500_i128, 200_u32).into_val(&e), )), sub_invocations: std::vec![] } )] ); assert_eq!(token.allowance(&user2, &user3), 500); token.transfer(&user1, &user2, &600); assert_eq!( e.auths(), std::vec![( user1.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( token.address.clone(), symbol_short!("transfer"), (&user1, &user2, 600_i128).into_val(&e), )), sub_invocations: std::vec![] } )] ); assert_eq!(token.balance(&user1), 400); assert_eq!(token.balance(&user2), 600); token.transfer_from(&user3, &user2, &user1, &400); assert_eq!( e.auths(), std::vec![( user3.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( token.address.clone(), Symbol::new(&e, "transfer_from"), (&user3, &user2, &user1, 400_i128).into_val(&e), )), sub_invocations: std::vec![] } )] ); assert_eq!(token.balance(&user1), 800); assert_eq!(token.balance(&user2), 200); token.transfer(&user1, &user3, &300); assert_eq!(token.balance(&user1), 500); assert_eq!(token.balance(&user3), 300); token.set_admin(&admin2); assert_eq!( e.auths(), std::vec![( admin1.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( token.address.clone(), symbol_short!("set_admin"), (&admin2,).into_val(&e), )), sub_invocations: std::vec![] } )] ); // Increase to 500 token.approve(&user2, &user3, &500, &200); assert_eq!(token.allowance(&user2, &user3), 500); token.approve(&user2, &user3, &0, &200); assert_eq!( e.auths(), std::vec![( user2.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( token.address.clone(), symbol_short!("approve"), (&user2, &user3, 0_i128, 200_u32).into_val(&e), )), sub_invocations: std::vec![] } )] ); assert_eq!(token.allowance(&user2, &user3), 0); } #[test] fn test_burn() { let e = Env::default(); e.mock_all_auths(); let admin = Address::generate(&e); let user1 = Address::generate(&e); let user2 = Address::generate(&e); let token = create_token(&e, &admin); token.mint(&user1, &1000); assert_eq!(token.balance(&user1), 1000); token.approve(&user1, &user2, &500, &200); assert_eq!(token.allowance(&user1, &user2), 500); token.burn_from(&user2, &user1, &500); assert_eq!( e.auths(), std::vec![( user2.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( token.address.clone(), symbol_short!("burn_from"), (&user2, &user1, 500_i128).into_val(&e), )), sub_invocations: std::vec![] } )] ); assert_eq!(token.allowance(&user1, &user2), 0); assert_eq!(token.balance(&user1), 500); assert_eq!(token.balance(&user2), 0); token.burn(&user1, &500); assert_eq!( e.auths(), std::vec![( user1.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( token.address.clone(), symbol_short!("burn"), (&user1, 500_i128).into_val(&e), )), sub_invocations: std::vec![] } )] ); assert_eq!(token.balance(&user1), 0); assert_eq!(token.balance(&user2), 0); } #[test] #[should_panic(expected = "insufficient balance")] fn transfer_insufficient_balance() { let e = Env::default(); e.mock_all_auths(); let admin = Address::generate(&e); let user1 = Address::generate(&e); let user2 = Address::generate(&e); let token = create_token(&e, &admin); token.mint(&user1, &1000); assert_eq!(token.balance(&user1), 1000); token.transfer(&user1, &user2, &1001); } #[test] #[should_panic(expected = "insufficient allowance")] fn transfer_from_insufficient_allowance() { let e = Env::default(); e.mock_all_auths(); let admin = Address::generate(&e); let user1 = Address::generate(&e); let user2 = Address::generate(&e); let user3 = Address::generate(&e); let token = create_token(&e, &admin); token.mint(&user1, &1000); assert_eq!(token.balance(&user1), 1000); token.approve(&user1, &user3, &100, &200); assert_eq!(token.allowance(&user1, &user3), 100); token.transfer_from(&user3, &user1, &user2, &101); } #[test] #[should_panic(expected = "Decimal must not be greater than 18")] fn decimal_is_over_eighteen() { let e = Env::default(); let admin = Address::generate(&e); let _ = TokenClient::new( &e, &e.register( Token, ( admin, 19_u32, String::from_val(&e, &"name"), String::from_val(&e, &"symbol"), ), ), ); } #[test] fn test_zero_allowance() { // Here we test that transfer_from with a 0 amount does not create an empty allowance let e = Env::default(); e.mock_all_auths(); let admin = Address::generate(&e); let spender = Address::generate(&e); let from = Address::generate(&e); let token = create_token(&e, &admin); token.transfer_from(&spender, &from, &spender, &0); assert!(token.get_allowance(&from, &spender).is_none()); } ``` The token example implements eight different tests to cover a wide array of potential behaviors and problems. However, all of the tests start with a few common pieces. In any test, the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. ```rust let e = Env::default(); ``` We mock authentication checks in the tests, which allows the tests to proceed as if all users/addresses/contracts/etc. had successfully authenticated. ```rust e.mock_all_auths(); ``` We're also using a `create_token` function to ease the repetition of having to register our token contract. The resulting `token` client is then used to invoke the contract during each test. ```rust // It is defined at the top of the file... fn create_token<'a>(e: &Env, admin: &Address) -> TokenClient<'a> { let token_contract = e.register( Token, ( admin, 7_u32, String::from_val(e, &"name"), String::from_val(e, &"symbol"), ), ); TokenClient::new(e, &token_contract) } // ... and it is used inside each test let token = create_token(&e, &admin); ``` All public functions within an `impl` block that has been annotated with the `#[contractimpl]` attribute will have a corresponding function in the test's generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract, the contract type is named `Token`, and the client type is named `TokenClient`. The six tests created for this example contract test a range of possible conditions and ensure the contract responds appropriately to each one: - **`test()`** - This function makes use of a variety of the built-in token functions to test the "predictable" way an asset might be interacted with by a user, as well as an administrator. - **`test_burn()`** - This function ensures a `burn()` invocation decreases a user's balance, and that a `burn_from()` invocation decreases a user's balance as well as consuming another user's allowance of that balance. - **`transfer_insufficient_balance()`** - This function ensures a `transfer()` invocation panics when the `from` user doesn't have the balance to cover it. - **`transfer_from_insufficient_allowance()`** - This function ensures a user with an existing allowance for someone else's balance cannot make a `transfer()` greater than that allowance. - **`decimal_is_over_eighteen()`** - This function tests that constructing a token with too high of a decimal precision will not succeed. - **`test_zero_allowance()`** - This function makes sure that a `transfer_from()` with an zero balance doesn't create an empty allowance. ## Build the Contract To build the contract, use the `stellar contract build` command. ```sh stellar contract build ``` A `.wasm` file should be outputted in the `target` directory: ```text target/wasm32v1-none/release/soroban_token_contract.wasm ``` ## Run the Contract If you have [`stellar-cli`] installed, you can deploy the contract and invoke its functions. ### Deploy ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_token_contract.wasm \ --alias token_example \ --source-account alice \ --network testnet ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/soroban_token_contract.wasm ` --alias token_example ` --source-account alice ` --network testnet ``` ### Invoke ```sh stellar contract invoke \ --id token_example \ --source-account alice \ --network testnet \ -- \ balance \ --id GBZV3NONYSUDVTEHATQO4BCJVFXJO3XQU5K32X3XREVZKSMMOZFO4ZXR ``` ```powershell stellar contract invoke ` --id token_example ` --source-account alice ` --network testnet ` -- ` balance ` --id GBZV3NONYSUDVTEHATQO4BCJVFXJO3XQU5K32X3XREVZKSMMOZFO4ZXR ``` [`stellar-cli`]: ../../../tools/cli/stellar-cli.mdx --- ## Upgradeable Contract Upgrading Wasm Bytecode for a Deployed Contract The [upgrading contracts](../../guides/conventions/upgrading-contracts.mdx) page demonstrates how to upgrade the Wasm bytecode using example contracts. --- ## Workspace The [workspace example] demonstrates how multiple smart contracts can be developed, tested, and built side-by-side in the same Rust workspace. [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples [workspace example]: https://github.com/stellar/soroban-examples/tree/v23.0.0/workspace ## Run the Example First go through the [Setup] process to get your development environment configured, then clone the `v23.0.0` tag of `soroban-examples` repository: [Setup]: ../getting-started/setup.mdx ```sh git clone https://github.com/stellar/soroban-examples ``` Or, skip the development environment setup and open this example in [GitHub Codespaces][open-in-github-codespaces] or [Code Anywhere][open-in-code-anywhere]. To run the tests for the example use `cargo test`. ```sh cd workspace cargo test ``` You should see three sets of output, one for `contract_a`, `contract_a_interface`, and `contract_b`. The first two crates in the workspace contain no tests, but the third crate should give you the following output: ```text running 1 test test test::test ... ok ``` ## Code ```rust title="workspace/contract_a_interface/src/lib.rs" #![no_std] use soroban_sdk::contractclient; #[contractclient(name = "ContractAClient")] pub trait ContractAInterface { fn add(x: u32, y: u32) -> u32; } ``` ```rust title="workspace/contract_a/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl}; use soroban_workspace_contract_a_interface::ContractAInterface; #[contract] pub struct ContractA; #[contractimpl] impl ContractAInterface for ContractA { fn add(x: u32, y: u32) -> u32 { x.checked_add(y).expect("no overflow") } } ``` ```rust title="workspace/contract_b/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl, Address, Env}; use soroban_workspace_contract_a_interface::ContractAClient; #[contract] pub struct ContractB; #[contractimpl] impl ContractB { pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 { let client = ContractAClient::new(&env, &contract); client.add(&x, &y) } } mod test; ``` ```rust title="workspace/src/contract_b/src/test.rs" #![cfg(test)] use soroban_sdk::Env; use crate::{ContractB, ContractBClient}; use soroban_workspace_contract_a::ContractA; #[test] fn test() { let env = Env::default(); // Register contract A using the native contract imported. let contract_a_id = env.register(ContractA, ()); // Register contract B defined in this crate. let contract_b_id = env.register(ContractB, ()); // Create a client for calling contract B. let client = ContractBClient::new(&env, &contract_b_id); // Invoke contract B via its client. Contract B will invoke contract A. let sum = client.add_with(&contract_a_id, &5, &7); assert_eq!(sum, 12); } ``` Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/workspace ## How It Works There are three crates that are part of the workspace. 1. `contract_a_interface` contains a trait, `ContractAInterface`, that only serves as a place to define the interface trait for Contract A. 2. `contract_a` contains a smart contract, `ContractA`, that implements logic, and conforms to the `ContractAInterface` trait. 3. `contract_b` is another smart contract, implementing a different interface, and makes a call to `ContractA`, and cross-calls the `contract_a` function. This is also the only crate in the workspace with defined tests. Let's take a look at each crate, and see how they all work together. ### Contract A Interface: The Trait The `contract_a_interface` crate defines a trait containing a contract interface. This interface is defined separate from the implementation and only defines what the exported functions of the implementation. The interface defines `add` as the only function the contract will contain. The `add` function requires two inputs, `x` and `y`, both `u32` integers, and returns a `u32` integer as well. The use of `contractclient` as an attribute macro on `ContractAInterface` means a client will be created, conforming to this interface, that can be used by contracts existing outside of the `contract_a_interface` crate. As you'll see later, the client, `ContractAClient`, is used in the `contract_b` crate to call the `add` function. ### Contract A: The Logic The `contract_a` crate contains the implementation for Contract A, defining for each function what it should actually _do_. The implementation takes the value of `x`, along with the value of `y`, and performs a `checked_add`, returning the sum of both numbers (while avoiding an overflow error). :::info The `add` function doesn't require an `Env` argument. That's totally fine! `Env` has many useful features that are available to your smart contracts, if you need them. But, you're not at all required to use it, if you don't. ::: All that is required to make use of the previously defined `ContractAInterface` is to `use` the interface, define a `ContractA` struct, and `impl` the interface for that struct. This crate uses the `contractimpl` attribute macro on the `ContractA` implementation, making the `add` function public and invocable by others on the Stellar network. ### Contract B: The Invocation Now that we've created a trait in `contract_a_interface`, and implemented it in `contract_a`, we can use the `contract_b` crate to invoke the `add` function and get the sum of our integers. We're creating and implementing an entirely new contract, `ContractB`. We're skipping the trait, and getting right to the contract itself. ::: info Defining the contract interface with a `trait` separately is optional. It's a great way to share the interface of a contract, and a contract client, with multiple contracts in a multi-contract workspace. ::: `ContractB` has one function, `add_with`, that requires three arguments: - `contract: Address` - The address of a contract which implements that required client interface, `ContractAInterface`. - `x: u32` and `y: u32` - The two numbers we want to (safely) compute the sum of. `ContractB` invokes `ContractA`'s `add` function, returning its value back to the original invoker. It's a bit of a long round-trip for this simple example, but it illustrates a really powerful way you can separate out interface/trait definitions from contract logic and share it and its client in a multi-contract workspace. ## Practical Use-Case Examples Beyond this simple example, this technique is versatile and useful. For example, this strategy could be used to: - Create and reference a standardized, consistent token interface. - Reuse a single interface that you want to incorporate across many different contracts. ## Build the Contracts To build the contracts into a set of `.wasm` files, use the `stellar contract build` command. Both `workspace/contract_a` and `workspace/contract_b` will be built, and you can use a single command, since our workspace defines its `members` in the `Cargo.toml` file: ```sh stellar contract build ``` Two `.wasm` files should be found in the `workspace/target` directory: ```text target/wasm32v1-none/release/soroban_workspace_contract_a.wasm target/wasm32v1-none/release/soroban_workspace_contract_b.wasm ``` The [`stellar-cli`] knows the `contract_a_interface` is not intended to be compiled into a .wasm, because the `contract_a_interface`'s `Cargo.toml` has its `crate-type` configured as `rlib` (rust library). Nice! ## Run the Contract If you have [`stellar-cli`] installed, you can invoke contract the functions. Both contracts must be deployed. ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_workspace_contract_a.wasm ``` ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/soroban_workspace_contract_b.wasm ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/soroban_workspace_contract_a.wasm ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/soroban_workspace_contract_b.wasm ``` Invoke `ContractB`'s `add_with` function, passing in `ContractA`'s address for `contract`, and integer values for `x` and `y` (e.g. as `5` and `7`). ```sh stellar contract invoke \ --id CONTRACT_B_ADDRESS \ -- \ add_with \ --contract CONTRACT_A_ADDRESS \ --x 5 \ --y 7 ``` ```powershell stellar contract invoke ` --id CONTRACT_B_ADDRESS ` -- ` add_with ` --contract CONTRACT_A_ADDRESS ` --x 5 ` --y 7 ``` [`stellar-cli`]: ../getting-started/setup.mdx#install-the-stellar-cli --- ## Get Started with Smart Contracts: Setup, Write in Rust & Deploy # Getting Started :::tip Start with [Scaffold Stellar](https://scaffoldstellar.org/docs/quick-start) for the fastest path to a working dApp on Stellar. Scaffold Stellar is a toolkit that bundles a CLI, contract templates, a smart contract registry, and a modern frontend for building full-stack dApps. _Use this getting started guide for a lower-level tour of Stellar smart contract development from the main Stellar CLI._ ::: Dive into smart contract development with this Getting Started tutorial. --- ## Deploy the Increment Smart Contract on Testnet Using the CLI: A Guide # 4. Deploy the Increment Contract ## Two-step deployment It's worth knowing that `deploy` is actually a two-step process. 1. **Upload the contract bytes to the network.** Soroban currently refers to this as _installing_ the contract—from the perspective of the blockchain itself, this is a reasonable metaphor. This uploads the bytes of the contract to the network, indexing it by its hash. This contract code can now be referenced by multiple contracts, which means they would have the exact same _behavior_ but separate storage state. 2. **Instantiate the contract.** This actually creates what you probably think of as a Smart Contract. It makes a new contract ID, and associates it with the contract bytes that were uploaded in the previous step. You can run these two steps separately. Let's try it with the Increment contract: :::info If the contract has not been build yet, run the build command `stellar contract build` from the contract's root directory. ::: ```sh stellar contract upload \ --network testnet \ --source-account alice \ --wasm target/wasm32v1-none/release/increment.wasm ``` ```powershell stellar contract upload ` --network testnet ` --source-account alice ` --wasm target/wasm32v1-none/release/increment.wasm ``` This returns the hash of the Wasm bytes, like `6ddb28e0980f643bb97350f7e3bacb0ff1fe74d846c6d4f2c625e766210fbb5b`. Now you can use `--wasm-hash` with `deploy` rather than `--wasm. Make sure to replace the example wasm hash with your own. ```sh stellar contract deploy \ --wasm-hash 6ddb28e0980f643bb97350f7e3bacb0ff1fe74d846c6d4f2c625e766210fbb5b \ --source-account alice \ --network testnet \ --alias increment ``` ```powershell stellar contract deploy ` --wasm-hash 6ddb28e0980f643bb97350f7e3bacb0ff1fe74d846c6d4f2c625e766210fbb5b ` --source-account alice ` --network testnet ` --alias increment ``` This command will return the contract id (e.g. `CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN`), and you can use it to invoke the contract like we did in previous examples. ```sh stellar contract invoke \ --id CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN \ --source-account alice \ --network testnet \ -- \ increment ``` ```powershell stellar contract invoke ` --id CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN ` --source-account alice ` --network testnet ` -- ` increment ``` You should see the following output: ```sh 1 ``` Run it a few more times to watch the count change. ## Run your own network/node Sometimes you'll need to run your own node: - Production apps! Stellar maintains public test RPC nodes for Testnet and Futurenet, but not for Mainnet. Instead, you will need to run your own node, and point your app at that. If you want to use a software-as-a-service platform for this, [various providers](../../../data/apis/rpc/providers.mdx) are available. - When you need a network that differs from the version deployed to Testnet. The RPC team maintains Docker containers that make this as straightforward as possible. See the [RPC](../../../data/apis/rpc/admin-guide/README.mdx) reference for details. ## Up next Ready to turn these deployed contracts into a simple web application? Head over to the [Build a Dapp Frontend section](../../apps/dapp-frontend.mdx). --- ## Deploy and Debug Smart Contracts on Testnet & Interact with Other Contracts # 2. Deploy to Testnet To recap what we've done so far, in [Setup](setup.mdx): - we set up our local environment to write Rust smart contracts - installed the stellar-cli - created a `hello-world` project, then tested and built the `HelloWorld` contract In this guide, we’ll generate a Testnet-funded identity, deploy the contract to Testnet, and interact with it using the Stellar CLI. ## Configure a Source Account When you deploy a smart contract to a network, you need to specify a source account's keypair that will be used to sign the transactions. Let's generate a keypair called `alice`. You can use any name you want, but it might be nice to have some named keys that you can use for testing, such as [`alice`, `bob`, and `carol`](https://en.wikipedia.org/wiki/Alice_and_Bob). Notice that the keypair's account will be funded using [Friendbot](../../../networks/README.mdx#friendbot). ```sh stellar keys generate alice --network testnet --fund ``` You can see the public key of `alice` with: ```sh stellar keys address alice ``` You can see all of the keys you generated, along with where on your filesystem their information is stored, with: ```sh stellar keys ls -l ``` ## Deploy To deploy your HelloWorld contract, run the following command: ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/hello_world.wasm \ --source-account alice \ --network testnet \ --alias hello_world ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/hello_world.wasm ` --source-account alice ` --network testnet ` --alias hello_world ``` This returns the contract's id, starting with a `C`. In this example, we're going to use `CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN`, so replace it with your actual contract id. :::tip We used the `--alias` flag in this deploy command which will create a `~/.config/stellar/contract-ids/hello_world.json` file that maps the alias `hello_world` to the contract id and network. This allows us to refer to this contract as its alias instead the contract id. ::: ## Interact Using the code we wrote in [Write a Contract](./hello-world.mdx#contract-source-code) and the resulting `.wasm` file we built in [Build](hello-world.mdx#build-the-contract), run the following command to invoke the `hello` function. :::info In the background, the CLI is making RPC calls. For information on that checkout out the [RPC](../../../data/apis/rpc/admin-guide/README.mdx) reference page. ::: ```sh stellar contract invoke \ --id CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN \ --source-account alice \ --network testnet \ -- \ hello \ --to RPC ``` ```powershell stellar contract invoke ` --id CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN ` --source-account alice ` --network testnet ` -- ` hello ` --to RPC ``` The following output should appear. ```json ["Hello", "RPC"] ``` :::note You may also see a message like: ``` ℹ️ Simulation identified as read-only. Send by rerunning with `--send=yes`. ``` This is expected! The `hello` function is **read-only**—it doesn't modify any on-chain state. When the CLI detects a read-only function, it automatically runs it as a **local simulation** instead of submitting a transaction to the network. This means: - **No fees**: The simulation is free—no transaction is submitted to the ledger. - **Instant result**: The output you see (`["Hello", "RPC"]`) is already the correct result. - **Ignore `--send=yes`**: For read-only functions, the simulation already gave you what you need. The `--send=yes` flag is only needed for **state-changing functions**—those that write to contract storage. For those, the CLI first simulates the transaction (to estimate fees and check for errors), then you use `--send=yes` to actually submit and persist changes on-chain. ::: :::info The `--` double-dash is required! This is a general [CLI pattern](https://unix.stackexchange.com/questions/11376/what-does-double-dash-mean) used by other commands like [cargo run](https://doc.rust-lang.org/cargo/commands/cargo-run.html). Everything after the `--`, sometimes called [slop](https://github.com/clap-rs/clap/issues/971), is passed to a child process. In this case, `stellar contract invoke` builds an _implicit CLI_ on-the-fly for the `hello` method in your contract. It can do this because Soroban SDK embeds your contract's schema / interface types right in the `.wasm` file that gets deployed on-chain. You can also try: ```sh stellar contract invoke ... -- --help ``` and ```sh stellar contract invoke ... -- hello --help ``` ::: ## Summary In this lesson, we learned how to: - deploy a contract to Testnet - interact with a deployed contract Next we'll add a new contract to this project, and see how our workspace can accommodate a multi-contract project. The new contract will show off a little bit of Soroban's storage capabilities. --- ## Build a frontend for the Hello World contract # 5. Build a Hello World Frontend In the previous examples, we invoked the contracts using the Stellar CLI, and in this last part of the guide we'll create a web app that interacts with the Hello World contract through TypeScript bindings. :::info This example shows one way of creating a binding between a contract and a frontend. For a more comprehensive guide to Dapp frontends, see the [Build a Dapp Frontend](../../apps/dapp-frontend.mdx) documentation. For tooling that helps you start with smart contracts integrated with a working frontend environment quickly, jump to learn more about [Scaffold Stellar](#using-scaffold-stellar-to-rapidly-develop-dapps). ::: ## Initialize a frontend toolchain from scratch You can build a Stellar dapp with any frontend toolchain or integrate it into any existing full-stack app. For this tutorial, we're going to use [Astro](https://astro.build). Astro works with React, Vue, Svelte, any other UI library, or no UI library at all. In this tutorial, we're not using a UI library. The smart contract-specific parts of this tutorial will be similar no matter what frontend toolchain you use. If you're new to frontend, don't worry. We won't go too deep. But it will be useful for you to see and experience the frontend development process used by smart contract apps. We'll cover the relevant bits of JavaScript and Astro, but teaching all of frontend development and Astro is beyond the scope of this tutorial. Let's get started. You're going to need [Node.js](https://nodejs.org/en/download/package-manager) v20 or greater. If you haven't yet, install it now. We want to create an Astro project with the Hello World contract from the previous lessons integrated. To do this, we install the default Astro project: ```sh npm create astro@latest ``` This project has the following directory structure. ```sh extra-escape ├── astro.config.mjs ├── package-lock.json ├── package.json ├── packages ├── public ├── README.md ├── src │   ├── assets │   │   ├── astro.svg │   │   └── background.svg │   ├── components │   │   └── Welcome.astro │   ├── layouts │   │   └── Layout.astro │   └── pages │   └── index.astro └── tsconfig.json ``` ## Generate an NPM package for the Hello World contract Before we open the new frontend files, let's generate an NPM package for the Hello World contract. This is our suggested way to interact with contracts from frontends. These generated libraries work with any JavaScript project (not a specific UI like React), and make it easy to work with some of the trickiest bits of smart contracts on Stellar, like encoding [XDR](../../../learn/fundamentals/contract-development/types/fully-typed-contracts.mdx). This is going to use the CLI command `stellar contract bindings typescript`: ```sh stellar contract bindings typescript \ --network testnet \ --contract-id hello_world \ --output-dir packages/hello_world ``` :::tip Notice that we were able to use the contract alias, `hello_world`, in place of the contract id! ::: The binding will be created in as a NPM package in the directory `packages/hello_world` as specified in the CLI command. We'll need to build the bindings package, since (in its initial state) the package is mostly TypeScript types and stubs for the various contract functions. ```sh cd packages/hello_world npm install npm run build cd ../.. ``` We attempt to keep the code in these generated libraries readable, so go ahead and look around. Open up the new `packages/hello_world` directory in your editor. If you've built or contributed to Node projects, it will all look familiar. You'll see a `package.json` file, a `src` directory, a `tsconfig.json`, and even a README. ## Call the contract from the frontend Now let's open up `src/pages/index.astro` and use the binding to call the `hello` contract function with an argument. The default Astro project consists of a page (`pages/index.astro`) and a welcome component (`component/Welcome.astro`), and we don't need any of that code. Replace the `pages/index.astro` code with this code (the welcome component will not be needed): ```ts title="src/pages/index.astro" --- const contract = new Client.Client({ ...Client.networks.testnet, rpcUrl: 'https://soroban-testnet.stellar.org:443' }); const { result } = await contract.hello({to: "Devs!"}); const greeting = result.join(" "); --- {greeting} ``` First we import the binding library, and then we need to define a contract client we can use for invoking the contract function we deployed to testnet in a previous step. The `hello()` contract function is invoked synchronously with the argument `{to: "Devs!"}` and the expected response is an array consisting of "Hello" and "Devs!". We join the result array and the constant `greeting` should now hold the text `Hello Devs!` Jumping down to the HTML section we now want to display the `greeting` text in the browser. Let's see it in action! Start the dev server: ```sh npm run dev ``` And open [localhost:4321](http://localhost:4321) in your browser. You should see the greeting from the contract! You can try updating the argument to `{ to: 'Stellar' }`. When you save the file, the page will automatically update. :::info When you start up the dev server with `npm run dev`, you will see similar output in your terminal as when you ran `npm run init`. This is because the `dev` script in package.json is set up to run `npm run init` and `astro dev`, so that you can ensure that your deployed contract and your generated NPM package are always in sync. If you want to just start the dev server without the initialize.js script, you can run `npm run astro dev`. ::: ## Using Scaffold Stellar to rapidly develop dapps [Scaffold Stellar](../../../tools/scaffold-stellar.mdx) is a developer toolkit for building decentralized applications and smart contracts on the Stellar blockchain, integrated with a frontend application. Getting a running set of smart contracts and a frontend can be done in just a few short steps with Scaffold Stellar. This tutorial will show you how to make a new Scaffold Stellar project. If you'd like to use existing smart contracts in a Scaffold Stellar project, all you need to do is copy them to the `contracts/` folder in your project root! 1. Install the Scaffold Stellar CLI Since you already have the Stellar CLI installed, you have everything you need to install Scaffold Stellar: ```sh cargo binstall stellar-scaffold-cli ``` We recommend using [binstall](https://crates.io/crates/cargo-binstall) for faster installs, or use `cargo install --locked stellar-scaffold-cli`. Scaffold Stellar is a plugin on the Stellar CLI, meaning you'll use `stellar scaffold` from the command line. 2. Initialize a fresh Scaffold Stellar project ```sh stellar scaffold init ``` Scaffold Stellar is a plugin on the Stellar CLI, so you'll run commands as `stellar scaffold `. `init` initializes a fresh Stellar Scaffold project at the provided path. 3. Install Node dependencies & set up environment variables Make sure you already have [Node.js](https://nodejs.org/en/download/package-manager), and run: ```sh cd # make sure you're in your new project's directory npm install # install frontend dependencies cp .env.example .env # copies the example frontend environment variable file to your own copy ``` 4. Start your app in development mode! ```sh npm start # or npm run dev ``` This command does two tasks concurrently: it runs `stellar scaffold watch --build-clients` which compiles your smart contracts, updating them as you change them, deploying them to your local Stellar chain (make sure Docker is running!), and configures all these settings via your `environments.toml` file in your root project folder. The second task it runs is `vite`, a popular JavaScript build tool to compile and watch your React frontend for changes. After a little compiling time, you'll be able to see your running app at [localhost:5173](http://localhost:5173)! Learn more about [Scaffold Stellar](../../../tools/scaffold-stellar.mdx). --- ## Write, Test, and Deploy a Rust Smart Contract # Hello World Once you've [set up](./setup.mdx) your development environment, you're ready to create your first smart contract. ## Create a New Project Create a new project using the `init` command to create a `soroban-hello-world` project. ```sh stellar contract init soroban-hello-world ``` The `init` command will create a Rust workspace project, using the recommended structure for including Soroban contracts. Let’s take a look at the project structure: ``` . ├── Cargo.lock ├── Cargo.toml ├── README.md └── contracts ├── hello_world │   ├── Cargo.toml │   ├── Makefile │   ├── src │   │   ├── lib.rs │   │   └── test.rs ``` ### Cargo.toml The `Cargo.toml` file at the root of the project is set up as Rust Workspace, which allows us to include multiple smart contracts in one project. #### Rust Workspace The `Cargo.toml` file sets the workspace’s members as all contents of the `contracts` directory and sets the workspace’s `soroban-sdk` dependency version including the `testutils` feature, which will allow test utilities to be generated for calling the contract in tests. ```toml title="Cargo.toml" [workspace] resolver = "2" members = [ "contracts/*", ] [workspace.dependencies] soroban-sdk = "26" ``` :::info The `testutils` are automatically enabled inside [Rust unit tests] inside the same crate as your contract. If you write tests from another crate, you'll need to require the `testutils` feature for those tests and enable the `testutils` feature when running your tests with `cargo test --features testutils` to be able to use those test utilities. ::: #### `release` Profile Configuring the `release` profile to optimize the contract build is critical. The network enforces a maximum contract size (currently 128KB on Mainnet, a value that can change with network configuration). Rust programs, even small ones, without these configurations almost always exceed this size. The `Cargo.toml` file has the following release profile configured. ```toml [profile.release] opt-level = "z" overflow-checks = true debug = 0 strip = "symbols" debug-assertions = false panic = "abort" codegen-units = 1 lto = true ``` #### `release-with-logs` Profile Configuring a `release-with-logs` profile can be useful if you need to build a `.wasm` file that has logs enabled for printing debug logs when using the [`stellar-cli`]. Note that this is not necessary to access debug logs in tests or to use a step-through-debugger. ```toml [profile.release-with-logs] inherits = "release" debug-assertions = true ``` See the [logging example] for more information about how to log. [logging example]: ../example-contracts/logging.mdx ### Contracts Directory The `contracts` directory is where Soroban contracts will live, each in their own directory. There is already a `hello_world` contract in there to get you started. #### Contract-specific Cargo.toml file Each contract should have its own `Cargo.toml` file, which relies on the top-level `Cargo.toml` that we just discussed. This is where we can specify contract-specific package information. ```toml title="contracts/hello_world/Cargo.toml" [package] name = "hello-world" version = "0.0.0" edition = "2021" publish = false ``` The `crate-type` is configured to `cdylib` which is required for building contracts. ```toml [lib] crate-type = ["cdylib"] doctest = false ``` We also have included the soroban-sdk dependency, configured to use the version from the workspace Cargo.toml. ```toml [dependencies] soroban-sdk = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } ``` #### Contract Source Code Creating a Soroban contract involves writing Rust code in the project’s `lib.rs` file. All contracts should begin with `#![no_std]` to ensure that the Rust standard library is not included in the build. The Rust standard library is large and not well suited to being deployed into small programs like those deployed to blockchains. ```rust #![no_std] ``` The contract imports the types and macros that it needs from the `soroban-sdk` crate. ```rust use soroban_sdk::{contract, contractimpl, vec, Env, String, Vec}; ``` Many of the types available in typical Rust programs, such as `std::vec::Vec`, are not available, as there is no allocator and no heap memory in Soroban contracts by default (the SDK does provide an opt-in allocator through its `alloc` feature — see the [alloc example](../example-contracts/alloc.mdx)). The `soroban-sdk` provides a variety of types like `Vec`, `Map`, `Bytes`, `BytesN`, `Symbol`, that all utilize the Soroban environment's memory and native capabilities. Primitive values like `u128`, `i128`, `u64`, `i64`, `u32`, `i32`, and `bool` can also be used. Floats and floating point math are not supported. Contract inputs must not be references. The `#[contract]` attribute designates the `Contract` struct as the type to which contract functions are associated. This implies that the struct will have contract functions implemented for it. ```rust #[contract] pub struct Contract; ``` Contract functions are defined within an `impl` block for the struct, which is annotated with `#[contractimpl]`. It is important to note that contract functions should have names with a maximum length of 32 characters. Additionally, if a function is intended to be invoked from outside the contract, it should be marked with the `pub` visibility modifier. It is common for the first argument of a contract function to be of type `Env`, allowing access to a copy of the Soroban environment, which is typically necessary for various operations within the contract. ```rust #[contractimpl] impl Contract { pub fn hello(env: Env, to: String) -> Vec { vec![&env, String::from_str(&env, "Hello"), to] } } ``` Putting those pieces together a simple contract looks like this. ```rust title="contracts/hello_world/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl, vec, Env, String, Vec}; #[contract] pub struct Contract; #[contractimpl] impl Contract { pub fn hello(env: Env, to: String) -> Vec { vec![&env, String::from_str(&env, "Hello"), to] } } mod test; ``` Note the `mod test` line at the bottom, this will tell Rust to compile and run the test code, which we’ll take a look at next. #### Contract Unit Tests Writing tests for Soroban contracts involves writing Rust code using the test facilities and toolchain that you'd use for testing any Rust code. Given our `Contract`, a simple test will look like this. ```rust #![no_std] use soroban_sdk::{contract, contractimpl, vec, Env, String, Vec}; #[contract] pub struct Contract; #[contractimpl] impl Contract { pub fn hello(env: Env, to: String) -> Vec { vec![&env, String::from_str(&env, "Hello"), to] } } mod test; ``` ```rust #![cfg(test)] use super::*; use soroban_sdk::{vec, Env, String}; #[test] fn test() { let env = Env::default(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); let words = client.hello(&String::from_str(&env, "Dev")); assert_eq!( words, vec![ &env, String::from_str(&env, "Hello"), String::from_str(&env, "Dev"), ] ); } ``` In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run inside of. ```rust let env = Env::default(); ``` The contract is registered with the environment using the contract type. Contracts can specify a fixed contract ID as the first argument, or provide `None` and one will be generated. ```rust let contract_id = env.register(Contract, ()); ``` All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `Contract`, and the client is named `ContractClient`. ```rust let client = ContractClient::new(&env, &contract_id); let words = client.hello(&String::from_str(&env, "Dev")); ``` The values returned by functions can be asserted on: ```rust assert_eq!( words, vec![ &env, String::from_str(&env, "Hello"), String::from_str(&env, "Dev"), ] ); ``` ## Run the Tests Run `cargo test` and watch the unit test run. You should see the following output: ```sh cargo test ``` ``` running 1 test test test::test ... ok ``` Try changing the values in the test to see how it works. :::note The first time you run the tests you may see output in the terminal of cargo compiling all the dependencies before running the tests. ::: ## Build the contract To build a smart contract to deploy or run, use the `stellar contract build` command. ```sh stellar contract build ``` :::tip If you get an error like `can't find crate for 'core'`, it means you didn't install the wasm32 target during the [setup step](./setup.mdx#install-the-target). You can do so by running `rustup target add wasm32v1-none` (reminder, this requires Rust `v1.84.0` or higher). ::: This is a small wrapper around `cargo build` that sets the target to `wasm32v1-none` and the profile to `release` (it also [optimizes](#optimizing-builds) the resulting `.wasm` file by default). You can think of it as a shortcut for the following command: ```sh cargo build --target wasm32v1-none --release ``` A `.wasm` file will be outputted in the `target` directory, at `target/wasm32v1-none/release/hello_world.wasm`. The `.wasm` file is the built contract. The `.wasm` file contains the logic of the contract, as well as the contract's [specification / interface types](../../../learn/fundamentals/contract-development/types/fully-typed-contracts.mdx), which can be imported into other contracts who wish to call it. This is the only artifact needed to deploy the contract, share the interface with others, or integration test against the contract. ## Optimizing Builds The `stellar contract build` command optimizes the generated `.wasm` by default, so there's no extra step needed to minimize the size of your contract. If you'd like to skip that optimization (during quick, iterative development, for example), you can disable it: ```sh stellar contract build --optimize=false ``` :::tip Optimized contract builds are only necessary when deploying to a network with fees or when analyzing and profiling a contract to get it as small as possible. If you're just starting out writing a contract, un-optimized builds will work just fine. ::: :::note Older versions of the Stellar CLI provided a standalone `stellar contract optimize` command. That command is now deprecated in favor of the (default) `--optimize` flag on `stellar contract build`. ::: ## Summary In this section, we wrote a simple contract that can be deployed to a Soroban network. Next we'll learn to deploy the HelloWorld contract to Stellar's Testnet network and interact with it over RPC using the CLI. [rust unit tests]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html [`stellar-cli`]: setup.mdx#install-the-stellar-cli --- ## Set Up and Configure Your Environment for Writing Smart Contracts # Setup Stellar smart contracts are small programs written in the [Rust] programming language. To build and develop contracts you need the following prerequisites: - A [Rust] toolchain - An editor that supports Rust - [Stellar CLI] ## Install Rust If you use macOS, Linux, or another Unix-like OS, the simplest method to install a Rust toolchain is to install `rustup`. Install `rustup` with the following command. ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` Then restart the terminal. On Windows, download and run [rustup-init.exe](https://static.rust-lang.org/rustup/dist/i686-pc-windows-gnu/rustup-init.exe). You can continue with the default settings by pressing Enter. :::tip The Stellar CLI uses emojis in its output. To properly render them on Windows, it is recommended to use the [Windows Terminal](https://learn.microsoft.com/en-us/windows/terminal). See [how to install Windows Terminal](https://learn.microsoft.com/en-us/windows/terminal/install) on Microsoft Learn. If the CLI is used in the built in Windows Command Prompt or Windows PowerShell the CLI will function as expected but the emojis will appear as question marks. ::: If you're already using [WSL](https://learn.microsoft.com/en-us/windows/wsl/install), you can also follow the instructions for Linux. For other methods of installing [Rust], see: https://www.rust-lang.org/tools/install Stellar smart contracts require **Rust toolchain `v1.84.0` or higher**, as the `wasm32v1-none` target is only available in recent versions. To check your version: ```sh rustc --version ``` If you need to update: ```sh rustup update stable ``` ## Install the target You'll need a "target" for which your smart contract will be compiled. Install the `wasm32v1-none` target (again, this requires Rust `v1.84.0` or higher). ```sh rustup target add wasm32v1-none ``` :::note When you install Rust, the WebAssembly target is installed per-toolchain. If you update your Rust version, you'll need to reinstall the `wasm32v1-none` target for the new toolchain. ::: You can learn more about the finer points of what this target brings to the table, in our page all about the [Stellar Rust dialect](../../../learn/fundamentals/contract-development/rust-dialect.mdx#limited-webassembly-features). This page describes the subset of Rust functionality that is available to you within Stellar smart contract environment. ## Configure an editor Many editors have support for Rust. Visit the following link to find out how to configure your editor: https://www.rust-lang.org/tools Here are the tools to you need to configure your editor: 1. [Visual Studio Code](https://code.visualstudio.com) as code editor (or another code editor that supports Rust) 2. [Rust Analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) for Rust language support 3. [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) for step-through-debugging ## Install the Stellar CLI The [Stellar CLI](https://github.com/stellar/stellar-cli) can execute smart contracts on futurenet, testnet, mainnet, as well as in a local sandbox. :::info The latest stable release is [v{latestVersion}](https://github.com/stellar/stellar-cli/releases/latest). ::: ### Install There are a few ways to install the [latest release](https://github.com/stellar/stellar-cli/releases) of Stellar CLI. Install using script (macOS, Linux): ```sh curl -fsSL https://github.com/stellar/stellar-cli/raw/main/install.sh | sh ``` Install with Homebrew (macOS, Linux): ```sh brew install stellar-cli ``` Install with cargo from source: Install using script (macOS, Linux): ```sh curl -fsSL https://github.com/stellar/stellar-cli/raw/main/install.sh | sh ``` Install with Homebrew (macOS, Linux): ```sh brew install stellar-cli ``` Install with cargo from source: :::note Installing from source requires a C build system. To install a C build system on Debian/Ubuntu, use: ``` sudo apt update && sudo apt install -y build-essential ``` ::: Using the installer: 1. Download the installer from the latest release. 2. Go to your Downloads folder, double click the installer and follow the wizard instructions. 3. Restart your terminal to use the `stellar` command. Using [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget): Install with cargo from source: :::info Report issues and share feedback about the Stellar CLI [here](https://github.com/stellar/stellar-cli/issues/new/choose). ::: ### Documentation The auto-generated comprehensive reference documentation is available [here](../../../tools/cli/stellar-cli.mdx). ### Autocompletion You can use `stellar completion` to generate shell completion for different shells. You should absolutely try it out. It will feel like a super power! To enable autocomplete on the current shell session: ```sh source <(stellar completion --shell bash) ``` To enable autocomplete permanently, run the following command, then restart your terminal: ```sh echo "source <(stellar completion --shell bash)" >> ~/.bashrc ``` To enable autocomplete on the current shell session: ```sh source <(stellar completion --shell zsh) ``` To enable autocomplete permanently, run the following commands, then restart your terminal: ```sh echo "source <(stellar completion --shell zsh)" >> ~/.zshrc ``` To enable autocomplete on the current shell session: ```sh stellar completion --shell fish | source ``` To enable autocomplete permanently, run the following command, then restart your terminal: ```sh echo "stellar completion --shell fish | source" >> ~/.config/fish/config.fish ``` To enable autocomplete on the current shell session: ```powershell stellar completion --shell powershell | Out-String | Invoke-Expression ``` To enable autocomplete permanently, run the following commands, then restart your terminal: ```powershell New-Item -ItemType Directory -Path $(Split-Path $PROFILE) -Force if (-Not (Test-Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE | Out-Null } Add-Content $PROFILE 'Set-PSReadlineKeyHandler -Key Tab -Function MenuComplete' Add-Content $PROFILE 'stellar completion --shell powershell | Out-String | Invoke-Expression' ``` :::tip If you get an error like `cannot be loaded because running scripts is disabled on this system`, you may need to change your [Execution Policy](https://go.microsoft.com/fwlink/?LinkID=135170) with `Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser`. **Before running this command, be sure you understand the implications of doing so.** ::: To enable autocomplete on the current shell session: ```sh source (stellar completion --shell elvish) ``` To enable autocomplete permanently, run the following commands, then restart your terminal: ```sh echo "source (stellar completion --shell elvish)" >> ~/.elvish/rc.elv ``` [rust]: https://www.rust-lang.org/ [stellar cli]: #install-the-stellar-cli --- ## Write a Smart Contract to Store & Retrieve Data with Increment Example # 3. Storing Data Now that we've built a basic Hello World example contract, we'll write a simple contract that stores and retrieves data. This will help you see the basics of Soroban's storage system. This is going to follow along with the [increment example](https://github.com/stellar/soroban-examples/tree/v22.0.1/increment), which has a single function that increments an internal counter and returns the value. If you want to see a working example, [try it in Devcontainers](https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web). This tutorial assumes that you've already completed the previous steps in Getting Started: [Setup](./setup.mdx), [Hello World](./hello-world.mdx), and [Deploy to Testnet](./deploy-to-testnet.mdx). ## Adding the increment contract In addition to creating a new project, the `stellar contract init` command also allows us to initialize a new contract workspace within an existing project. In this example, we're going to initialize a new contract and use the `--name` flag to specify the name of our new contract, `increment`. This command will not overwrite existing files unless we explicitly pass in the `--overwrite` flag. From within our `soroban-hello-world` directory, run: ```sh stellar contract init . --name increment ``` This creates a new `contracts/increment` directory with placeholder code in `src/lib.rs` and `src/test.rs`, which we'll replace with our new increment contract and corresponding tests. ``` └── contracts ├── increment ├── Cargo.toml ├── Makefile └── src ├── lib.rs └── test.rs ``` We will go through the contract code in more detail below, but for now, replace the placeholder code in `contracts/increment/src/lib.rs` with the following. ```rust #![no_std] use soroban_sdk::{contract, contractimpl, log, symbol_short, Env, Symbol}; const COUNTER: Symbol = symbol_short!("COUNTER"); #[contract] pub struct IncrementContract; #[contractimpl] impl IncrementContract { /// Increment increments an internal counter, and returns the value. pub fn increment(env: Env) -> u32 { let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); log!(&env, "count: {}", count); count += 1; env.storage().instance().set(&COUNTER, &count); env.storage().instance().extend_ttl(50, 100); count } } mod test; ``` ### Imports This contract begins similarly to our Hello World contract, with an annotation to exclude the Rust standard library, and imports of the types and macros we need from the `soroban-sdk` crate. ```rust title="contracts/increment/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl, log, symbol_short, Env, Symbol}; ``` ### Contract Data Keys ```rust const COUNTER: Symbol = symbol_short!("COUNTER"); ``` Contract data is associated with a key, which can be used at a later time to look up the value. `Symbol` is a short (up to 32 characters long) string type with limited character space (only `a-zA-Z0-9_` characters are allowed). Identifiers like contract function names and contract data keys are represented by `Symbol`s. The `symbol_short!()` macro is a convenient way to pre-compute short symbols up to 9 characters in length at compile time using `Symbol::short`. It generates a compile-time constant that adheres to the valid character set of letters (a-zA-Z), numbers (0-9), and underscores (\_). If a symbol exceeds the 9-character limit, `Symbol::new` should be utilized for creating symbols at runtime. ### Contract Data Access ```rust let mut count: u32 = env .storage() .instance() .get(&COUNTER) .unwrap_or(0); // If no value set, assume 0. ``` The `Env.storage()` function is used to access and update contract data. The executing contract is the only contract that can query or modify contract data that it has stored. The data stored is viewable on ledger anywhere the ledger is viewable, but contracts executing within the Soroban environment are restricted to their own data. The `get()` function gets the current value associated with the counter key. If no value is currently stored, the value given to `unwrap_or(...)` is returned instead. Values stored as contract data and retrieved are transmitted from [the environment](../../../learn/fundamentals/contract-development/environment-concepts.mdx) and expanded into the type specified. In this case a `u32`. If the value can be expanded, the type returned will be a `u32`. Otherwise, if a developer cast it to be some other type, a panic would occur at the unwrap. ```rust env.storage() .instance() .set(&COUNTER, &count); ``` The `set()` function stores the new count value against the key, replacing the existing value. ### Managing Contract Data TTLs with `extend_ttl()` ```rust env.storage().instance().extend_ttl(100, 100); ``` All contract data has a Time To Live (TTL), measured in ledgers, that must be periodically extended. If an entry's TTL is not periodically extended, the entry will eventually become "archived." You can learn more about this in the [State Archival](../../../learn/fundamentals/contract-development/storage/state-archival.mdx) document. For now, it's worth knowing that there are three kinds of storage: `Persistent`, `Temporary`, and `Instance`. This contract only uses `Instance` storage: `env.storage().instance()`. Every time the counter is incremented, this storage's TTL gets extended by 100 [ledgers](../../../learn/fundamentals/stellar-data-structures/ledgers.mdx), or about 500 seconds. ### Build the contract From inside `soroban-hello-world`, run: ```sh stellar contract build ``` Check that it built: ```bash ls target/wasm32v1-none/release/*.wasm ``` You should see both `hello_world.wasm` and `increment.wasm`. ## Tests Replace the placeholder code in `contracts/increment/src/test.rs` with the following increment test code. ```rust title="contracts/increment/src/test.rs" #![cfg(test)] use crate::{IncrementContract, IncrementContractClient}; use soroban_sdk::Env; #[test] fn test() { let env = Env::default(); let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.increment(), 1); assert_eq!(client.increment(), 2); assert_eq!(client.increment(), 3); } ``` This uses the same concepts described in the Hello World example. Make sure it passes: ```sh cargo test ``` You'll see that this runs tests for the whole workspace; both the Hello World contract and the new Increment contract. If you want to see the output of the `log!` call, run the tests with `--nocapture`: ``` cargo test -- --nocapture ``` You should see the the diagnostic log events with the count data in the output: ``` running 1 test [Diagnostic Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM, topics:[log], data:["count: {}", 1] [Diagnostic Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM, topics:[log], data:["count: {}", 2] [Diagnostic Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM, topics:[log], data:["count: {}", 3] test test::test ... ok ``` ## Take it further Can you figure out how to add `get_current_value` function to the contract? What about `decrement` or `reset` functions? ## Summary In this section, we added a new contract to this project, that made use of Soroban's storage capabilities to store and retrieve data. We also learned about the different kinds of storage and how to manage their TTLs. Next we'll learn a bit more about deploying contracts to Soroban's Testnet network and interact with our incrementor contract using the CLI. --- ## An Overview of Smart Contracts on Stellar, Including the Rust SDK and FAQs # Overview Soroban is the smart contracts platform on the Stellar network. These contracts are small programs written in the [Rust language](https://www.rust-lang.org) and compiled as [WebAssembly](https://webassembly.org) (Wasm) for deployment. :::note For a comprehensive introduction to Stellar smart contracts, view the [Smart Contract Learn Section](../../learn/fundamentals/contract-development/overview.mdx). ::: Write your first smart contract on Stellar using the [Getting Started Guide](./getting-started/setup.mdx). ## Developing Smart Contracts Stellar smart contracts have several characteristics (such as resource limits, security considerations, and more) that force contracts to use only a narrow subset of the full Rust language and must use specialized libraries for most tasks. Learn more in the [Contract Rust Dialect section](../../learn/fundamentals/contract-development/rust-dialect.mdx). In particular, the Rust standard library and most third-party libraries (called [crates](../../learn/migrate/evm/solidity-and-rust-advanced-concepts#crates)) will not be available for direct off-the-shelf use in contracts due to the abovementioned constraints. Some crates can be adapted for use in contracts, and others may be incorporated into the host environment as host objects or functions. :::note Other languages may be supported in the future, but at this time, only Rust is supported. ::: ## Soroban Rust SDK Contracts are developed using a software development kit (SDK). The [Soroban Rust SDK](../../tools/sdks/contract-sdks.mdx#soroban-rust-sdk) consists of a Rust crate and a command-line (CLI) tool. The SDK crate acts as a substitute for the Rust standard library — providing data structures and utility functions for contracts — as well as providing access to smart-contract-specific functionality from the contract environment, like cryptographic hashing and signature verification, access to on-chain persistent storage, and location and invocation of secondary contracts via stable identifiers. The Soroban SDK CLI tool provides a developer-focused front-end for: - Compiling - Testing - Inspecting - Versioning - Deploying It also includes a complete implementation of the contract host environment that is identical to the one that runs on-chain, called [local testing mode](../../learn/fundamentals/contract-development/errors-and-debugging/debugging.mdx#local-testing-mode). With this capability, contracts can be run locally on a developer's workstation and can be tested and debugged directly with a local debugger within a standard IDE, as well as a native test harness for fast-feedback unit testing and high-speed fuzzing or property testing. ## Host environment The host environment is a set of Rust crates compiled into the SDK CLI tool and stellar-core. It comprises a set of host objects and functions, an interface to on-chain storage and contract invocation, a resource-accounting and fee-charging system, and a Wasm interpreter. Most contract developers will not frequently need to interact with the host environment directly — SDK functions wrap most of its facilities and provide richer and more ergonomic types and functions — but it is helpful to understand its structure to understand the conceptual model the SDK is presenting. Some parts of the host environment will likely be visible when testing or debugging contracts compiled natively on a local workstation. Learn more in the [Environment Concepts section](../../learn/fundamentals/contract-development/environment-concepts.mdx). ## Stellar smart contract FAQs {/* prettier-ignore-start */}
**What is Soroban to Stellar? Is it a new blockchain?** Soroban is not a new blockchain. Soroban is a smart contract platform integrated into the existing Stellar blockchain. It is an additive feature that lives alongside and doesn't replace the existing set of Stellar operations.
**How do I invoke a Soroban contract on Stellar?** Invoke a Soroban contract by submitting a transaction that contains the new operation: [`InvokeHostFunctionOp`](../../learn/fundamentals/transactions/list-of-operations.mdx#invoke-host-function).
**Can Soroban contracts use Stellar accounts for authentication?** Yes. Stellar accounts are shared with Soroban. Smart contacts have access to Stellar account signer configuration and know the source account that directly invoked them in a transaction. Check out the [Authorization section](../../learn/fundamentals/contract-development/authorization.mdx) for more information.
**Can Soroban contracts interact with Stellar assets?** Yes. Soroban contains a built-in [Stellar Asset Contract](../../tokens/stellar-asset-contract.mdx) that can interact with classic trustlines.
**Do issuers of Stellar assets maintain authorization over an asset sent to a non-account identifier in Soroban (`AUTH_REQUIRED`, `AUTH_REVOCABLE`, `AUTH_CLAWBACK`)?** Yes. Issuers retain the same level of control on Soroban as they have regularly. This functionality is accessible through a set of admin functions (clawback, set_auth) on the built-in Stellar Asset Contract.
**Can Soroban contracts interact with any other Stellar operations?** No. Aside from the interactions with accounts and assets mentioned above. This means that Soroban contracts cannot interact with the SDEX, claimable balances, or sponsorships.
**Does the Stellar base reserve apply to Soroban contracts?** No. Soroban has a different [fee structure](../../learn/fundamentals/fees-resource-limits-metering.mdx), and ledger entries that are allocated by Soroban contracts do not add to an account's required minimal balance.
**Need help finding what you're looking for?** Ask in the Developer channels in the [Stellar Developer Discord](https://discord.gg/stellardev).
**Should I issue my token as a Stellar asset or a Soroban contract token?** To the greatest extent possible, we recommend issuing tokens as Stellar assets. These tokens will benefit from being interoperable with the existing tools available in the Stellar ecosystem and are more performant because the Stellar Asset Contract is built into the host. Read more in the [Tokens Overview](../../tokens/README.mdx).
{/* prettier-ignore-end */} --- ## STOP WHAT YOU'RE DOING If you're here to make some fixes to Stellar's API documentation (first, thank you!), you probably don't want to be in this location. Most of the API documentation located here has been generated by the `.yml` files contained in the `/openapi` directory. Unless you're working on something that's _outside_ the `/api/resources` directory, you'll probably want to check in `/openapi` instead of here. --- ## Data Overview There are several products to choose from when interacting with the Stellar Network, and each one is optimized for different access patterns, so it's important to choose the right tool based on your specific use-case. These tools allow users to query network data, submit transactions, and interact with smart contracts without needing to understand the low-level details of Stellar Core. Use the summaries below to determine which types of data tools are right for your use case. ## [Analytics](./analytics/README.mdx) Explore and analyze Stellar network data to uncover trends, track activity, and generate insights for decision-making and reporting. **Why Use It:** - You want historical insights and visualizations for network activity and ecosystem health - You don't need to submit transactions to the network - You don't need real-time data ## [APIs](./apis/README.mdx) Connect to the Stellar network in real-time, enabling seamless data retrieval, transaction submission, and application integration. **Why Use It:** - You want to develop applications that interact with Stellar - You want real-time data access and transaction submission and processing ## [Indexers](./indexers/README.mdx) Organize and optimize Stellar blockchain data for efficient querying, deeper analysis, and enhanced accessibility. **Why Use It:** - You want structured and scalable access to historical and real-time blockchain data - You want easy access to processed and transformed raw ledger data for querying ## [Oracles](./oracles/README.mdx) Integrate off-chain data with the Stellar network, enabling smart contracts and decentralized applications to interact with external information sources. **Why Use It:** - You want to use off-chain data in conjunction with Stellar smart contracts for expanded functionality and automation --- ## Analytics Overview Learn about analytics and analytics providers on the Stellar network ## [Hubble](./hubble/README.mdx) Hubble is an open-source, read-only BigQuery data warehouse maintained by SDF that provides a complete historical record of the Stellar network, ideal for large-scale analysis of transactions, fees, and network trends. It is not suitable for real-time data needs or simple lookups, and its documentation focuses on exploratory analysis, connection methods, and query optimization. By offering a structured and query-ready dataset, Hubble enables users to explore Stellar network data without managing raw data pipelines. Within Hubble, the stellar-etl product powers the extraction and transformation of Stellar network data into BigQuery. This open-source pipeline allows developers to extend or customize data ingestion workflows based on their analytical needs. Stellar-etl provides flexibility in how data is processed, enabling users to extract specific ledger entries, operations, or event logs based on their requirements. ## [Data Analytics Providers](./analytics-providers/analytics-providers.mdx) Data Analytics Providers provide a complete historical record of Pubnet Stellar network data for analysis and insights. These platforms enable users to explore network trends but are not designed for real-time data access or transaction execution. The key difference is that these ecosystem platforms operate independently of SDF and provide metrics and dashboards about the Stellar network. --- ## Data Analytics Providers The following is a list of data analytics platforms that make a complete historical record of Pubnet Stellar network data available. | Platform | Description | Home page and Signup | Pricing | Data Access | Dashboard access | | --- | --- | --- | --- | --- | --- | | Hubble | SDF provides a publicly available raw dataset with a complete historical record of Stellar network data, updated every 15 minutes. | [Hubble](https://console.cloud.google.com/bigquery?project=crypto-stellar&ws=!1m4!1m3!3m2!1scrypto-stellar!2scrypto_stellar) [Signup](https://cloud.google.com/?hl=en) | End users incur the cost to execute queries. Visit the [BigQuery Pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models) page to learn more. | Please see the [Analyst Guide](./hubble/analyst-guide) for more information. | N/A | | Dune | Raw dataset for the Stellar network available in the Dune portal. Updated every hour. You can compare different chains on the platform. | [Dune](https://dune.com) [Signup](https://dune.com/auth/register) | Free for simpler queries. Need to pay for complex queries. | To access datasets, click [here](https://dune.com/queries?category=canonical&namespace=stellar). | Explore community dashboards, network overview, and datasets [here](https://dune.com/blockchains/stellar). | | Artemis | Aggregated dataset for the Stellar network available in BigQuery as well as the Artemis portal. Provides dashboards to compare Stellar chain activity with ecosystem chains. | [Artemis](https://www.artemis.xyz) [Signup](https://app.artemisanalytics.com/artemis-terminal-auth?flow=SIGN_UP) | Free dashboards. Need to pay for accessing the raw dataset. | For accessing Artemis-transformed Stellar dataset, upgrade to the enterprise plan and then star [this dataset](https://console.cloud.google.com/bigquery?ws=!1m4!1m3!3m2!1sartemis-bigquery-share!2sshared_us). | Explore [chain compare](https://app.artemisanalytics.com/chains?selectedChains=stellar) and [application activity](https://app.artemis.xyz/application-activity/stellar) dashboards. | | Nansen | Aggregated dataset for the Stellar network available in the Nansen portal. Provides dashboards to compare Stellar chain activity with ecosystem chains. | [Nansen](https://app.nansen.ai) [Signup](https://app.nansen.ai/auth/signup) | Free dashboards. Need to pay for accessing the raw dataset. | For accessing Nansen-transformed Stellar dataset, upgrade to the Pioneer plan and then star [this dataset](https://console.cloud.google.com/bigquery?project=nansen-query). | Explore [chain activity, entity, and ecosystem](https://app.nansen.ai/macro/blockchains?chain=stellar&tab=overview). | | Ortege | Ortege hosts data services to make blockchain data more accessible through real time APIs and a managed [Lakehouse](https://docs.ortege.ai/en/ortege-products/ortege-lakehouse) for analytics exploration. The lakehouse follows a standardize schema across blockchains, making it easy to plug-and-play across L1s. | [Ortege](https://www.ortege.ai) [Signup](https://app.ortege.ai/register/form) | Free for simpler queries, for larger workloads, user must purchase credits in the warehouse | Here is a [link](https://docs.ortege.ai/en/ortege-products/ortege-lakehouse/data-catalog/in-house-datasets) to their data catalog for data access. | Explore curated dashboards [here](https://app.ortege.ai/dashboard/list), or build your own | | Flipside | Raw dataset access to run queries, create dashboards and gain actionable intelligence on various crypto ecosystems. Flipside sponsors analytics bounties to encourage open source sharing of insights as well as developer quests. | [Flipside](https://flipsidecrypto.xyz/home) [Signup](https://flipsidecrypto.xyz/home/sign-up) | Entire data access (raw, dashboards) is free. | To access the dataset, sign up on Flipside and find the "Stellar" database in [Flipside studio](https://flipsidecrypto.xyz/studio). | Explore [community dashboards](https://flipsidecrypto.xyz/insights/dashboards?search=stellar). | | DefiLlama | TVL aggregator for DeFi (Decentralized Finance). | [DefiLlama](https://defillama.com) | Free(open source) access to TVL dashboard | For data access, checkout [API Docs](https://defillama.com/docs/api) | Explore the [TVL Dashboard](https://defillama.com/chain/Stellar). | | Messari | Messari serves as a central hub for research and data related to the cryptoeconomy. With customizable and user-friendly features. | [Messari](https://messari.io) | Free access to dashboards and data ([subscription](https://messari.io/account/plan) for more advanced access) | For data access, checkout [API Docs](https://docs.messari.io/reference/introduction) | Explore the [Stellar Portal](https://stellar.messari.io). | | rwa.xyz | Aggregated information about tokenized real-world assets. | [rwa.xyz](https://www.rwa.xyz) | Free access to dashboards and general rwa information | For data access, checkout [Data & API Platform](https://app.rwa.xyz/platform-overview) | Explore the [Stellar Dashboard](https://app.rwa.xyz/networks/stellar). | | Token Terminal | Token Terminal is a full stack onchain data platform focused on standardizing financial and alternative data for the most widely used blockchains and decentralized applications. | [Token Terminal](https://tokenterminal.com) | Free access to dashboards. Paid [API access](https://tokenterminal.com/explorer/account/api) | For data access, checkout [API Docs](https://tokenterminal.com/docs/api-reference/introduction) | Explore the [Stellar Dashboard](https://tokenterminal.com/explorer/projects/stellar). | | SonarX | SonarX provides a one-stop-shop for indexed Stellar data. | [SonarX](https://www.sonarx.com) | Paid access to raw and curated datasets. | There are two products for data access, [Real-Time Data](https://www.sonarx.com/product/realtime) and [Historical Streaming](https://www.sonarx.com/product/historical) | N/A | | Range | The blockchain security and intelligence platform; helping the best teams build and use DeFi protocols, blockchains, rollups, and cross-chain bridges with peace of mind. | [Range](https://www.range.org) | Free access to analytics. Paid access to APIs and Security Products | Stellar has their own [Home Page](https://stellar.range.org) and we are featured on their [Stable Coin](https://stable.range.org) explorer as well. | Explore the [Stellar Dashboard](https://stellar.range.org). | {/* Allium login not working as of the writing of this doc */} {/* | Allium | Raw dataset access to the Stellar network. | [Allium](https://www.allium.so) | Free data access and queries | For data access, checkout [Allium Docs](https://docs.allium.so/) | N/A | */} ## Run Your Own Hubble If you are interested in running your own Hubble, please checkout the [Developer Guide](../hubble/developer-guide/README.mdx). --- ## Hubble ## What is Hubble? Hubble is an open-source, publicly available dataset that provides a complete historical record of the Stellar network. It ingests and presents the data produced by the Stellar network in a format that is easier to consume than the performance-oriented data representations used by Stellar Core. The dataset is hosted on BigQuery–meaning it is suitable for large, analytic workloads, historical data retrieval and complex data aggregation. **Hubble should not be used for real-time data retrieval and cannot submit transactions to the network.** For real time use cases, we recommend [running an API server](../../apis/rpc/admin-guide/README.mdx). This guide describes when to use Hubble and how to connect. To view the underlying data structures, queries and examples, use the [Viewing Metadata](./analyst-guide/viewing-metadata.mdx) and [Optimizing Queries](./analyst-guide/optimizing-queries.mdx) tutorials. ## Why Use Hubble? Some questions are hard to answer with the RPC API as its API is quite minimal. This is because its infrastructure is optimized for quick reads and writes so that it can process online transactions. This is where Hubble comes in. It is optimized to execute complex queries and scan large amounts of data. Hubble can store orders of magnitude more data than RPC and will not run into storage constraints. Queries that require pagination in RPC or timeout can be returned in a single query. Hubble empowers users to explore, analyze, and derive meaningful conclusions from the data without the burden of maintaining a database. Users should be aware of the following limitations: - Hubble is read-only; it cannot interact with the Stellar Network. - The database is updated in intraday batches. There is no guarantee for same-day data availability. - The SDF hosts a public instance of Hubble, and end users incur the cost to execute queries. Visit the [BigQuery Pricing Page](https://cloud.google.com/bigquery/pricing#analysis_pricing_models) to learn more. ## Why We Chose BigQuery BigQuery is Google Cloud’s data warehouse that comes with some key features that fulfill Stellar’s analytic needs. First, BigQuery allows anyone to make a dataset publicly available. This means that the SDF can contribute open source repositories to build and maintain a data warehouse and also host a public instance. BigQuery also separates storage from compute, which makes it sustainable to host a public instance. The maintainer only has to pay the cost of storage without incurring the cost of the analytics running on the dataset. Most importantly, BigQuery is the de facto platform for blockchain datasets. By selecting BigQuery, Stellar Network data is located with other blockchain data, which allows for cross-chain analytics. --- ## Analyst Guide All you need to know to use Hubble data for analysis. --- ## Connecting BigQuery offers multiple connection methods to Hubble. This guide details three common methods: - [BigQuery UI](#bigquery-ui) - analysts that need to perform ad hoc analysis using SQL - [BigQuery SDK](#bigquery-sdk) - developers that need to integrate data into applications - [Looker Studio](#looker-studio) - business people that need to visualize data ## Prerequisites To access Hubble, you will need a Google Cloud Project with billing and the BigQuery API enabled. For more information, please follow the instructions provided by [Google Cloud](https://cloud.google.com/bigquery/docs/quickstarts/query-public-dataset-console). Google does provide a BigQuery Sandbox for free that allows users to explore datasets in a limited capacity. ## BigQuery UI 1. From a browser, open the [crypto-stellar.crypto_stellar](http://console.cloud.google.com/bigquery?ws=!1m4!1m3!3m2!1scrypto-stellar!2scrypto_stellar) dataset. 2. This will open the public dataset `crypto_stellar`, where you can browse its contents in the **Explorer** pane. 3. Click the **star** icon in the Explorer pane. This will favorite the dataset for you. More detailed information about starring resources can be found [here](https://cloud.google.com/bigquery/docs/bigquery-web-ui#star_resources). :::note Hubble cannot be found from the Explorer pane! You cannot search for the dataset. To view the dataset, you **must** use the [dataset link](https://console.cloud.google.com/bigquery?ws=!1m4!1m3!3m2!1scrypto-stellar!2scrypto_stellar). ::: Copy and paste the following example query in the Editor: ```sql select account_id, balance from `crypto-stellar.crypto_stellar.accounts_current` order by balance desc; ``` This query will return the XLM balances for all Stellar wallet addresses, ordered from largest to smallest amounts. ## BigQuery SDK There are multiple [BigQuery API Client Libraries](https://cloud.google.com/bigquery/docs/reference/libraries) available. The following example uses Python to access the Hubble dataset. Use [this guide](https://cloud.google.com/python/docs/setup) for help setting up a python development environment. Install the client library locally, and configure your environment to use your Google Cloud Project: ```bash # verify python version python3 --version # if you do not have pip, install it python -m pip install --upgrade pip # install bigquery client library pip install --upgrade google-cloud-bigquery gcloud config set project PROJECT_ID ``` Use the Python Interpreter to run the example below to list the tables available in Hubble: ```python from google.cloud import bigquery # Construct a BigQuery client object. client = bigquery.Client() dataset_id = 'crypto-stellar.crypto_stellar' # Make an API request tables = client.list_tables(dataset_id) # List the tables found in Hubble print(f'Tables contained in {dataset_id}':) for table in tables: print(f'{table.project}.{table.dataset_id}.{table.table_id}') ``` Run the example below to run a query and print the results: ```python from google.cloud import bigquery # Construct a BigQuery client object. client = bigquery.Client() query = """ SELECT account_id, balance, FROM `crypto-stellar.crypto_stellar.accounts_current` ORDER BY balance DESC LIMIT 10; """ # Make an API request query_job = client.query(query) print("The query data:") for row in query_job: # Row values can be accessed by field name or index. print(f'account_id={row[0]}, balance={row["balance"]}') ``` There are various ways to extract and load data using BigQuery. See the [BigQuery Client Documentation](https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client) for more information. ## Looker Studio [Looker Studio](https://cloud.google.com/looker-studio) is a business intelligence tool that can be used to connect to and visualize data from the Hubble dataset. To connect Hubble as a data source: 1. Open [Looker Studio](https://lookerstudio.google.com) 2. Click on **Create** > **Data Source** 3. Search for the BigQuery connector 4. _(Optional)_ Change the name of the data source at the top of the webpage 5. Click _Shared Projects_ > Select your Google Cloud Project 6. Enter `crypto-stellar` as the Shared Project name 7. Click on the Dataset `crypto_stellar` 8. Select the desired table to connect 9. Click `CONNECT` on the top right of the webpage. And you're connected! General information about Looker Studio can be found [here](https://support.google.com/looker-studio). General information about connecting data sources can be found [here](https://support.google.com/looker-studio/topic/6370331?hl=en&ref_topic=7441382&sjid=14945902445646860578-NA). --- ## Creating Visualizations This page will show how you can create visualizations with Hubble and [Google Looker Studio](https://cloud.google.com/looker-studio?hl=en). By the end of the tutorial you will have created two graphs that will help visualize temporary VS persistent contract data entry distribution and expiration. As you can see, persistent contract data entries account for roughly 25% of all contract data entries with the rest being temporary. There are also a lot more expired temporary contract data entries. This is expected because temporay entries cannot be bumped whereas persistent entries can be bumped/restored. For more information, you can read about [State Archival and the Contract Data Lifecycle](../../../../learn/fundamentals/contract-development/storage/state-archival.mdx#contract-data-type-descriptions). ## Prerequisites 1. Make sure you have connected to Hubble by following the instructions in the [Connecting](../developer-guide/connecting-to-bigquery/README.mdx) page 2. You have access to [Google Looker Studio](https://cloud.google.com/looker-studio?hl=en) 3. You have read and understand the general [Best Practices](./optimizing-queries.mdx#best-practices) for querying BigQuery data ## Create a Report in Looker Studio ### Attach Data Sources to Looker Studio 1. Select `Create --> Data source` 2. Find and select the `BigQuery Google Connector` 3. Find the desired tables that you want to connect. For this example you will want to add a data source for: 1. `crypto-stellar.crypto_stellar.contract_data` ### Create a New Report (Dashboard) 1. Select `Create --> Report` 2. Add your data sources from above 3. Insert a `Pie chart` 4. Choose `contract_data` as the `Data source` 5. Choose `closed_at` as the `Date Range Dimension` and `contract_durability` as the `Dimension` 6. You should now have a pie chart showing the percentage of Temporary VS Persistent Contract Data Durability ### Use Custom SQL to Create a Chart 1. In your report, click `Add Data` which will be near the bottom right of your window 2. Select `BigQuery` and choose `CUSTOM QUERY` and select your desired `Billing Project` where the query will be charged 3. Add the following query and click `Add` ```sql -- Find the latest ledger sequence within Hubble. -- This may be slightly behind the actual Stellar latest ledger -- because Hubble is scheduled to run and insert data at 10 minute intervals with latest_ledger_in_hubble as ( select max(sequence) as latest_ledger_sequence from `crypto-stellar.crypto_stellar.history_ledgers` ), -- Find all the ttl that have expired expired_ttl as ( select key_hash , live_until_ledger_seq -- Saving the date to aggregate on at the final step of the query , date(closed_at) as ledger_date from `crypto-stellar.crypto_stellar_dbt.ttl_current` where true -- Filter for expired entries only with the use of latest_ledger_sequence and live_until_ledger_seq < (select latest_ledger_sequence from latest_ledger_in_hubble) ) -- Aggregate based on the month and contract durability type select date_trunc(et.ledger_date, month) as month_agg , cd.contract_durability , count(1) as expired_entry_count from expired_ttl as et join `crypto-stellar.crypto_stellar_dbt.contract_data_current` as cd on et.key_hash = cd.ledger_key_hash where true -- Optionally filter for a specific date/date range and et.ledger_date between '2024-02-01' and '2024-10-31' group by 1,2 order by 1 desc, 2 ``` 4. Insert `Column chart` 5. Select the following: - `BigQuery Custom SQL` as your `Data source` - `month_agg` as the `Dimension` - `contract_durability` as the `Breakdown Dimension` - `expired_entry_count` as the `Metric` 6. You should now have a column chart (bar chart) showing the expired Soroban contract entries ### Applying a Global Filter 1. Click `+Add quick filter` to apply a filter throughout the whole report 2. Select `contract_durability` to filter by `contract_durability` values 3. Select only `ContractDataDurabilityPersistent` and click `Apply` 4. Your charts should now be filtered and show only `ContractDataDurabilityPersistent` data --- ## History VS State Tables This page describes the differences between History and State tables within Hubble. ## What are state tables? These track ledger entry changes, meaning they store how each ledger entry evolved over time instead of just the final state of the ledger entry. This means that state tables are a full history log of ledger entry states that are changed based on actions (operations) from the Stellar network. - Each row represents a single change to a ledger entry - You will see multiple rows of the same ledger entry hash if it has been modified multiple times in different ledger sequences - This is not a snapshot/SCD type 2 table. There is no valid to/from dates in these tables Note that this is a different behavior when compared to the [RPC's `getLedgerEntries` endpoint](../../../apis/rpc/api-reference/methods). RPC will return the current value of a given ledger entry (a single result) whereas the Hubble state tables will return all the changes over time for a given ledger entry (multiple results). To get similar behavior as RPC you can filter and return the lastest entry from the Hubble state tables or you can use the `_current` version of the state tables listed in the `crypto-stellar.crypto_stellar_dbt` dataset. ### Example contents of a state table Let's say we only want the ledger entries for the balance of `DUMMYACCOUNT`. The `account` state table would return the following: | account_id | ledger_sequence | balance | | ------------ | --------------- | ------- | | DUMMYACCOUNT | 1 | 10 | | DUMMYACCOUNT | 2 | 15 | | DUMMYACCOUNT | 3 | 5 | As seen in the table, each row shows the `balance` of `DUMMYACCOUNT` at a sepecific `ledger_sequence`. ### List of state tables - [Accounts](../data-catalog/data-dictionary/bronze/accounts.mdx) - [Claimable Balances](../data-catalog/data-dictionary/bronze/claimable-balances.mdx) - [Contract Code](../data-catalog/data-dictionary/bronze/contract-code.mdx) - [Contract Data](../data-catalog/data-dictionary/bronze/contract-data.mdx) - [Liquidity Pools](../data-catalog/data-dictionary/bronze/liquidity-pools.mdx) - [Offers](../data-catalog/data-dictionary/bronze/offers.mdx) - [Trustlines](../data-catalog/data-dictionary/bronze/trustlines.mdx) - [TTL](../data-catalog/data-dictionary/bronze/ttl.mdx) ## What are history tables? These tables are a full history log of actions on the network. - Each row represents some action on the network (ledger closing, transactions, operations, etc...) - Encompasses the complete chronological history of activity on the network ### Example contents of a history table Let's say we operations for a specific transaction. The `history_operations` table would return the following: | transaction_hash | id | ledger_sequence | type | details_json | | --- | --- | --- | --- | --- | | TXHASH1 | 1 | 1 | 13 | `{"some": "operation details"}` | | TXHASH1 | 2 | 1 | 2 | `{"some": "operation details"}` | | TXHASH1 | 3 | 1 | 13 | `{"some": "operation details"}` | As seen in the table, each row shows the distinct operation with its unique `id`. ### List of history tables - [History Assets](../data-catalog/data-dictionary/bronze/history-assets.mdx) - [History Contract Events](../data-catalog/data-dictionary/bronze/history-contract-events.mdx) - [History Effects](../data-catalog/data-dictionary/bronze/history-effects.mdx) - [History Ledgers](../data-catalog/data-dictionary/bronze/history-ledgers.mdx) - [History Operations](../data-catalog/data-dictionary/bronze/history-operations.mdx) - [History Trades](../data-catalog/data-dictionary/bronze/history-trades.mdx) - [History Transactions](../data-catalog/data-dictionary/bronze/history-transactions.mdx) - [Enriched History Operations](../data-catalog/data-dictionary/silver/enriched-history-operations.mdx) --- ## Optimizing Queries Hubble has terabytes of data to explore—that’s a lot of data! With access to so much data at your fingertips, it is crucial to performance-tune your queries. One of the strengths of BigQuery is also its pitfall: you have access to tremendous compute capabilities, but you pay for what you use. If you fine-tune your queries, you will have access to powerful insights at the fraction of the cost of maintaining a data warehouse yourself. It is, however, easy to incur burdensome costs if you are not careful. ## Best Practices ### Pay attention to table structure. Large tables are partitioned and clustered according to common access patterns. Prune only the partitions you need, and filter or aggregate by clustered fields when possible. Joining tables on strings is expensive. Refrain from joining on string keys if you can utilize integer keys instead. Read the docs on [Viewing Metadata](./viewing-metadata.mdx) to learn more about table metadata. #### Example - Profiling Operation Types Let’s say you wanted to profile the [types of operations](../../../../learn/fundamentals/transactions/list-of-operations.mdx) submitted to the Stellar Network monthly. ======== Let’s say you wanted to profile the [types of operations](../../../../learn/fundamentals/transactions/list-of-operations.mdx) submitted to the Stellar Network monthly. ```sql # Inefficient query select datetime_trunc(batch_run_date, month) as `month`, type_string, count(id) as count_operations from `crypto-stellar.crypto_stellar.history_operations` group by `month`, type_string order by `month` ``` The `history_operations` table is partitioned by `batch_run_date` and clustered by `transaction_id`, `source_account` and `type`. Query costs are greatly reduced by pruning unused partitions. In this case, you could filter out operations submitted before 2023. This reduces the query cost by 4x. ```sql # Prune out partitions you do not need select datetime_trunc(batch_run_date, month) as `month`, type_string, count(id) as count_operations from `crypto-stellar.crypto_stellar.history_operations` -- batch_run_date is a datetime object, formatted as ISO 8601 where batch_run_date > '2023-01-01T00:00:00' group by `month`, type_string order by `month` ``` Switching the aggregation field to `type`, which is a clustered field, will cut costs by a third: ```sql # Prune out partitions you do not need # Aggregate by clustered field, `type` select datetime_trunc(batch_run_date, month) as `month`, `type`, count(id) as count_operations from `crypto-stellar.crypto_stellar.history_operations` where batch_run_date >= '2023-01-01T00:00:00' group by `month`, `type` order by `month` ``` **Performance Summary** By pruning partitions and aggregating on a clustered field, the query processing costs reduce by a factor of 8. | | Bytes Processed | Cost | | ---------------- | --------------- | ------ | | Original Query | 408.1 GB | $2.041 | | Improved Query 1 | 83.06 GB | $0.415 | | Improved Query 2 | 54.8 GB | $0.274 | ### Be as specific as possible. Do not write `SELECT *` statements unless you need every column returned in the query response. Since BigQuery is a columnar database, it can skip reading data entirely if the columns are not included in the select statement. The wider the table is, the more crucial it is to select _only_ what you need. #### Example - Transaction Fees Let’s say you needed to view the fees for all transactions submitted in May 2023. What happens if you write a `SELECT *`? ```sql # Inefficient query select * from `crypto-stellar.crypto_stellar.history_transactions` where batch_run_date >= '2023-05-01' and batch_run_date < '2023-06-01' ``` This query is estimated to cost almost $4! If you only need fee information, you can filter down the data, reducing the query costs by a factor of 50x: ```sql # Select only the columns you need select id, transaction_hash, ledger_sequence, account, fee_account, max_fee, fee_charged, new_max_fee from `crypto-stellar.crypto_stellar.history_transactions` where batch_run_date >= '2023-05-01' and batch_run_date < '2023-06-01' ``` **Performance Summary** Hubble stores wide tables. Query performance is greatly improved by selecting only the data you need. This principle is critical when exploring the operations and transactions tables, which are the largest tables in Hubble. | | Bytes Processed | Cost | | -------------- | --------------- | ------ | | Original Query | 769.45 GB | $3.847 | | Improved Query | 16.6 GB | $0.083 | :::tip The BigQuery console lets you preview table data for free, which is the equivalent of writing a `SELECT *`. Click on the table name, and pull up the preview pane. ::: ### Filter early. When writing complex queries, filter the data as early as possible. Push `WHERE` and `GROUP BY` clauses up in the query to reduce the amount of data scanned. :::caution `LIMIT` clauses speed up performance, but **do not** reduce the amount of data scanned. Only the _final_ results returned to the user are limited. Use with caution. ::: Push transformations and mathematical functions to the end of the query when possible. Functions like `TRIM()`, `CAST()`, `SUM()`, and `REGEXP_*` are resource intensive and should only be applied to final table results. ## Estimating Costs If you need to estimate costs before running a query, there are several options available to you: ### BigQuery Console The BigQuery Console comes with a built-in query validator. It verifies query syntax and provides an estimate of the number of bytes processed. The validator can be found in the upper right hand corner of the Query Editor, next to the green checkmark. To calculate the query cost, convert the number of bytes processed into terabytes, and multiply the result by $5: `(estimated bytes read / 1TB) * $5` Paste the following query into the Editor to view the estimated bytes processed. ```sql select timestamp_trunc(closed_at, month) as month, sum(tx_set_operation_count) as total_operations from `crypto-stellar.crypto_stellar.history_ledgers` where batch_run_date >= '2023-01-01T00:00:00' and batch_run_date < '2023-06-01T00:00:00' and closed_at >= '2023-01-01T00:00:00' and closed_at < '2023-06-01T00:00:00' group by month ``` The validator estimates that 51.95MB of data will be read. 0.00005195 TB \* $5 = $0.000259. _That’s a cheap query!_ ### dryRun Config Parameter If you are submitting a query through a [BigQuery client library](https://cloud.google.com/bigquery/docs/reference/libraries), you can perform a dry run to estimate the total bytes processed before submitting the query job. ```python from google.cloud import bigquery # Construct a BigQuery client object client = bigquery.Client() # Set dry run to True to get bytes processed job_config = bigquery.QueryJobConfig(dry_run=True, use_query_cache=False) # Pass in job config sql = """ select timestamp_trunc(closed_at, month) as month, sum(tx_set_operation_count) as total_operations from `crypto-stellar.crypto_stellar.history_ledgers` where batch_run_date >= '2023-01-01T00:00:00' and batch_run_date < '2023-06-01T00:00:00' and closed_at >= '2023-01-01T00:00:00+00' and closed_at < '2023-06-01T00:00:00' group by month """ # Make API request query_job = client.query((sql), job_config = job_config) # Calculate the cost cost = (query_job.total_bytes_processed / 1000000000000) * 5 print(f'This query will process {query_job.total_bytes_processed} bytes') print(f'This query will cost approximately ${cost}') ``` There are also [IDE plugins](https://plugins.jetbrains.com/plugin/15884-bigquery-query-size-estimator) that can approximate cost. For more information regarding query costs, read the [BigQuery documentation](https://cloud.google.com/bigquery/docs/estimate-costs). --- ## Queries for Horizon/RPC-like Data [Horizon](../../../apis/horizon/README.mdx) and [RPC](../../../apis/rpc/README.mdx) both provide API endpoints to retrieve data from the Stellar network. The following example queries retrieve the same data by using Hubble with the added benefit of being able to return historical data. ## Accounts [horizon/accounts](../../../apis/horizon/api-reference/resources/accounts/README.mdx) Users interact with the Stellar network through accounts. Everything else in the ledger—assets, offers, trustlines, etc.—are owned by accounts, and accounts must authorize all changes to the ledger through signed transactions. Learn more about [accounts](../../../../learn/glossary.mdx#account). ### General account information and XLM balance ```sql select account_id , sequence_number as sequence , sequence_ledger , sequence_time , num_subentries , home_domain , last_modified_ledger , balance -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.accounts` where true -- equivalent account_id parameter and account_id= -- filter to the specific date or date range and batch_run_date between '2024-01-01' and '2024-01-02' order by closed_at desc ``` ### Non-XLM asset balance information ```sql select account_id , asset_code , asset_issuer , asset_type , balance -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.trust_lines` where true -- equivalent account_id parameter and account_id= -- filter to the specific date or date range and batch_run_date between '2024-01-01' and '2024-01-02' order by closed_at desc ``` ### Account signer information ```sql select account_id , signer -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.account_signers` where true -- equivalent account_id parameter and account_id= -- filter to the specific date or date range and batch_run_date between '2024-01-01' and '2024-01-02' order by closed_at desc ``` ### List all accounts ```sql select account_id from `crypto-stellar.crypto_stellar_dbt.accounts_current` where true -- limit does not reduce the amount of data BigQuery queries limit 1000 ``` ### Retrieve an Account’s Transactions ```sql select id , transaction_hash , ledger_sequence , operation_count , successful -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_transactions` where true -- equivalent account_id parameter and account= -- highly recommended to provide a date range to reduce amount of data queried and closed_at >= timestamp(current_date) -- limit does not reduce the amount of data BigQuery queries limit 1000 ``` ### Retrieve an Account’s Operations ```sql select op_account_id , op_id , type_string , ledger_sequence -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` where true -- equivalent account_id parameter and op_account_id= -- highly recommended to provide a date range to reduce amount of data queried and closed_at >= timestamp(current_date) -- limit does not reduce the amount of data BigQuery queries limit 1000 ``` ### Retrieve an Account’s Payments ```sql select op_account_id , transaction_id , transaction_hash , op_id , ledger_sequence , `from` , `to` , asset_code , asset_issuer , asset_type , amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` where true -- equivalent account_id parameter and op_account_id= and type in (0, 1, 2, 13, 8) -- create_account, payment, path_payment_strict_recieve, path_payment_strict_send, and account_merge -- highly recommended to provide a date range to reduce amount of data queried and closed_at >= timestamp(current_date) -- limit does not reduce the amount of data BigQuery queries limit 1000 ``` ### Retrieve an Account’s Effects ```sql select eho.op_account_id , he.operation_id , eho.type_string , he.details.asset_code , he.details.asset_issuer , he.details.asset_type , he.details.amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_effects` as he inner join `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` as eho on he.operation_id = eho.op_id and eho.closed_at >= timestamp(current_date) where true -- equivalent account_id parameter and op_account_id= -- highly recommended to provide a date range to reduce amount of data queried and he.closed_at >= timestamp(current_date) -- limit does not reduce the amount of data BigQuery queries limit 1000 ``` ### Retrieve an Account’s Offers ```sql select seller_id , offer_id , selling_asset_code , buying_asset_code , amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.offers_current` where true -- equivalent account_id parameter and seller_id= ``` ### Retrieve an Account’s Trades ```sql with selling_side as ( select selling_account_address as account_id , history_operation_id , selling_asset_code , buying_asset_code , selling_amount as amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_trades` where true -- equivalent account_id parameter and selling_account_address= -- highly recommended to provide a date range to reduce amount of data queried and batch_run_date >= current_date ), buying_side as ( select buying_account_address as account_id , history_operation_id , selling_asset_code , buying_asset_code , buying_amount as amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_trades` where true -- equivalent account_id parameter and buying_account_address= -- highly recommended to provide a date range to reduce amount of data queried and batch_run_date >= current_date ), union_data as ( select account_id , history_operation_id , selling_asset_code , buying_asset_code , amount from selling_side union all select account_id , history_operation_id , selling_asset_code , buying_asset_code , amount from buying_side ) select * from union_data ``` ## Assets [horizon/assets](../../../apis/horizon/api-reference/resources/assets/README.mdx) Assets are representations of value issued on the Stellar network. An asset consists of a type, code, and issuer. Learn more about [assets](../../../../learn/glossary.mdx#asset). ### List all Assets ```sql with assets as ( select asset_code , asset_issuer -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_assets` where true -- equivalent asset_code/asset_issuer parameters and asset_code= and asset_issuer= ), accounts as ( select count(ac.account_id) as num_accounts , countif(ac.flags=1) as authorized_accounts , sum(balance) as amount from assets join `crypto-stellar.crypto_stellar_dbt.trust_lines_current` as ac on assets.asset_code = ac.asset_code and assets.asset_issuer = ac.asset_issuer where true ), liquidity_pools as ( select count(*) as num_liquidity_pools , sum(lpc.asset_a_amount) as liquidity_pools_amount from assets join `crypto-stellar.crypto_stellar_dbt.liquidity_pools_current` as lpc on assets.asset_code = lpc.asset_a_code and assets.asset_issuer = lpc.asset_a_issuer where true ), current_claimable_balances as ( -- TODO: This should be replaced with the claimable_balances_current table when available SELECT cb.balance_id , cb.asset_code , cb.asset_issuer , cb.asset_amount , cb.deleted , cb.last_modified_ledger , row_number() over(partition by cb.balance_id, cb.asset_code, cb.asset_issuer order by last_modified_ledger desc, deleted desc) as rn FROM assets join `crypto-stellar.crypto_stellar.claimable_balances` as cb on assets.asset_code = cb.asset_code and assets.asset_issuer = cb.asset_issuer where true order by last_modified_ledger asc ), claimable_balances as ( select count(*) as num_claimable_balances , sum(asset_amount) as claimable_balances_amount from current_claimable_balances where true and rn = 1 and deleted = false ) select * from assets join accounts on true join liquidity_pools on true join claimable_balances on true ``` ### Getting Stellar Asset Contract Information for an Asset ```sql select count(balance_holder) as num_contracts , sum(parse_bignumeric(balance)) * .0000001 as contracts_amount from `crypto-stellar.crypto_stellar_dbt.contract_data_current` where true and contract_id = and balance != "" ``` ## Claimable Balances [horizon/claimable_balances](../../../apis/horizon/api-reference/resources/claimablebalances/README.mdx) A Claimable Balance represents the transfer of ownership of some amount of an asset. Claimable balances provide a mechanism for setting up a payment which can be claimed in the future. This allows you to make payments to accounts which are currently not able to accept them. ### List All Claimable Balances ```sql current_claimable_balances as ( -- TODO: This should be replaced with the claimable_balances_current table when available select balance_id , asset_code , asset_issuer , asset_amount , deleted , last_modified_ledger -- please see table schema for more available columns and data , row_number() over(partition by balance_id, asset_code, asset_issuer order by last_modified_ledger desc, deleted desc) as rn from `crypto-stellar.crypto_stellar.claimable_balances` as cb where true -- equivalent asset_code/asset_issuer parameters and asset_code= and asset_issuer= order by last_modified_ledger asc ) select * from current_claimable_balances where true and rn = 1 and deleted = false -- other filters for sponsor and claimants --and sponsor = --and claimants.destination = ``` ### Retrieve a Claimable Balance ```sql with assets as ( -- Used to filter to a specific asset select asset_code , asset_issuer from `crypto-stellar.crypto_stellar.history_assets` where true -- equivalent asset_code/asset_issuer parameters and asset_code= and asset_issuer= ), current_claimable_balances as ( -- TODO: This should be replaced with the claimable_balances_current table when available select cb.balance_id , cb.asset_code , cb.asset_issuer , cb.asset_amount , cb.deleted , cb.last_modified_ledger -- please see table schema for more available columns and data , row_number() over(partition by cb.balance_id, cb.asset_code, cb.asset_issuer order by last_modified_ledger desc, deleted desc) as rn from assets join `crypto-stellar.crypto_stellar.claimable_balances` as cb on assets.asset_code = cb.asset_code and assets.asset_issuer = cb.asset_issuer where true order by last_modified_ledger asc ) select * from current_claimable_balances where true and rn = 1 and deleted = false and balance_id = ``` ### Retrieve Related Operations and Transactions ```sql select op_id , transaction_id -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` where true and balance_id = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ## Effects [horizon/effects](../../../apis/horizon/api-reference/resources/effects/README.mdx) Effects represent specific changes that occur in the ledger as a result of successful operations, but are not necessarily directly reflected in the ledger or history, as transactions and operations are. ### List All Effects ```sql select type , type_string -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_effects` where true -- Optional filtering options --and operation_id = --and address = --and details.liquidity_pool_id = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ## Ledgers [horizon/ledgers](../../../apis/horizon/api-reference/resources/ledgers/README.mdx) Each ledger stores the state of the network at a point in time and contains all the changes - transactions, operations, effects, etc. - to that state. Learn more about [ledgers](../../../../learn/glossary.mdx#ledger). ### List All Ledgers ```sql select sequence -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_ledgers` where true -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ### Retrieve a Ledger’s Transactions and Operations ```sql select ledger_sequence -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` where true and ledger_sequence = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ### Retrieve a Ledger’s Payments ```sql select ledger_sequence -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` as eho where true and type in (0, 1, 2, 8, 13) -- create_account, payment, path_payment_strict_recieve, path_payment_strict_send, and account_merge and ledger_sequence = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ## Liquidity Pools [horizon/liquidity_pools](../../../apis/horizon/api-reference/resources/liquiditypools/README.mdx) Liquidity Pools provide a simple, non-interactive way to trade large amounts of capital and enable high volumes of trading. ### List Liquidity Pools ```sql select liquidity_pool_id , fee , trustline_count , asset_a_code , asset_a_issuer , asset_a_amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.liquidity_pools_current` where true ``` ### Retrieve a Liquidity Pool ```sql select liquidity_pool_id , fee , trustline_count , asset_a_code , asset_a_issuer , asset_a_amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.liquidity_pools_current` where true and liquidity_pool_id = ``` ### Retrieve a Liquidity Pool’s Transactions and Operations ```sql select op_id , transaction_id -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` where true and liquidity_pool_id = ``` ### Retrieve a Liquidity Pool’s Related Trades ```sql select selling_liquidity_pool_id , selling_amount , buying_amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_trades` where true and selling_liquidity_pool_id = ``` ## Offers [horizon/offers](../../../apis/horizon/api-reference/resources/offers/README.mdx) Offers are statements about how much of an asset an account wants to buy or sell. Learn more about [offers](../../../../learn/glossary.mdx#decentralized-exchange). ### List All Offers ```sql select offer_id , seller_id , selling_asset_code , selling_asset_issuer , amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.offers_current` where true -- optional filter by account --and seller_id = and closed_at between and ``` ### Retrieve an Offer ```sql select offer_id , seller_id , selling_asset_code , selling_asset_issuer , amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.offers_current` where true and offer_id = and closed_at between and ``` ### Retrieve an Offer’s Trades ```sql select selling_offer_id , selling_amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_trades` where true and selling_offer_id = -- offer_id could also be the buying_offer_id --and buying_offer_id = ``` ## Operations [horizon/operations](../../../apis/horizon/api-reference/resources/operations/README.mdx) Operations are objects that represent a desired change to the ledger: payments, offers to exchange currency, changes made to account options, etc. Operations are submitted to the Stellar network grouped in a Transaction. Each of Stellar’s operations have a unique operation object. ### List All Operations ```sql select op_id -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` where true -- optional filters --and op_source_account = --and ledger_sequence = --and liquidity_pool_id = --and transaction_hash = --and transaction_id = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ### List All Payments ```sql select op_id -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` as eho where true -- optional filters --and op_source_account = --and ledger_sequence = --and liquidity_pool_id = --and transaction_hash = --and transaction_id = and type in (0, 1, 2, 8, 13) -- create_account, payment, path_payment_strict_recieve, path_payment_strict_send, and account_merge -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ### Retrieve an Operation ```sql select op_id -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` as eho where true -- op_id = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ### Retrieve an Operation’s Effects ```sql select operation_id -- please see table schema for more available columns and data from `crypto-stellar.crypto_stella.history_effects` where true operation_id = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ## Trades [horizon/trades](../../../apis/horizon/api-reference/resources/trades/README.mdx) When an offer is fully or partially fulfilled, a trade happens. Trades can also be caused by successful path payments, because path payments involve fulfilling offers. A trade occurs between two parties—`base` and `counter`. Which is which is either arbitrary or determined by the calling query. Learn more about [trades](../../../../learn/glossary.mdx#decentralized-exchange). ### List All Trades ```sql select -- base_asset == selling_asset -- counter_asset == buying_asset selling_asset_code , selling_asset_issuer , selling_amount , buying_asset_code , buying_asset_issuer , buying_amount -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_trades` where true -- optional filter columns and selling_offer_id = and buying_offer_id = and selling_asset_code = and selling_asset_issuer = and selling_liquidity_pool_id = and selling_account_address = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ## Transactions [horizon/transactions](../../../apis/horizon/api-reference/resources/transactions/README.mdx) Transactions are commands that modify the ledger state and consist of one or more operations. Learn more about [transactions](../../../../learn/glossary.mdx#transaction). ### List All Transactions ```sql select id , max_fee , operation_count -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_transactions` where true -- optional filters --and account = --and ledger_sequence = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ### Retrieve a Transaction ```sql select id , max_fee , operation_count -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_transactions` where true and id = -- transaction_hash is also available --and transaction_hash = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ### Retrieve a Transaction’s Operations ```sql select transaction_id , op_id , max_fee , operation_count -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` where true and transaction_id = -- transaction_hash is also available --and transaction_hash = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ## Trade Aggregations [horizon/trade_aggregations](../../../apis/horizon/api-reference/aggregations/trade-aggregations/README.mdx) A trade aggregation represents aggregated statistics on an asset pair (base and counter) for a specific time period. Trade aggregations are useful to developers of trading clients and provide historical trade data. ### List Trade Aggregations ```sql select date(closed_at) , selling_asset_code , selling_asset_issuer , buying_asset_code , buying_asset_issuer , count(1) as number_of_trades -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_trades` where true -- there are various columns to filter by -- and selling_asset_code = -- and selling_asset_issuer = -- and buying_asset_code = -- and buying_asset_issuer = -- and closed_at between and group by 1,2,3,4,5 ``` ## Fee Stats [horizon/fee-stats](../../../apis/horizon/api-reference/aggregations/fee-stats/README.mdx) Fee stats are used to predict what fee to set for a transaction before submitting it to the network. Two models are available: `ledger_fee_stats_agg` (ledger-grain) and `daily_fee_stats_agg` (daily-grain). Both provide separate Classic and Soroban fee breakdowns, inclusion fee metrics, and surge pricing statistics. ### Retrieve Daily Fee Stats ```sql select day_agg , total_fee_charged , max_fee_charged , txn_count , failed_txn_count , classic_txn_count , classic_sum_fee_charged , classic_max_fee_charged , classic_pct_ledgers_in_surge , soroban_txn_count , soroban_sum_fee_charged , soroban_max_fee_charged , soroban_sum_inclusion_fee_charged , soroban_sum_resource_fee , soroban_pct_ledgers_in_surge -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.daily_fee_stats_agg` where true and day_agg between and ``` ### Retrieve Ledger-Level Fee Stats ```sql select day_agg , ledger_sequence , total_fee_charged , max_fee_charged , txn_count , classic_txn_count , classic_sum_fee_charged , classic_is_surge_ledger , soroban_txn_count , soroban_sum_fee_charged , soroban_sum_inclusion_fee_charged , soroban_sum_resource_fee , soroban_is_surge_ledger -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.ledger_fee_stats_agg` where true and day_agg between and -- optional filter by ledger --and ledger_sequence = ``` ## getEvents [rpc/getEvents](../../../apis/rpc/api-reference/methods/getEvents.mdx) Contract events and diagnostic events are used for supplemental information related to a contract invocation ```sql select contract_id , topics_decoded , data_decoded -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar.history_contract_events` where true -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` ## getLedgerEntries [rpc/getLedgerEntries](../../../apis/rpc/api-reference/methods/getLedgerEntries.mdx) Enables the retrieval of various ledger states, such as accounts, trustlines, offers, data, claimable balances, and liquidity pools. It also provides direct access to inspect a contract's current state, its code, or any other ledger entry ### Contract Data Entries ```sql select ledger_key_hash , contract_id , balance_holder , balance from `crypto-stellar.crypto_stellar_dbt.contract_data_current` where true and ledger_key_hash = ``` ### Contract Code Entries ```sql select ledger_key_hash , contract_code_hash from `crypto-stellar.crypto_stellar_dbt.contract_code_current` where true and ledger_key_hash = ``` ## getTransaction [rpc/getTransaction](../../../apis/rpc/api-reference/methods/getTransaction.mdx) Transactions are commands that modify the ledger state and consist of one or more operations. Learn more about [transactions](../../../../learn/glossary.mdx#transaction). ```sql select transaction_id , op_id , max_fee , operation_count -- please see table schema for more available columns and data from `crypto-stellar.crypto_stellar_dbt.enriched_history_operations` where true and transaction_id = -- transaction_hash is also available --and transaction_hash = -- highly recommended to provide a date range to reduce amount of data queried and closed_at between and ``` --- ## Viewing Metadata Hubble publishes metadata which can help users determine which tables to query, how frequently the dataset updates, and general information about the dataset. There are two ways to access this information: ## BigQuery Explorer When accessing Hubble from its starred link, the Explorer pane will load metadata about the `crypto-stellar.crypto_stellar` dataset. Use the Toggle to view the contents of the Dataset. Clicking a table name will load the following: - _Schema_ - detailed information about the table schema, including column definitions and data types. Viewing the schema helps write a SQL query - _Details_ - general information about the table itself, including partitioning, clustering and table size. Viewing details helps with query optimization - _Preview_ - raw sample data from the table. The data presented is the equivalent of running a `SELECT *` statement ## INFORMATION_SCHEMA BigQuery supports read-only, system-defined views that provide metadata information about BigQuery objects. The views can be queried via SQL from the BigQuery UI or Client Libraries. :::note Queries executed against the `INFORMATION_SCHEMA` cannot be cached and **will** incur data processing charges for each run. ::: From the BigQuery Editor, the following query will list all tables in Hubble: ```sql # List all tables in Hubble #standardSQL select * from `crypto-stellar.crypto_stellar`.INFORMATION_SCHEMA.TABLES; ``` If you want details on a particular table, you can return the table schema: ```sql # List all columns for the accounts table select table_name, column_name, is_nullable, data_type, is_partitioning_column from `crypto-stellar.crypto_stellar`.INFORMATION_SCHEMA.COLUMNS where table_name = "accounts"; ``` More on the `INFORMATION_SCHEMA` can be found [here](https://cloud.google.com/bigquery/docs/information-schema-intro). --- ## Data Catalog View all Hubble data catalog information. --- ## Data Dictionary --- ## Bronze 🥉 Raw, untransformed data from the Stellar network. These tables break down and organize Ledger Metadata into a series of useful tables such as breaking down Ledger Metadata into history_ledgers, history_transactions, and history operations tables. ## When to use These tables are best used for deep diving into raw Stellar data where specific custom filtering is needed. One example is retrieving and analyzing a specific contract's data entries using the `contract_data` table. Please check if any of the silver and gold tables work for your use case before using the bronze tables as they may incur higher BigQuery costs due to their size. --- ## Account Signers ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | account_id, signer, closed_at | | Partition Field(s) | batch_run_date (MONTH partition) | | Clustered Field(s) | account_id, signer, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.account_signers) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | account_id | The unique identifier of the account. | string | | Yes | | | signer | The address of the account that is allowed to authorize (sign) transactions for another account. This process is called multi-sig. | string | | Yes | | | weight | The numeric weight of the signer. All weights on a transaction are added up and used to determine if a transaction meets the threshold requirements to complete the transaction. | float | | Yes | | | sponsor | The account address of the sponsor who is paying the reserves for this ledger entry. | string | | No | | | last_modified_ledger | The ledger sequence number when the ledger entry (this unique signer for the account) was modified. Deletions do not count as a modification and will report the prior modification sequence number | integer | | Yes | | | ledger_entry_change | Code that describes the ledger entry change type that was applied to the ledger entry. | integer | | Yes | | | deleted | Indicates whether the ledger entry (balance id) has been deleted or not. Once an entry is deleted, it cannot be recovered. | boolean | | Yes | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | --- ## Accounts ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | account_id, closed_at | | Partition Field(s) | batch_run_date (MONTH partition) | | Clustered Field(s) | account_id, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.accounts) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | account_id | The address of the account. The address is the account's public key encoded in base32. All account addresses start with a `G` | string | | Yes | | | balance | The number of units of XLM held by the account | float | | Yes | The `accounts` table only reports monetary balances for XLM. Any other asset class is reported in the `trust_lines` table. | | buying_liabilities | The sum of all buy offers owned by this account for XLM only | float | | Yes | The `accounts` table only reports monetary balances for XLM. Any other asset class is reported in the `trust_lines` table. For buy offers, the account must hold the amount of asset to complete the transaction | | selling_liabilities | The sum of all sell offers owned by this account for XLM only | float | | Yes | The `accounts` table only reports monetary balances for XLM. Any other asset class is reported in the `trust_lines` table. | | sequence_number | The account's current sequence number. The sequence number controls operations applied to an account. Operations must submit a unique sequence number that is incremented by 1 in order to apply the operation to the account so that account changes will not collide within a ledger | integer | | Yes | | | num_subentries | The total number of ledger entries connected to this account. Ledger entries include: trustlines, offers, signers, and data entries. (Claimable balances are counted under sponsoring entries, not subentries). Any newly created trustline, offer, signer or data entry will increase the number of subentries by 1. Accounts may have up to 1,000 subentries | integer | | Yes | Each entry on a ledger takes up space, which is expensive to store on the blockchain. For each entry, an account is required to hold a [minimum XLM balance](../../../../../../learn/glossary.mdx#minimum-balance). The reserve is calculated by (2 + num_subentries - num_sponsoring + num_sponsored) \* 0.5XLM | | inflation_destination | Deprecated: The account address to receive an inflation payment when they are disbursed on the network. | string | | Yes | Inflation was discontinued in 2019 by validator vote. | | flags | Denotes the enabling and disabling of certain asset issuer privileges | integer | 0 - None, Default1 - Auth Required (all trustlines by default are untrusted and require manual trust established)2 - Auth Revocable (allows trustlines to be revoked if account no longer trusts asset)4 - Auth Immutable (all auth flags are read only when set)8 - Auth Clawback Enabled (asset can be clawed back from the user) | Yes | Flags are set on the issuer accounts for an asset. When user accounts trust an asset, the flags applied to the asset originate from this account | | home_domain | The domain that hosts this account's stellar.toml file | string | | Yes | Only applies to asset issuer accounts. The stellar.toml file contains metadata about the asset issuer which helps identify who the issuer is and instills trust in the asset | | master_weight | The weight of the master key, which is the private key for this account. If a master key is `0,` the account is locked and cannot be used. | integer | Integers from 1 to 255 | Yes | | | threshold_low | The sum of the weight of all signatures that sign a transaction for the low threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | Yes | Each operation falls under a specific threshold category: low, medium or high. Thresholds define the level of privilege an operation needs in order to succeed (this is a security measure) Low Security: Allow Trust, Set Trust Line Flags, Bump Sequence and Claim Claimable Balance Medium Security: Everything Else High Security: Account Merge, Set Options | | threshold_medium | The sum of the weight of all signatures that sign a transaction for the medium threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | Yes | Each operation falls under a specific threshold category: low, medium or high. Thresholds define the level of privilege an operation needs in order to succeed (this is a security measure) Low Security: Allow Trust, Set Trust Line Flags, Bump Sequence and Claim Claimable Balance Medium Security: Everything Else High Security: Account Merge, Set Options | | threshold_high | The sum of the weight of all signatures that sign a transaction for the high threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | Yes | Each operation falls under a specific threshold category: low, medium or high. Thresholds define the level of privilege an operation needs in order to succeed (this is a security measure) Low Security: Allow Trust, Set Trust Line Flags, Bump Sequence and Claim Claimable Balance Medium Security: Everything Else High Security: Account Merge, Set Options | | last_modified_ledger | The ledger sequence number when the ledger entry (this unique signer for the account) was modified. Deletions do not count as a modification and will report the prior modification sequence number | integer | | Yes | If an account updates a signer's weight at sequence 1234 and then decides to delete the signer at 2345, the deleted record will still have a modified sequence of 1234. | | ledger_entry_change | Code that describes the ledger entry change type that was applied to the ledger entry. | integer | 0 - Ledger Entry Created1 - Ledger Entry Updated2 - Ledger Entry Deleted3 - Ledger Entry State (value of the entry) | Yes | Valid entry change types are 0, 1, and 2 for ledger entries of type `accounts` | | deleted | Indicates whether the ledger entry (account id) has been deleted or not. Once an entry is deleted, it cannot be recovered. | boolean | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | The table is partitioned on batch_run_date. It is recommended to always include the batch_run_date in the filter if possible to help reduce query cost. | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | sponsor | The account address of the sponsor who is paying the reserves for this account. | string | | No | | | num_sponsored | The number of reserves sponsored for this account (meaning another account is paying for the minimum balance). Sponsored entries do not incur any reserve requirement on the account that owns the entry. | integer | | No | Defaults to 0 Accounts, offers, trustlines, data and signers can be optionally sponsored. Claimable Balances must be sponsored. See more information on sponsorship [here](../../../../../../build/guides/transactions/sponsored-reserves.mdx). | | num_sponsoring | The number of reserves sponsored by this account. Entries sponsored by this account incur a reserve requirement | integer | | No | Defaults to 0 Accounts, offers, trustlines, data and signers can be optionally sponsored. Claimable Balances must be sponsored. See more information on sponsorship [here](../../../../../../build/guides/transactions/sponsored-reserves.mdx). | | sequence_ledger | The unsigned 32-bit ledger number of the sequence number's age | integer | | No | Reflects the last time an account touched its sequence number. Note that even if the Bump Sequence operation has no effect, eg it does not increase the sequence number, it still counts as a "touch" | | sequence_time | The UNIX timestamp of the sequence number's age | timestamp | | No | Reflects the last time an account touched its sequence number. Note that even if the Bump Sequence operation has no effect, eg it does not increase the sequence number, it still counts as a "touch" | --- ## Claimable Balances(Bronze) ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | balance_id, closed_at | | Partition Field(s) | batch_run_date (MONTH partition) | | Clustered Field(s) | asset_id, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.claimable_balances) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | balance_id | A unique identifier for this claimable balance. The Balance id is a compilation of `Balance Type` + `SHA-256 hash history_operation_id` | string | | Yes | The Balance Type is fixed at V0, `00000000`. If there is a protocol change that materially impacts the mechanics of claimable balances, the balance type would update to V1. | | claimants | The list of entries which are eligible to claim the balance and preconditions that must be fulfilled to claim | array[record] | | Yes | Multiple accounts can be specified in the claimants record, including the account of the balance creator. | | claimants.destination | The account id who can claim the balance | string | | Yes | | | claimants.predicate | The condition which must be satisfied so the destination can claim the balance. The predicate can include logical rules using AND, OR and NOT logic. | array[record] | | Yes | | | claimants.predicate.unconditional | If true it means this clause of the condition is always satisfied | boolean | | No | When the predicate is only unconditional = true, it means that the balance can be claimed under any conditions | | claimants.predicate.abs_before | Deadline for when the balance must be claimed. If a balance is claimed before the date then the clause of the condition is satisfied. | string | | No | | | claimants.predicate.rel_before | A relative deadline for when the claimable balance can be claimed. The value represents the number of seconds since the close time of the ledger which created the claimable balance | integer | | No | This condition is useful when creating a timebounds based on creation conditions. If the creator wanted a balance only claimable one week after creation, this condition would satisfy that rule. | | claimants.predicate.abs_before_epoch | A UNIX epoch value in seconds representing the same deadline date as abs_before. | integer | | No | | | asset_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | Yes | | | asset_code | The 4 or 12 character code representation of the asset on the network | string | | No | | | asset_issuer | The address of the account that created the asset | string | | No | | | asset_amount | The amount of the asset that can be claimed | float | | Yes | | | sponsor | The account address of the sponsor who is paying the reserves for this claimable balance | string | | No | Sponsors of claimable balances are the creators of the balance. | | flags | Denotes the enabling and disabling of certain balance issuer privileges | integer | 0 - None, Default1 - Auth Clawback Enabled | Yes | Flags are set by the claimable balance accounts for an asset. When user accounts claim a balance, the flags applied to the asset originate from this account | | last_modified_ledger | The ledger sequence number when the ledger entry (this unique signer for the account) was modified. Deletions do not count as a modification and will report the prior modification sequence number | integer | | Yes | | | ledger_entry_change | Code that describes the ledger entry change type that was applied to the ledger entry. | integer | 0 - Ledger Entry Created1 - Ledger Entry Updated2 - Ledger Entry Deleted3 - Ledger Entry State (value of the entry) | Yes | Valid entry change types are 0, and 2 for ledger entries of type `claimable_balances`. Once created, a balance cannot be updated. | | deleted | Indicates whether the ledger entry (balance id) has been deleted or not. Once an entry is deleted, it cannot be recovered. | boolean | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | The table is partitioned on batch_run_date. It is recommended to always include the batch_run_date in the filter if possible to help reduce query cost. | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | asset_id | Unique identifier for asset_code, asset_issuer | integer | | No | | --- ## Config Settings ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | config_setting_id, closed_at | | Partition Field(s) | closed_at (MONTH partition) | | Clustered Field(s) | last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.config_settings) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | config_setting_id | Config setting id types | integer | | Yes | | | contract_max_size_bytes | Max size of contract | integer | | No | | | ledger_max_instructions | Max instructions per ledger | integer | | No | | | tx_max_instructions | Max instructions per transaction | integer | | No | | | fee_rate_per_instructions_increment | Fee for instructions increment | integer | | No | | | tx_memory_limit | Transaction memory limit | integer | | No | | | ledger_max_read_ledger_entries | Max read ledger entries per ledger | integer | | No | | | ledger_max_read_bytes | Max read bytes per ledger | integer | | No | | | ledger_max_write_ledger_entries | Max write ledger entries per ledger | integer | | No | | | ledger_max_write_bytes | Max write bytes per ledger | integer | | No | | | tx_max_read_ledger_entries | Max read ledger entries per transaction | integer | | No | | | tx_max_read_bytes | Max read bytes per transaction | integer | | No | | | tx_max_write_ledger_entries | Max write ledger entries per transaction | integer | | No | | | tx_max_write_bytes | Max write bytes per transaction | integer | | No | | | fee_read_ledger_entry | Fee for read ledger entry | integer | | No | | | fee_write_ledger_entry | Fee for write ledger entry | integer | | No | | | fee_read_1kb | Fee per 1kb read | integer | | No | | | bucket_list_target_size_bytes | Bucket list target size | integer | | No | | | write_fee_1kb_bucket_list_low | Write fee for bucket list per 1kb | integer | | No | | | write_fee_1kb_bucket_list_high | Write fee for bucket list per 1kb | integer | | No | | | bucket_list_write_fee_growth_factor | Write growth fee for bucket list | integer | | No | | | fee_historical_1kb | Fee for historical storage per 1kb | integer | | No | | | tx_max_contract_events_size_bytes | Max transaction contract event size | integer | | No | | | fee_contract_events_1kb | Fee for contract event size per 1kb | integer | | No | | | ledger_max_txs_size_bytes | Max ledger transaction size | integer | | No | | | tx_max_size_bytes | Max transaction size | integer | | No | | | fee_tx_size_1kb | Fee for transaction size per 1kb | integer | | No | | | contract_cost_params_cpu_insns | Constant and linear model parameters for cost parameters for CPU | integer | | No | | | contract_cost_params_mem_bytes | Constant and linear model parameters for cost parameters for memory | integer | | No | | | contract_data_key_size_bytes | Max size of contract data keys | integer | | No | | | contract_data_entry_size_bytes | Max size of contract data entries | integer | | No | | | max_entry_ttl | Max TTL that can be set for an entry (sequence created at + this) | integer | | No | | | min_temp_entry_ttl | Min temporary entry TTL (sequence created at + this) | integer | | No | | | min_persistent_entry_ttl | Min persistent entry TTL (sequence created at + this) | integer | | No | | | auto_bump_ledgers | Automatic bump ledgers amount | integer | | No | | | persistent_rent_rate_denominator | Persistent entry rent rate denominator | integer | | No | | | temp_rent_rate_denominator | Temporary entry rent rate denominator | integer | | No | | | max_entries_to_ttl | Max entries to TTL | integer | | No | | | bucket_list_size_window_sample_size | Bucket list size window sample size | integer | | No | | | eviction_scan_size | Eviction scan size | integer | | No | | | starting_eviction_scan_level | Starting eviction scan level | integer | | No | | | ledger_max_tx_count | Max transactions in a ledger | integer | | No | | | bucket_list_size_window | Bucket list size window | integer | | No | | | min_temporary_ttl | The minimum number of entries for which a temporary entry can live on the ledger | integer | | No | | | min_persistent_ttl | The minimum number of entries for which a persisted entry can live on the ledger | integer | | No | | | max_entries_to_archive | Maximum number of entries that emit archival meta in a single ledger | integer | | No | | | last_modified_ledger | The ledger sequence number when the ledger entry (this unique signer for the account) was modified. Deletions do not count as a modification and will report the prior modification sequence number | integer | | Yes | | | ledger_entry_change | Code that describes the ledger entry change type that was applied to the ledger entry. | integer | | Yes | | | deleted | Indicates whether the ledger entry (balance id) has been deleted or not. Once an entry is deleted, it cannot be recovered. | boolean | | Yes | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | --- ## Contract Code ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | contract_code_hash, closed_at | | Partition Field(s) | closed_at (MONTH partition) | | Clustered Field(s) | last_modified_ledger, contract_code_hash | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.contract_code) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | contract_code_hash | Soroban contract code hash | string | | Yes | | | contract_code_ext_v | Contract code extension version | integer | | | | | last_modified_ledger | The ledger sequence number when the ledger entry (this unique signer for the account) was modified. Deletions do not count as a modification and will report the prior modification sequence number | integer | | Yes | | | ledger_entry_change | Code that describes the ledger entry change type that was applied to the ledger entry. | integer | | Yes | | | deleted | Indicates whether the ledger entry (balance id) has been deleted or not. Once an entry is deleted, it cannot be recovered. | boolean | | Yes | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | | ledger_key_hash | Ledger key hash used to identify expiring contract data or contract code ledger entries | string | | Yes | | | n_instructions | Number of instructions in contract code | integer | | | | | n_functions | Number of functions in contract code | integer | | | | | n_globals | Number of global variables in contract code | integer | | | | | n_table_entries | Number of table entries in contract code | integer | | | | | n_types | Number of types in contract code | integer | | | | | n_data_segments | Number of data segments in contract code | integer | | | | | n_elem_segments | Number of element segments in contract code | integer | | | | | n_imports | Number of imports in contract code | integer | | | | | n_exports | Number of exports in contract code | integer | | | | | n_data_segment_bytes | Number of data segment bytes in contract code | integer | | | | --- ## Contract Data ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | ledger_key_hash, closed_at | | Partition Field(s) | closed_at (MONTH partition) | | Clustered Field(s) | last_modified_ledger, contract_id | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.contract_data) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | contract_id | Soroban contract id | string | | Yes | | | contract_key_type | Contract key type which is an ScVal that can have the following values | string | | | | | contract_durability | Contract can either be temporary or persistent | string | | | | | asset_code | The 4 or 12 character code representation of the asset on the network. | string | | | | | asset_issuer | The account address of the original asset issuer that created the asset. | string | | | | | asset_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | | | | | balance_holder | The address/account that holds the balance of the asset in contract data | string | | | | | balance | The number of units of XLM held by the account | string | | | | | last_modified_ledger | The ledger sequence number when the ledger entry (this unique signer for the account) was modified. Deletions do not count as a modification and will report the prior modification sequence number | integer | | Yes | | | ledger_entry_change | Code that describes the ledger entry change type that was applied to the ledger entry. | integer | | Yes | | | deleted | Indicates whether the ledger entry (balance id) has been deleted or not. Once an entry is deleted, it cannot be recovered. | boolean | | Yes | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | | ledger_key_hash | Ledger key hash used to identify expiring contract data or contract code ledger entries | string | | Yes | | | key | The encoded key used to identify a specific piece of contract data. The encoded key has two components (type and value) where type describes the data type and the value describes the encoded value of the data type for the contract data | json | | | | | key_decoded | The human-readable or decoded version of the key. This provides an understandable format of the key, making it easier to interpret and use in analysis | json | | | | | val | The encoded value associated with the key in the contract data. The encoded val has two components (type and value) where type describes the data type and the value describes the encoded value of the data type for the contract data | json | | | | | val_decoded | The human-readable or decoded version of the value. This provides a clear and understandable representation of the value, making it easier to interpret and use in analysis | json | | | | | contract_data_xdr | The XDR (External Data Representation) encoding of the contract data. XDR is a standard format used to serialize and deserialize data, ensuring interoperability across different systems. This field contains the raw, serialized contract data in XDR format | string | | | | --- ## Evicted Keys ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | ledger_key_hash, closed_at | | Partition Field(s) | N/A | | Clustered Field(s) | N/A | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.evicted_keys) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | ledger_key_hash | Ledger key hash used to identify expiring contract data or contract code ledger entries | string | | Yes | | | is_evicted | If true, the ledger key hash has been evicted | boolean | | | closed_at | The UNIX timestamp of the sequence number's age | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | --- ## History Assets ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | asset_code, asset_issuer, asset_type | | Partition Field(s) | batch_run_date (MONTH partition) | | Clustered Field(s) | asset_code, asset_issuer, asset_type | | Documentation | [dbt_docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.history_assets_staging) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | id | Unique identifier for the asset code, type and issuer combination. This is not a primary key on the table | float | | Yes | Deprecated | | asset_id | Unique identifier for asset_code, asset_issuer | integer | | No | | | asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | Yes | XLM is the native asset to the network. XLM has no asset code or issuer representation and will instead be displayed with an asset type of 'native' | | asset_code | The 4 or 12 character code representation of the asset on the network | string | | No | Asset codes have no guarantees of uniqueness. The combination of asset code, issuer and type represents a distinct asset | | asset_issuer | The account address of the original asset issuer that created the asset | string | | No | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | The table is partitioned on batch_run_date. It is recommended to always include the batch_run_date in the filter if possible to help reduce query cost. | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | --- ## History Contract Events ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | | | Partition Field(s) | closed_at (MONTH partition) | | Clustered Field(s) | contract_id | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.history_contract_events) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | transaction_hash | A hex-encoded SHA-256 hash of this transaction's XDR-encoded form | string | | | | | transaction_id | The transaction identifier in which the operation executed. There can be up to 100 operations in a given transaction | integer | | Yes | | | successful | Indicates if this transaction was successful or not | boolean | | No | A transaction's success does not indicate whether it was included and written to a ledger. It only indicates whether the operations in the transaction were successfully applied to mutate the ledger state. | | in_successful_contract_call | Indicates whether or not the event is in a successful contract call | boolean | | Yes | | | contract_id | | string | | | | | type | The numeric event type | integer | 0, 1, 2 | Yes | | | type_string | The string event type | string | ContractEventTypeSystem, ContractEventTypeDiagnostic, ContractEventTypeContract | Yes | | | topics | The topics part of an event contains identifying information and metadata for the event and generally what the event signifies. For example, for SAC this could be a "transfer" event signifying token value movement. The values within topics are base64 encoded XDR | json | | Yes | | | topics_decoded | Decoded, human readable version of the event topic | json | | Yes | | | data | The data part of an event is an object that contains the value(s) significant to an event. For example, for SAC this could be the "amount" of the token movement from a "transfer" event. The values within topics are base64 encoded XDR | json | | Yes | | | data_decoded | Decoded, human readable version of the event data object | json | | Yes | | | contract_event_xdr | The base64 encoded XDR of the event | string | | Yes | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of "scheduled\_\_[batch_end_date]-[dag_alias]". Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of the ledger that this transaction was included in | integer | | Yes | | | operation_id | Unique identifier for an operation. | integer | | No | | --- ## History Effects | Name | Description | Data Type | Domain Values | Primary Key? | Natural Key? | Partition or Cluster Field? | Required? | Notes | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | address | The address of the account. The address is the account's public key encoded in base32. All account addresses start with a \`G\` | string | | | | cluster | | | | address_muxed | Address multiplexed | string | | | | | | | | operation_id | Unique identifier for an operation | integer | | | | cluster | | | | type | The number indicating which type of effect | integer | | | | cluster | | | | type_string | The string indicating which type of effect | string | | | | | | | | details | Unstructured JSON object that contains details based on the type of effect. Each effect will return its own relevant details, with the rest of the details as null | record | | | | | | | | details.liquidity_pool | Liquidity pools provide a simple, non-interactive way to trade large amounts of capital and enable high volumes of trading | record | | | | | | | | details.liquidity_pool.fee_bp | The number of basis points charged as a percentage of the trade in order to complete the transaction. The fees earned on all trades are divided amongst pool shareholders and distributed as an incentive to keep money in the pools | integer | | | | | | | | details.liquidity_pool.id | Unique identifier for a liquidity pool. There cannot be duplicate pools for the same asset pair. Once a pool has been created for the asset pair, another cannot be created. | string | | | | | | | | details.liquidity_pool.total_shares | Total number of pool shares issued | numeric | | | | | | | | details.liquidity_pool.total_trustlines | Number of trustlines for the associated pool shares | integer | | | | | | | | details.liquidity_pool.type | The mechanism that calculates pricing and division of shares for the pool. With the initial AMM rollout, the only type of liquidity pool allowed to be created is a constant product pool. | string | | | | | | | | details.liquidity_pool.reserves | Reserved asset in liquidity pool | record | | | | | | | | details.liquidity_pool.reserves.asset | Reserve asset | string | | | | | | | | details.liquidity_pool.reserves.amount | Reserve asset amount | numeric | | | | | | | | details.reserves_received | Asset amount received for reserves from liquidity pool withdraw | record | | | | | | | | details.reserves_received.asset | Recieved asset | string | | | | | | | | details.reserves_received.amount | Recieved asset amount | numeric | | | | | | | | details.reserves_deposited | Asset amount deposited for reserves from liquidity pool deposit | record | | | | | | | | details.reserves_deposited.asset | Deposited asset | string | | | | | | | | details.reserves_deposited.amount | Deposited asset amount | numeric | | | | | | | | details.reserves_revoked | Asset amount revoked for reserves from liquidity pool revoke | record | | | | | | | | details.reserves_revoked.asset | Revoked asset | string | | | | | | | | details.reserves_revoked.amount | Revoked asset amount | numeric | | | | | | | | details.reserves_revoked.claimable_balance_id | Claimable balance id | string | | | | | | | | details.bought | Asset bought from trade | record | | | | | | | | details.bought.asset | Asset bought | string | | | | | | | | details.bought.amount | Asset amount bought | numeric | | | | | | | | details.sold | Asset sold from trade | record | | | | | | | | details.sold.asset | Asset sold | string | | | | | | | | details.sold.amount | Asset amount sold | numeric | | | | | | | | details.shares_revoked | Shares revoked from liquidity pool revoke | numeric | | | | | | | | details.shares_received | Shares received from liquidity pool deposit | numeric | | | | | | | | details.shares_redeemed | Shares redeemed from liquidity pool withrdaw | numeric | | | | | | | | details.liquidity_pool_id | Unique identifier for a liquidity pool | string | | | | | | | | details.balance_id | The unique identifier of the claimable balance. The id is comprised of 8 character type code + SHA-256 hash of the history operation id that created the balance. The balance id can be joined back to the \`claimable_balances\` table to gather more details about the balance | string | | | | | | | | details.new_seq | New sequence number after bump sequence | integer | | | | | | | | details.name | The manage data operation allows an account to write and store data directly on the ledger in a key value pair format. The name is the key for a data entry. | string | | | | | | | | details.value | Value of data from manage data effect | string | | | | | | | | details.trustor | Account address of trustor | string | | | | | | | | details.limit | The upper bound amount of an asset that an account can hold | numeric | | | | | | | | details.inflation_destination | Inflation destination account id | string | | | | | | | | details.authorized_flag | Auth value for set trustline flags | boolean | | | | | | | | details.auth_immutable_flag | Auth value for set trustline flags | boolean | | | | | | | | details.authorized_to_maintain_liabilites | Auth value for set trustline flags | boolean | | | | | | | | details.auth_revocable_flag | Auth value for set trustline flags | boolean | | | | | | | | details.auth_required_flag | Auth value for set trustline flags | boolean | | | | | | | | details.auth_clawback_enabled_flag | Auth value for set trustline flags | boolean | | | | | | | | details.claimable_balance_clawback_enabled_flag | Auth value for set trustline flags | boolean | | | | | | | | details.clawback_enabled_flag | Auth value for set trustline flags | boolean | | | | | | | | details.high_threshold | The sum of the weight of all signatures that sign a transaction for the high threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | | | | | | | details.med_threshold | The sum of the weight of all signatures that sign a transaction for the medium threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | | | | | | | details.low_threshold | The sum of the weight of all signatures that sign a transaction for the low threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | | | | | | | details.home_domain | The home domain used for the stellar.toml file discovery | string | | | | | | | | details.asset_issuer | The account address of the original asset issuer that created the asset | string | | | | | | | | details.asset | Asset on network | string | | | | | | | | details.asset_code | The 4 or 12 character code representation of the asset on the network | string | | | | | | | | details.asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | | | | | | | | details.signer | The address of the account that is allowed to authorize (sign) transactions for another account. This process is called multi-sig | string | | | | | | | | details.sponsor | The account address of the sponsor who is paying the reserves for this signer | string | | | | | | | | details.new_sponsor | The new account address of the sponsor who is paying the reserves for this signer | string | | | | | | | | details.former_sponsor | The former account address of the sponsor who is paying the reserves for this signer | string | | | | | | | | details.weight | Signer weight | integer | | | | | | | | details.public_key | Signer public key | string | | | | | | | | details.amount | Asset amount | numeric | | | | | | | | details.starting_balance | Account asset starting balance | numeric | | | | | | | | details.seller | Selling account | string | | | | | | | | details.seller_muxed | Account multiplexed | string | | | | | | | | details.seller_muxed_id | Account multiplexed id | integer | | | | | | | | details.offer_id | The unique id for the offer. This id can be joined with the \`offers\` table | integer | | | | | | | | details.sold_amount | Amount of asset sold | numeric | | | | | | | | details.sold_asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | | | | | | | | details.sold_asset_code | The 4 or 12 character code representation of the asset on the network | string | | | | | | | | details.sold_asset_issuer | The account address of the original asset issuer that created the asset | string | | | | | | | | details.bought_asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | | | | | | | | details.bought_asset_code | The 4 or 12 character code representation of the asset on the network | string | | | | | | | | details.bought_asset_issuer | The account address of the original asset issuer that created the asset | string | | | | | | | | details.bought_amount | Amount of asset bought | numeric | | | | | | | | details.data_name | Ledger entry data name | string | | | | | | | | details.predicate | The condition which must be satisfied so the destination can claim the balance. The predicate can include logical rules using AND, OR and NOT logic. | record | | | | | | | | details.predicate.not | | record | | | | | | | | details.predicate.not.abs_before | | string | | | | | | | | details.predicate.not.rel_before | | integer | | | | | | | | details.predicate.not.unconditional | | boolean | | | | | | | | details.predicate.not.and | | record | | | | | | | | details.predicate.not.and.abs_before | | string | | | | | | | | details.predicate.not.and.rel_before | | integer | | | | | | | | details.predicate.not.and.unconditional | | boolean | | | | | | | | details.predicate.not.and.abs_before_epoch | | integer | | | | | | | | details.predicate.not.or | | record | | | | | | | | details.predicate.not.or.abs_before | | string | | | | | | | | details.predicate.not.or.rel_before | | integer | | | | | | | | details.predicate.not.or.unconditional | | boolean | | | | | | | | details.predicate.not.or.abs_before_epoch | | integer | | | | | | | | details.predicate.not.not | | record | | | | | | | | details.predicate.not.not.abs_before | | string | | | | | | | | details.predicate.not.not.rel_before | | integer | | | | | | | | details.predicate.not.not.unconditional | | boolean | | | | | | | | details.predicate.not.not.abs_before_epoch | | integer | | | | | | | | details.predicate.not.abs_before_epoch | | integer | | | | | | | | details.predicate.type | | integer | | | | | | | | details.predicate.and | | record | | | | | | | | details.predicate.and.abs_before | | string | | | | | | | | details.predicate.and.rel_before | | integer | | | | | | | | details.predicate.and.unconditional | | boolean | | | | | | | | details.predicate.and.and | | record | | | | | | | | details.predicate.and.and.abs_before | | string | | | | | | | | details.predicate.and.and.rel_before | | integer | | | | | | | | details.predicate.and.and.unconditional | | boolean | | | | | | | | details.predicate.and.and.abs_before_epoch | | integer | | | | | | | | details.predicate.and.or | | record | | | | | | | | details.predicate.and.or.abs_before | | string | | | | | | | | details.predicate.and.or.rel_before | | integer | | | | | | | | details.predicate.and.or.unconditional | | boolean | | | | | | | | details.predicate.and.or.abs_before_epoch | | integer | | | | | | | | details.predicate.and.not | | record | | | | | | | | details.predicate.and.not.abs_before | | string | | | | | | | | details.predicate.and.not.rel_before | | integer | | | | | | | | details.predicate.and.not.unconditional | | boolean | | | | | | | | details.predicate.and.not.abs_before_epoch | | integer | | | | | | | | details.predicate.and.abs_before_epoch | | integer | | | | | | | | details.predicate.or | | record | | | | | | | | details.predicate.or.abs_before | | string | | | | | | | | details.predicate.or.rel_before | | integer | | | | | | | | details.predicate.or.unconditional | | boolean | | | | | | | | details.predicate.or.and | | record | | | | | | | | details.predicate.or.and.abs_before | | string | | | | | | | | details.predicate.or.and.rel_before | | integer | | | | | | | | details.predicate.or.and.unconditional | | boolean | | | | | | | | details.predicate.or.and.not | | record | | | | | | | | details.predicate.or.and.not.abs_before | | string | | | | | | | | details.predicate.or.and.not.rel_before | | integer | | | | | | | | details.predicate.or.and.not.unconditional | | boolean | | | | | | | | details.predicate.or.and.not.abs_before_epoch | | integer | | | | | | | | details.predicate.or.and.abs_before_epoch | | integer | | | | | | | | details.predicate.or.or | | record | | | | | | | | details.predicate.or.or.abs_before | | string | | | | | | | | details.predicate.or.or.rel_before | | integer | | | | | | | | details.predicate.or.or.unconditional | | boolean | | | | | | | | details.predicate.or.or.abs_before_epoch | | integer | | | | | | | | details.predicate.or.not | | record | | | | | | | | details.predicate.or.not.abs_before | | string | | | | | | | | details.predicate.or.not.rel_before | | integer | | | | | | | | details.predicate.or.not.unconditional | | boolean | | | | | | | | details.predicate.or.not.abs_before_epoch | | integer | | | | | | | | details.predicate.or.abs_before_epoch | | integer | | | | | | | | details.predicate.abs_before | Deadline for when the balance must be claimed. If a balance is claimed before the date then the clause of the condition is satisfied. | string | | | | | | | | details.predicate.rel_before | A relative deadline for when the claimable balance can be claimed. The value represents the number of seconds since the close time of the ledger which created the claimable balance \#### Notes: This condition is useful when creating a timebounds based on creation conditions. If the creator wanted a balance only claimable one week after creation, this condition would satisfy that rule. | integer | | | | | | | | details.predicate.unconditional | If true it means this clause of the condition is always satisfied. \#### Notes: When the predicate is only unconditional = true, it means that the balance can be claimed under any conditions | boolean | | | | | | | | details.predicate.abs_before_epoch | A UNIX epoch value in seconds representing the same deadline date as abs_before. | integer | | | | | | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | | | | | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | | | MONTH partition | | | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | | | | | | ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | id | | Partition Field(s) | batch_run_date (MONTH partition) | | Clustered Field(s) | address, operation_id, type | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.history_effects) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | id | This unique effect id is the concatenation of the operation id and the effect index | string | | Yes | | | index | The index of the effect within the transaction. This index helps to order effects that result from the same transaction, indicating the sequence in which they occurred. | integer | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | | address | The address of the account. The address is the account's public key encoded in base32. All account addresses start with a `G` | string | | | | | address_muxed | Address multiplexed | string | | | | | operation_id | Unique identifier for an operation | integer | | | | | type | The number indicating which type of effect | integer | | | | | type_string | The string indicating which type of effect | string | | | | | details | Unstructured JSON object that contains details based on the type of effect. Each effect will return its own relevant details, with the rest of the details as null | record | | | | | details.liquidity_pool | Liquidity pools provide a simple, non-interactive way to trade large amounts of capital and enable high volumes of trading | record | | | | | details.liquidity_pool.fee_bp | The number of basis points charged as a percentage of the trade in order to complete the transaction. The fees earned on all trades are divided amongst pool shareholders and distributed as an incentive to keep money in the pools | integer | | | | | details.liquidity_pool.id | Unique identifier for a liquidity pool. There cannot be duplicate pools for the same asset pair. Once a pool has been created for the asset pair, another cannot be created. | string | | | | | details.liquidity_pool.total_shares | Total number of pool shares issued | numeric | | | | | details.liquidity_pool.total_trustlines | Number of trustlines for the associated pool shares | integer | | | | | details.liquidity_pool.type | The mechanism that calculates pricing and division of shares for the pool. With the initial AMM rollout, the only type of liquidity pool allowed to be created is a constant product pool. | string | | | | | details.liquidity_pool.reserves | Reserved asset in liquidity pool | record | | | | | details.liquidity_pool.reserves.asset | Reserve asset | string | | | | | details.liquidity_pool.reserves.amount | Reserve asset amount | numeric | | | | | details.reserves_received | Asset amount received for reserves from liquidity pool withdraw | record | | | | | details.reserves_received.asset | Received asset | string | | | | | details.reserves_received.amount | Received asset amount | numeric | | | | | details.reserves_deposited | Asset amount deposited for reserves from liquidity pool deposit | record | | | | | details.reserves_deposited.asset | Deposited asset | string | | | | | details.reserves_deposited.amount | Deposited asset amount | numeric | | | | | details.reserves_revoked | Asset amount revoked for reserves from liquidity pool revoke | record | | | | | details.reserves_revoked.asset | Revoked asset | string | | | | | details.reserves_revoked.amount | Revoked asset amount | numeric | | | | | details.reserves_revoked.claimable_balance_id | Claimable balance id | string | | | | | details.bought | Asset bought from trade | record | | | | | details.bought.asset | Asset bought | string | | | | | details.bought.amount | Asset amount bought | numeric | | | | | details.sold | Asset sold from trade | record | | | | | details.sold.asset | Asset sold | string | | | | | details.sold.amount | Asset amount sold | numeric | | | | | details.shares_revoked | Shares revoked from liquidity pool revoke | numeric | | | | | details.shares_received | Shares received from liquidity pool deposit | numeric | | | | | details.shares_redeemed | Shares redeemed from liquidity pool withdraw | numeric | | | | | details.liquidity_pool_id | Unique identifier for a liquidity pool | string | | | | | details.balance_id | The unique identifier of the claimable balance. The id is comprised of 8 character type code + SHA-256 hash of the history operation id that created the balance. The balance id can be joined back to the `claimable_balances` table to gather more details about the balance | string | | | | | details.new_seq | New sequence number after bump sequence | integer | | | | | details.name | The manage data operation allows an account to write and store data directly on the ledger in a key value pair format. The name is the key for a data entry. | string | | | | | details.value | Value of data from manage data effect | string | | | | | details.trustor | Account address of trustor | string | | | | | details.limit | The upper bound amount of an asset that an account can hold | numeric | | | | | details.inflation_destination | Inflation destination account id | string | | | | | details.authorized_flag | Auth value for set trustline flags | boolean | | | | | details.auth_immutable_flag | Auth value for set trustline flags | boolean | | | | | details.authorized_to_maintain_liabilities | Auth value for set trustline flags | boolean | | | | | details.auth_revocable_flag | Auth value for set trustline flags | boolean | | | | | details.auth_required_flag | Auth value for set trustline flags | boolean | | | | | details.auth_clawback_enabled_flag | Auth value for set trustline flags | boolean | | | | | details.claimable_balance_clawback_enabled_flag | Auth value for set trustline flags | boolean | | | | | details.clawback_enabled_flag | Auth value for set trustline flags | boolean | | | | | details.high_threshold | The sum of the weight of all signatures that sign a transaction for the high threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | | | | details.med_threshold | The sum of the weight of all signatures that sign a transaction for the medium threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | | | | details.low_threshold | The sum of the weight of all signatures that sign a transaction for the low threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | | | | details.home_domain | The home domain used for the stellar.toml file discovery | string | | | | | details.asset_issuer | The account address of the original asset issuer that created the asset | string | | | | | details.asset | Asset on network | string | | | | | details.asset_code | The 4 or 12 character code representation of the asset on the network | string | | | | | details.asset_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | | | | | details.signer | The address of the account that is allowed to authorize (sign) transactions for another account. This process is called multi-sig | string | | | | | details.sponsor | The account address of the sponsor who is paying the reserves for this signer | string | | | | | details.new_sponsor | The new account address of the sponsor who is paying the reserves for this signer | string | | | | | details.former_sponsor | The former account address of the sponsor who is paying the reserves for this signer | string | | | | | details.weight | Signer weight | integer | | | | | details.public_key | Signer public key | string | | | | | details.amount | Asset amount | numeric | | | | | details.starting_balance | Account asset starting balance | numeric | | | | | details.seller | Selling account | string | | | | | details.seller_muxed | Account multiplexed | string | | | | | details.seller_muxed_id | Account multiplexed id | integer | | | | | details.offer_id | The unique id for the offer. This id can be joined with the `offers` table | integer | | | | | details.sold_amount | Amount of asset sold | numeric | | | | | details.sold_asset_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | | | | | details.sold_asset_code | The 4 or 12 character code representation of the asset on the network | string | | | | | details.sold_asset_issuer | The account address of the original asset issuer that created the asset | string | | | | | details.bought_asset_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | | | | | details.bought_asset_code | The 4 or 12 character code representation of the asset on the network | string | | | | | details.bought_asset_issuer | The account address of the original asset issuer that created the asset | string | | | | | details.bought_amount | Amount of asset bought | numeric | | | | | details.data_name | Ledger entry data name | string | | | | | details.predicate | The condition which must be satisfied so the destination can claim the balance. The predicate can include logical rules using AND, OR and NOT logic. | record | | | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. | datetime | | | | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. | timestamp | | | | --- ## History Ledgers ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | sequence | | Partition Field(s) | closed_at (MONTH partition) | | Clustered Field(s) | sequence, closed_at | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.history_ledgers) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | | --- | --- | --- | --- | --- | --- | --- | | sequence | The sequence number that corresponds to the individual ledgers. As ledgers are written to the network, the sequence is incremented by 1 | integer | | Yes | | | ledger_hash | The hex-encoded SHA-256 hash that represents the ledger's XDR-encoded form | string | | Yes | | | previous_ledger_hash | The hex-encoded SHA-256 hash of the ledger that immediately precedes this ledger | string | | No | | | transaction_count | The number of successful transactions submitted and completed by the network in this ledger | integer | | Yes | Defaults to 0 | | operation_count | The total number of successful operations applied to this ledger | integer | | Yes | Defaults to 0 | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | id | Unique identifier for the ledger | integer | | Yes | | | total_coins | Total number of lumens in circulation | integer | | Yes | | | fee_pool | The cumulative sum of all transaction fees (in stroops) ever collected by the network as of this ledger's close. This is a running balance stored in the ledger header, not the fees for a single ledger. | integer | | Yes | | | base_fee | The fee (in stroops) the network charges per operation in a transaction for the given ledger. The minimum base fee is 100, with the ability to increase if transaction demand exceeds ledger capacity. When this occurs, the ledger enters surge pricing | integer | | Yes | The stroop is the fractional representation of a lumen (XLM). 1 stroop is 0.0000001 XLM. | | base_reserve | The reserve (in stroops) the network requires an account to retain as a minimum balance in order to be a valid account on the network. The current minimum reserve is 10 XLM | integer | 5000000 100000000 | Yes | The stroop is the fractional representation of a lumen (XLM). 1 stroop is 0.0000001 XLM. | | max_tx_set_size | The maximum number of operations that Stellar validator nodes have agreed to process in a given ledger. Since Protocol 11, ledger capacity has been measured in operations rather than transactions | integer | 50 - original max 500 1000 - current max | Yes | | | protocol_version | The protocol verstion that the Stellar network was running when this ledger was committed. Protocol versions are released ~every 6 months | integer | integers 1 - 19 (will increment) | Yes | Defaults to 0 | | ledger_header | A base64-encoded string of the raw LedgerHeader xdr struct for this ledger | bytes | | No | | | successful_transaction_count | The number of successful transactions submitted and completed by the network in this ledger | integer | | No | | | failed_transaction_count | The number of failed transactions submitted to the network in this ledger. The transaction was still paid for but contained an error that prevented it from executing | integer | | No | | | tx_set_operation_count | The total number of operations in the transaction set for this ledger, including failed transactions. | integer | | No | Transactions on Stellar are atomic. If one of the operations within a transaction set fails, the entire transaction will failed, including any other operations. | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | The table is partitioned on batch_run_date. It is recommended to always include the batch_run_date in the filter if possible to help reduce query cost. | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | soroban_fee_write_1kb | Soroban write fee costs | integer | | No | | | signature | The signing hash of the validator node which writes the transaction set to the network.This signature ensures the integrity and authenticity of the ledger, confirming that it has not been tampered with | string | | Yes | | | node_id | The id of winning validator node which is allowed to write transaction set to the network. The winning validator is decided by the network | string | | Yes | | | total_byte_size_of_bucket_list | The size, in bytes, of the Bucketlist DB for the Stellar Network | integer | | Yes | | | evicted_ledger_keys_type | The list of type of keys which have been evicted in a given ledger entry. | string | | No | | | evicted_ledger_keys_hash | The list of keys hash which have been evicted in a given ledger entry. | string | | | No | | --- ## History Operations ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | id | | Partition Field(s) | batch_run_date (MONTH partition) | | Clustered Field(s) | transaction_id, source_account, type | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.history_operations) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | id | Unique identifier for an operation | integer | | Yes | The operation id is the transaction id + order number | | source_account | The account address that originates the operation | string | | Yes | Defaults to '' | | source_account_muxed | If an account is multiplexed (muxed), the virtual account address that originates the operation | string | | No | | | transaction_id | The transaction identifier in which the operation executed. There can be up to 100 operations in a given transaction | integer | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | | type | The number indicating which type of operation this operation executes | integer | 0 - Create Account1 - Payment2 - Path Payment Strict Receive3 - Manage Sell Offer4 - Create Passive Sell Offer5 - Set Options6 - Change Trust7 - Allow Trust8 - Account Merge9 - Inflation10 - Manage Data11 - Bump Sequence12 - Manage Buy Offer13 - Path Payment Strict Send14 - Create Claimable Balance15 - Claim Claimable Balance16 - Begin Sponsoring Future Reserves17 - End Sponsoring Future Reserves18 - Revoke Sponsorship19 - Clawback20 - Clawback Claimable Balance21 - Set Trust Line Flags22 - Liquidity Pool Deposit23 - Liquidity Pool Withdraw | Yes | | | type_string | The string indicating which type of operation this operation executes | string | | Yes | | | details | Unstructured JSON object that contains details based on the type of operation executed | blob | see details below (E10:E108) | No | Bigquery does not have a JSON field type (currently in pre-GA pilot only) so this field is a structured, sparse, record field instead. In the upstream data, this field is a true json blob | | details.account | The new resulting account address that is created and funded (create operation) The account address that is being removed and merged into another account (merge operation) | string | | No | | | details.account_muxed | The virtual address of the account if the account is multiplexed | string | | No | | | details.account_muxed_id | Integer representation of the virtual address of the account if the account is multiplexed | integer | | No | | | details.account_id | The address of the account which is no longer sponsored | string | | No | | | details.amount | Float representation of the amount of an asset sent/offered/etc | float | | No | | | details.asset | The asset available to be claimed in the form of "asset_code:issuing_address". If the claimable balance is in XLM, it is reported as "native" | string | | No | | | details.asset_code | The 4 or 12 character code representation of the asset on the network | string | 1 - Payment2 - Path Payment Strict Receive6 - Change Trust7 - Allow Trust13 - Path Payment Strict Send19 - Clawback21 - Set Trust Line Flags | No | Asset codes have no guarantees of uniqueness. The combination of asset code, issuer and type represents a distinct asset | | details.asset_issuer | The account address of the original asset issuer that created the asset | string | 1 - Payment2 - Path Payment Strict Receive6 - Change Trust7 - Allow Trust13 - Path Payment Strict Send19 - Clawback21 - Set Trust Line Flags | No | | | details.asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | No | XLM is the native asset to the network. XLM has no asset code or issuer representation and will instead be displayed with an asset type of 'native' | | details.authorize | Indicates whether the trustline is authorized. 0 is the account is not authorized to transact with the asset in any way. 1 if the account is authorized to transact with the asset. 2 if the account is authorized to maintain orders, but not to perform other transactions. | boolean | | No | | | details.balance_id | The unique identifier of the claimable balance. The id is comprised of 8 character type code + SHA-256 hash of the history operation id that created the balance. | string | | No | | | details.buying_asset_code | The 4 or 12 character code representation of the asset that is either bought or offered to buy in a trade | string | | No | | | details.buying_asset_issuer | The account address of the original asset issuer that created the asset bought or offered to buy | string | | No | | | details.buying_asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | No | XLM is the native asset to the network. XLM has no asset code or issuer representation and will instead be displayed with an asset type of 'native' | | details.claimable_balance_id | The balance id of the claimable balance which is no longer sponsored | string | | No | | | details.claimant | The account address of the account which claimed the claimable balance | string | | No | | | details.claimant_muxed | If the account is multiplexed, the virtual address of the account which claimed the claimable balance | string | | No | | | details.claimant_muxed_id | If the account is multiplexed, an integer representation of the muxed account which claimed the balance | integer | | No | | | details.claimants | An unstructured field that lists account addresses eligible to claim a balance and the conditions which must be satisfied to claim the balance (typically time bound conditions) | array[record] | | No | | | details.data_account_id | The account address of the account whose data entry is no longer sponsored | string | | No | | | details.data_name | The name of the data entry which is no longer sponsored | string | | No | | | details.from | The account address from which the payment originates (the sender account) | string | | No | | | details.from_muxed | If the account is multiplexed, the virtual address of the sender account | string | | No | | | details.from_muxed_id | If the account is multiplexed, the integer representation of the virtual address of the sender account | integer | | No | | | details.funder | When a new account is created, an account address "funds" the new account | string | | No | | | details.funder_muxed | If the account is multiplexed, the virtual address of the account funding the new account | string | | No | | | details.funder_muxed_id | If the account is multiplexed, the integer representation of the virtual address of the funding account | integer | | No | | | details.high_threshold | The sum of the weight of all signatures that sign a transaction for the high threshold operation | integer | | No | Each operation falls under a specific threshold category: low, medium, or high. Thresholds define the level of privilege an operation needs in order to succeed. Low Security: Allow Trust, Set Trust Line Flags, Bump Sequence and Claim Claimable Balance; Medium Security: Everything Else; High Security: Account Merge, Set Options | | details.home_domain | The home domain used for the stellar.toml file discovery | string | | No | | | details.inflation_dest | The account address specifying where to send inflation funds. The concept of inflation on the network has been discontinued | string | | No | Inflation was retired from the network in 2019. | | details.into | The account address receiving the deleted account's lumens. This is the account in which the intended deleted account will be merged | string | | No | | | details.into_muxed | If the account is multiplexed, the virtual address of the account receiving the deleted account's lumens | string | | No | | | details.into_muxed_id | If the account is multiplexed, the integer representation of the account receiving the deleted account's lumens | integer | | No | | | details.limit | The upper bound amount of an asset that an account can hold | float | | No | | | details.low_threshold | The sum of the weight of all signatures that sign a transaction for the low threshold operation | integer | | No | Each operation falls under a specific threshold category: low, medium, or high. Thresholds define the level of privilege an operation needs in order to succeed. Low Security: Allow Trust, Set Trust Line Flags, Bump Sequence and Claim Claimable Balance; Medium Security: Everything Else; High Security: Account Merge, Set Options | | details.master_key_weight | An account's private key is called the master key. For signing transactions, the account holder can specify a weight for the master key, which contributes to thresholds validation when processing a transaction | integer | Integers from 1 to 255 | No | Defaults to 1 | | details.med_threshold | The sum of the weight of all signatures that sign a transaction for the medium threshold operation | integer | | No | Each operation falls under a specific threshold category: low, medium, or high. Thresholds define the level of privilege an operation needs in order to succeed. Low Security: Allow Trust, Set Trust Line Flags, Bump Sequence and Claim Claimable Balance; Medium Security: Everything Else; High Security: Account Merge, Set Options | | details.name | The manage data operation allows an account to write and store data directly on the ledger in a key-value pair format. The name is the key for a data entry | string | | No | If the name is new, the manage data operation will add the given name/value pair to the account. If the name is already present, the associated value will be modified. | | details.offer_id | The unique id for the offer. This id can be joined with the `offers` table | integer | | No | | | details.path | Path payments maximize the best exchange rate path when sending money from one asset to another asset. The intermediary assets that this path hops through will be reported in the record. This feature is especially useful when the market between the original asset pair is illiquid | record | | No | Up to 6 paths are permitted for a single payment. Example: sending EUR -> MXN could look like EUR -> BTC -> CNY -> XLM -> MXN to maximize the best exchange rate. Payments are atomic, so if an exchange in the middle of a path payment fails, the entire payment will fail, meaning the user will keep their original funds. They will not be stuck with an intermediary asset in the event of payment failure. | | details.price | The ratio of selling asset to buying asset. This is a number representing how many units of a selling asset it takes to get 1 unit of a buying asset | array[float] | | No | | | details.price_r | Precise representation of the buy and sell price of the assets on an offer. The n is the numerator, the d is the denominator. By calculating the ratio of n/d you can calculate the price of the bid or ask | record | | No | | | details.selling_asset_code | The 4 or 12 character code representation of the asset that is either sold or offered to sell in a trade | string | | No | | | details.selling_asset_issuer | The account address of the original asset issuer that created the asset sold or offered to sell | string | | No | | | details.selling_asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | No | XLM is the native asset to the network. XLM has no asset code or issuer representation and will instead be displayed with an asset type of 'native' | | details.set_flags | Array of numeric values of the flags set for a given trustline in the operation | array[integer] | 1 - Auth Required2 - Auth Revocable4 - Auth Immutable | No | | | details.set_flags_s | Array of string values of the flags set for a given trustline in the operation | array[string] | Auth RequiredAuth RevocableAuth Immutable | No | | | details.signer_account_id | The address of the account of the signer no longer sponsored | string | | No | | | details.signer_key | The address of the signer which is no longer sponsored | string | | No | | | details.signer_weight | The weight of the new signer. For transactions, multiple accounts can sign a transaction from a source account. This weight contributes towards calculating whether the transaction exceeds the specified threshold weight to complete the transaction | integer | Integers from 1 to 255 | No | | | details.source_amount | The originating amount sent designated in the source asset | float | | No | | | details.source_asset_code | The 4 or 12 character code representation of the asset that is originally sent | string | | No | | | details.source_asset_issuer | The account address of the original asset issuer that created the asset sent | string | | No | | | details.source_asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | No | XLM is the native asset to the network. XLM has no asset code or issuer representation and will instead be displayed with an asset type of 'native' | | details.source_max | The maximum amount to be sent, designated in the source asset | float | | No | Exchanging an asset causes a small amount of the asset value to be spent in fees and exchange rates. The sender can specify a maximum amount they are willing to send if the rates between the asset pair are unfavorable. | | details.starting_balance | The amount of XLM to send to the newly created account. The account starting balance will need to exceed the minimum balance necessary to hold an account on the Stellar Network | float | | No | | | details.to | The address of the account receiving the payment funds | string | | No | | | details.to_muxed | If the account is multiplexed, the virtual address of the account receiving the payment | string | | No | | | details.to_muxed_id | If the account is multiplexed, the integer representation of the virtual address of the recipient account | integer | | No | | | details.trustee | The issuing account address (only present for `credit` asset types) | string | | No | | | details.trustee_muxed | If the issuing account address is multiplexed, the virtual address | string | | No | | | details.trustee_muxed_id | If the issuing account address is multiplexed, the integer representation of the virtual address | integer | | No | | | details.trustline_account_id | The address of the account whose trustline is no longer sponsored | string | | No | | | details.trustline_asset | The asset of the trustline which is no longer sponsored | string | | No | A sponsor can determine they want to revoke sponsorship of certain assets but maintain the sponsorship of other assets | | details.trustor | The trusting account address, or the account being authorized or unauthorized | string | | No | | | details.trustor_muxed | If the trusting account is multiplexed, the virtual address of the account | string | | No | | | details.trustor_muxed_id | If the trusting account is multiplexed, the integer representation of the virtual address | integer | | No | | | details.value | The manage data operation allows an account to write and store data directly on the ledger in a key-value pair format. The value is the value of a key for a data entry | string | | No | | | details.clear_flags | Array of numeric values of the flags cleared for a given trustline in the operation. If the flag was originally set, this will delete the flag | array[integer] | 1 - Auth Required2 - Auth Revocable4 - Auth Immutable | No | | | details.clear_flags_s | Array of string values of the flags cleared for a given trustline in the operation. If the flag was originally set, this will delete the flag | array[string] | Auth RequiredAuth RevocableAuth Immutable | No | | | details.destination_min | The minimum amount to be received, designated in the expected destination asset | string | | No | Exchanging an asset causes a small amount of the asset value to be spent in fees and exchange rates. The sender can specify a guaranteed minimum amount they want sent to the recipient to ensure they receive a specified value. | | details.bump_to | The new desired value of the source account's sequence number | string | | No | | | details.authorize_to_maintain_liabilities | Indicates whether the trustline is authorized. 0 is the account is not authorized to transact with the asset in any way. 1 if the account is authorized to transact with the asset. 2 if the account is authorized to maintain orders, but not to perform other transactions. | boolean | | No | | | details.clawback_enabled | Indicates whether the asset can be clawed back by the asset issuer | boolean | | No | | | details.sponsor | The account address of another account that maintains the minimum balance in XLM for the source account to complete operations | string | Any Type | No | | | details.sponsored_id | The account address of the account which will be sponsored | string | | No | | | details.begin_sponsor | The account address of the account which initiated the sponsorship | string | | No | | | details.begin_sponsor_muxed | If the initiating sponsorship account is multiplexed, the virtual address | string | | No | | | details.begin_sponsor_muxed_id | If the initiating sponsorship account is multiplexed, the integer representation of the virtual address | integer | | No | | | details.liquidity_pool_id | Unique identifier for a liquidity pool | string | | No | Liquidity pools are automated money markets between an asset pair. A given pool will only ever have two assets unless there is a protocol change | | details.reserve_a_asset_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM | string | credit_alphanum4credit_alphanum12native | No | | | details.reserve_a_asset_code | The 4 or 12 character code representation of the asset of one of the two asset pairs in a liquidity pool | string | | No | | | details.reserve_a_asset_issuer | The account address of the original asset issuer that created one of the two asset pairs in the liquidity pool | string | | No | | | details.reserve_a_max_amount | The maximum amount of reserve a that can be deposited into the pool | float | | No | Deposit operations calculate via formula how much of both asset a and asset b should be deposited out of a source account and into a pool. The source account must deposit an equivalent value of both asset a and b. Since markets fluctuate, a maximum amount will specify the upper limit of an asset the account is willing to deposit | | details.reserve_a_deposit_amount | The amount of reserve a that ended up actually deposited into the pool | float | | No | | | details.reserve_b_asset_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM | string | credit_alphanum4credit_alphanum12native | No | | | details.reserve_b_asset_code | The 4 or 12 character code representation of the asset of one of the two asset pairs in a liquidity pool | string | | No | | | details.reserve_b_asset_issuer | The account address of the original asset issuer that created one of the two asset pairs in the liquidity pool | string | | No | | | details.reserve_b_max_amount | The maximum amount of reserve b that can be deposited into the pool | float | | No | Deposit operations calculate via formula how much of both asset a and asset b should be deposited out of a source account and into a pool. The source account must deposit an equivalent value of both asset a and b. Since markets fluctuate, a maximum amount will specify the upper limit of an asset the account is willing to deposit | | details.reserve_b_deposit_amount | The amount of reserve b that ended up actually deposited into the pool | float | | No | | | details.min_price | The floating point value indicating the minimum exchange rate for this deposit operation. Reported as Reserve A / Reserve B | float | | No | Market rates fluctuate for pricing and the source account can specify a minimum price they expect to receive as a ratio of the two assets in the pool | | details.min_price_r | A fractional representation of the prices of the two assets in a pool. The n is the numerator (value of asset a) and the d is the denominator (value of asset b) | array[record] | | No | | | details.max_price | The floating point value indicating the maximum exchange rate for this deposit operation. Reported as Reserve A / Reserve B | float | | No | Market rates fluctuate for pricing and the source account can specify a maximum price they expect to receive as a ratio of the two assets in the pool | | details.max_price_r | A fractional representation of the prices of the two assets in a pool. The n is the numerator (value of asset a) and the d is the denominator (value of asset b) | array[record] | | No | | | details.shares_received | A floating point number representing the number of pool shares received for this deposit. A pool share is a compilation of both asset a and asset b reserves. It is not possible to own only asset a or asset b in a pool | float | | No | | | details.reserve_a_min_amount | The minimum amount of reserve a that can be withdrawn from the pool | float | | No | | | details.reserve_a_withdraw_amount | The amount of reserve a that ended up actually withdrawn from the pool | float | | No | | | details.reserve_b_min_amount | The minimum amount of reserve b that can be withdrawn from the pool | float | | No | | | details.reserve_b_withdraw_amount | The amount of reserve b that ended up actually withdrawn from the pool | float | | No | | | details.shares | The number of shares withdrawn from the pool. It is not possible to withdraw only asset a or asset b; equal value must be withdrawn from the pool | float | | No | | | details.asset_balance_changes | The balance changes applied to an account or contract from an invoke host function. An asset must be a classic asset transferred through the [SAC](https://soroban.stellar.org/docs/tokens/stellar-asset-contract) to be included. | record | | No | | | details.asset_balance_changes.amount | The amount of token minted, transferred or burned using the SAC contract | integer | | No | | | details.asset_balance_changes.asset_code | The 4 or 12 character code representation of the asset transferred using SAC contract | string | | No | | | details.asset_balance_changes.asset_issuer | The wallet address of the account that issued the asset. This asset is a classic asset even though it is sent through SAC contract | string | | No | | | details.asset_balance_changes.asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM | string | credit_alphanum4credit_alphanum12native | No | | | details.asset_balance_changes.from | The originating wallet address or contract id from where the funds were sent | string | | No | | | details.asset_balance_changes.to | The destination wallet address or contract id where the funds go | string | | No | | | details.asset_balance_changes.type | The specific SAC operation type that indicates the type of value transfer occurring. | string | minttransferburn | No | | | details.parameters | The parameters passed to the function call for a Soroban contract. These are base64 encoded XDR. The record follows the format of `type` + `value` pair | record | | No | | | details.parameters_decoded | The decoded human-readable parameters passed to a function call for a Soroban contract. The record follows the format of `type` + `value` pair | record | | No | | | details.function | The function type invoked by the host operation | string | HostFunctionTypeHostFunctionTypeInvokeContractHostFunctionTypeHostFunctionTypeCreateContractHostFunctionTypeHostFunctionTypeUploadContractWasm | No | | | details.address | The wallet address used to create and deploy a Soroban contract instance | string | | No | | | details.type | The type of Soroban operation that is invoked within a host function | string | invoke_contractcreate_contractupload_wasmextend_footprint_ttlrestore_footprint | No | | | details.extend_to | The number of ledgers in which the Soroban ledger entry is extended | integer | | No | | | details.contract_id | The unique identifier of the deployed contract instance. Each custom Soroban contract and deployed SAC token will have a unique contract_id | string | | No | | | details.contract_code_hash | The hex-encoded SHA-256 hash that represents the contract code's XDR-encoded form | string | | No | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger | datetime | | Yes | The table is partitioned on batch_run_date. It is recommended to always include the batch_run_date in the filter if possible to help reduce query cost | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | We are aiming to repartition this table on closed_at | | operation_result_code | The result code returned when an operation is applied. This code is helpful for understanding failed operations | string | OperationResultCodeOpInnerOperationResultCodeOpBadAuthOperationResultCodeOpNoAccountOperationResultCodeOpNotSupportedOperationResultCodeOpTooManySubentriesOperationResultCodeOpExceededWorkLimitOperationResultCodeOpTooManySponsoring | Yes | Field will be backfilled at a future date | | operation_trace_code | The trace code returned when an operation is applied to the Stellar Network. This code is helpful for understanding nuanced failures by operation type. This code provides the lowest level detail regarding why a transaction fails | string | InvokeHostFunctionResultCodeInvokeHostFunctionSuccessMalformedTrappedResourceLimitExceededEntryArchivedInsufficientRefundableFeeExtendFootprintTtlResultCodeExtendFootprintTtlSuccessMalformedResourceLimitExceededInsufficientRefundableFeeRestoreFootprintResultCodeRestoreFootprintSuccessMalformedResourceLimitExceededInsufficientRefundableFee | Yes | See the XDR [documentation](https://pkg.go.dev/github.com/stellar/go-stellar-sdk/xdr#OperationResultTr) for more details | | details_json | Record that contains details based on the type of operation executed. Each operation will return its own relevant details, with the rest of the details as null | json | | | | --- ## History Trades ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | history_operation_id, order | | Partition Field(s) | ledger_closed_at (MONTH partition) | | Clustered Field(s) | selling_asset_id, buying_asset_id, trade_type | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.history_trades) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | history_operation_id | The operation id associated with the executed trade. The total amount traded in an operation can be broken up into multiple smaller trades spread across multiple orders by multiple parties | integer | | Yes | There is a many-to-one relationship for history_operation_id with the history_operations table. | | order | The sequential number assigned to the portion of a trade that is executed within an operation. The history_operation_id and order number together represent a unique trade segment | integer | | Yes | | | ledger_closed_at | The timestamp in UTC when the ledger with this trade was closed | timestamp | | Yes | | | selling_account_address | The account address of the selling party | string | | No | | | selling_asset_code | The 4 or 12 character code of the sold asset within a trade | string | | No | Asset codes have no guarantees of uniqueness. The combination of asset code, issuer, and type represents a distinct asset | | selling_asset_issuer | The account address of the original asset issuer for the sold asset within a trade | string | | No | | | selling_asset_type | The identifier for type of asset code used for the sold asset within the trade | string | credit_alphanum4credit_alphanum12native | Yes | XLM is the native asset to the network. XLM has no asset code or issuer representation and will instead be displayed with an asset type of 'native' | | selling_amount | The amount of sold asset that was moved from the seller account to the buyer account, reported in terms of the sold amount | float | | Yes | | | buying_account_address | The account address of the buying party | string | | No | | | buying_asset_code | The 4 or 12 character code of the bought asset within a trade | string | | No | Asset codes have no guarantees of uniqueness. The combination of asset code, issuer, and type represents a distinct asset | | buying_asset_issuer | The account address of the original asset issuer for the bought asset within a trade | string | | No | | | buying_asset_type | The identifier for type of asset code used for the bought asset within the trade | string | credit_alphanum4credit_alphanum12native | Yes | XLM is the native asset to the network. XLM has no asset code or issuer representation and will instead be displayed with an asset type of 'native' | | buying_amount | The amount of purchased asset that was moved from the seller account into the buying account, reported in terms of the bought asset | float | | Yes | | | price_n | The price ratio of the sold asset: bought asset. When taken with price_d, the price can be calculated by price_n/price_d | integer | | No | | | price_d | The price ratio of the sold asset: bought asset. When taken with price_n, the price can be calculated by price_n/price_d | integer | | No | | | selling_offer_id | The offer ID in the orderbook of the selling offer. If this offer was immediately and fully consumed, this will be a synthetic ID. | integer | | No | | | buying_offer_id | The offer ID in the orderbook of the buying offer. If this offer was immediately and fully consumed, this will be a synthetic ID. | integer | | No | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | No | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | No | The table is partitioned on batch_run_date. It is recommended to always include the batch_run_date in the filter if possible to help reduce query cost. | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | No | | | selling_liquidity_pool_id | The unique identifier for a liquidity pool if the trade was executed against a liquidity pool instead of the orderbook | string | | No | | | liquidity_pool_fee | The percentage fee (in basis points) of the total fee collected by the liquidity pool for executing the trade. The fee is pooled and distributed back to liquidity pool shareholders to incentivize users to stake money in the pool. | integer | 30 | No | Liquidity pool fees can only change with protocol changes to the network itself | | trade_type | Indicates whether the trade was executed against the orderbook (decentralized exchange) or liquidity pool | integer | 1 - Decentralized Exchange Trade2 - Liquidity Pool Trade | No | | | rounding_slippage | Applies to liquidity pool trades only. With fractional amounts of an asset traded, the network must round a fraction to the nearest whole number. This can cause the trade to "slip" price by a percentage compared with the original offer. Rounding slippage reports the percentage that dust trades slip before executing. | integer | | No | Defaults to 1. Rounding Slippage is always unprofitable for the trader and is not a valid way to try and extract more value from the network. | | seller_is_exact | Indicates whether the buying or selling party trade was impacted by rounding slippage. If true, the buyer was impacted. If false, the seller was impacted | boolean | | No | | | selling_asset_id | Unique identifier for selling_asset_code, selling_asset_issuer | integer | | No | | | buying_asset_id | Unique identifier for buying_asset_code, buying_asset_issuer | integer | | No | | --- ## History Transactions ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | id | | Partition Field(s) | batch_run_date (MONTH partition) | | Clustered Field(s) | account, ledger_sequence, successful | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.history_transactions) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | id | A unique identifier for this transaction | integer | | Yes | | | transaction_hash | A hex-encoded SHA-256 hash of this transaction's XDR-encoded form | string | | | | | ledger_sequence | The sequence number of the ledger that this transaction was included in | integer | | Yes | | | account | The account address that originates the transaction | string | | Yes | | | account_sequence | The source account's sequence number that this transaction consumed. Sequence numbers can only be used once and help maintain atomicity and idempotency on the network. | integer | | Yes | | | max_fee | The maximum fee (in stroops) that the source account is willing to pay for the transaction to be included in a ledger. When the network enters surge pricing, this helps determine if a transaction is included in the set | integer | | Yes | The stroop is the fractional representation of a lumen (XLM). 1 stroop is 0.0000001 XLM. | | operation_count | The number of operations contained within this transaction | integer | | Yes | A transaction is permitted to have up to 100 operations | | created_at | The date the transaction was created | timestamp | | No | | | memo_type | The type of memo | string | MemoTypeMemoHashMemoTypeMemoIdMemoTypeMemoNoneMemoTypeMemoReturnMemoTypeMemoText | Yes | Defaults to `MemoTypeMemoNone` | | memo | An optional freeform field that attaches a memo to a transaction | string | | No | Memos are heavily used by centralized exchanges to help with account management. | | time_bounds | A transaction precondition that can be set to determine when a transaction is valid. The user can set a lower and upper timebound, defined as a UNIX timestamp when the transaction can be executed. If the transaction attempts to execute outside of the time range, the transaction will fail | string | | No | | | successful | Indicates if this transaction was successful or not | boolean | | No | A transaction's success does not indicate whether it was included and written to a ledger. It only indicates whether the operations in the transaction were successfully applied to mutate the ledger state. | | fee_charged | The fee (in stroops) paid by the source account to apply this transaction to the ledger. At minimum, a transaction is charged # of operations \* base fee. The minimum base fee is 100 stroops | integer | | No | The stroop is the fractional representation of a lumen (XLM). 1 stroop is 0.0000001 XLM. | | inner_transaction_hash | A transaction hash of a transaction wrapped with its signatures for fee-bump transactions | string | | No | | | fee_account | An account that is not the originating source account for a transaction is allowed to pay transaction fees on behalf of the source account. These accounts are called fee accounts and incur all transaction costs for the source account. | string | | No | | | new_max_fee | If an account has a fee account, the fee account can specify a maximum fee (in stroops) that it is willing to pay for this account's fees. When the network is in surge pricing, the validators will consider the new_max_fee instead of the max_fee when determining if the transaction will be included in the transaction set | integer | | No | | | account_muxed | If the user has defined multiplexed (muxed) accounts, the account exists "virtually" under a traditional Stellar account address. This address distinguishes between the virtual accounts | string | | No | | | fee_account_muxed | If the fee account that sponsors fee is a multiplexed account, the virtual address will be listed here | string | | No | | | ledger_bounds | A transaction precondition that can be set to determine valid conditions for a transaction to be submitted to the network. Ledger bounds allow the user to specify a minimum and maxiumum ledger sequence number in which the transaction can successfully execute | string | | No | | | min_account_sequence | A transaction precondition that can be set to determine valid conditions for a transaction to be submitted to the network. This condition contains an integer representation of the lowest source account sequence number for which the transaction is valid | integer | | No | | | min_account_sequence_age | A transaction precondition that can be set to determine valid conditions for a transaction to be submitted to the network. This condition contains a minimum duration of time that must have passed since the source account's sequence number changed for the transaction to be valid | integer | | No | | | min_account_sequence_ledger_gap | A transaction precondition that can be set to determine valid conditions for a transaction to be submitted to the network. This condition contains an integer representation of the minimum number of ledgers that must have closed since the source account's sequence number change for the transaction to be valid | integer | | No | | | extra_signers | An array of up to two additional signers that must have corresponding signatures for this transaction to be valid | array[string] | | No | | | tx_envelope | base-64 encoded XDR blob | string | | No | | | tx_result | base-64 encoded XDR blob | string | | No | | | tx_meta | base-64 encoded XDR blob | string | | No | | | tx_fee_meta | base-64 encoded XDR blob | string | | No | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of "scheduled\_\_[batch_end_date]-[dag_alias]". Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | The table is partitioned on batch_run_date. It is recommended to always include the batch_run_date in the filter if possible to help reduce query cost. | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | resource_fee | The fee charged less the inclusion fee for the Soroban transaction. This is calculated by the read/write operations and how process intensive the Soroban transaction is | integer | | No | | | soroban_resources_instructions | Number of CPU instructions the Soroban transaction uses | integer | | No | | | soroban_resources_read_bytes | Number of bytes read by the Soroban transaction | integer | | No | | | soroban_resources_write_bytes | Number of bytes written by the Soroban transaction | integer | | No | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | We aim to repartition the table by closed_at | | transaction_result_code | The detailed result code that outlines why a transaction failed. This code is only useful for failed transactions. The full list of domain values can be found [here](https://pkg.go.dev/github.com/stellar/go-stellar-sdk/xdr#TransactionResultCode) | string | TransactionResultCodeTxFeeBumpInnerSuccessTransactionResultCodeTxSuccessTransactionResultCodeTxFailedTransactionResultCodeTxTooEarlyTransactionResultCodeTxTooLateTransactionResultCodeTxMissingOperationTransactionResultCodeTxBadSeqTransactionResultCodeTxBadAuthTransactionResultCodeTxInsufficientBalanceTransactionResultCodeTxNoAccountTransactionResultCodeTxInsufficientFeeTransactionResultCodeTxBadAuthExtraTransactionResultCodeTxInternalErrorTransactionResultCodeTxNotSupportedTransactionResultCodeTxFeeBumpInnerFailedTransactionResultCodeTxBadSponsorshipTransactionResultCodeTxBadMinSeqAgeOrGapTransactionResultCodeTxMalformedTransactionResultCodeTxSorobanInvalid | Yes | | | inclusion_fee_bid | The maximum bid the submitter is willing to pay for inclusion of the transaction. This fee is used to prioritize transactions that are included in the ledger. | integer | | No | | | inclusion_fee_charged | The fee charged for the transaction to be included in the ledger. This is a fixed fee for the entire ledger and starts at a minimum of 100 stroops. The fee increases based on demand | integer | | No | | | resource_fee_refund | The amount of the resource fee refunded to the transaction submitter. The refundable fees are calculated from rent, events and return value. Refundable fees are charged from the source account before the transaction is executed and then refunded based on the actual usage. | integer | | No | | | non_refundable_resource_fee_charged | The amount charged for the transaction that is not refundable | integer | | | | | refundable_resource_fee_charged | The amount charged for the transaction from the refundable_fee | integer | | | | | rent_fee_charged | The rent fee charged to persist the contract or contract code | integer | | | | | refundable_fee | The amount of resource fees that are refundable based on the actual usage of resources in the transaction | integer | | | | | tx_signers | The public keys of the signers who authorized the transaction. This field lists all the signatories that validated and approved the transaction, ensuring it meets the required authorization thresholds | string | | | | --- ## Liquidity Pools ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | liquidity_pool_id, closed_at | | Partition Field(s) | batch_run_date (MONTH partition) | | Clustered Field(s) | liquidity_pool_id, asset_a_id, asset_b_id, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.liquidity_pools) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | liquidity_pool_id | Unique identifier for a liquidity pool. There cannot be duplicate pools for the same asset pair. Once a pool has been created for the asset pair, another cannot be created. | string | | Yes | There is a good primer on AMMs [here](../../../../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#liquidity-pools) | | type | The mechanism that calculates pricing and division of shares for the pool. With the initial AMM rollout, the only type of liquidity pool allowed to be created is a constant product pool | string | constant_product | Yes | For more information regarding pricing and deposit calculations, read [CAP-38](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0038.md). | | fee | The number of basis points charged as a percentage of the trade in order to complete the transaction. The fees earned on all trades are divided amongst pool shareholders and distributed as an incentive to keep money in the pools | integer | 30 | Yes | Fees are distributed immediately to accounts as the transaction completes. There is no schedule for fee distribution | | trustline_count | Total number of accounts with trustlines authorized to the pool. To create a trustline, an account must trust both base assets before trusting a pool with the asset pair | integer | | Yes | If the issuer of A or B revokes authorization on the trustline, the account will automatically withdraw from every liquidity pool containing that asset and those pool trustlines will be deleted. | | pool_share_count | Participation in a liquidity pool is represented by a pool share. The total number of pool shares is calculated by a constant product formula and is an arbitrary number representing the amount of participation in the pool. | float | | Yes | Shares are not transferable; the only way to increase the number of pool shares held is to deposit into a liquidity pool. Conversely, decreasing pools shares can only be accomplished through a withdraw operation. Shares cannot be sent in payments or sold using offers. | | asset_a_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | Yes | | | asset_a_code | The 4 or 12 character code representation of the asset of one of the two asset pairs in a liquidity pool | string | | No | | | asset_a_issuer | The account address of the original asset issuer that created one of the two asset pairs in the liquidity pool | string | | No | | | asset_a_amount | The raw number of tokens locked in the pool for one of the two asset pairs in the liquidity pool | float | | Yes | The amount is a better representation of liquidity in the pool over pool share counts. | | asset_b_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | Yes | | | asset_b_code | The 4 or 12 character code representation of the asset of one of the two asset pairs in a liquidity pool | string | | No | | | asset_b_issuer | The account address of the original asset issuer that created one of the two asset pairs in the liquidity pool | string | | No | | | asset_b_amount | The raw number of tokens locked in the pool for one of the two asset pairs in the liquidity pool | float | | Yes | The amount is a better representation of liquidity in the pool over pool share counts. | | last_modified_ledger | The ledger sequence number when the ledger entry (this unique signer for the account) was modified. Deletions do not count as a modification and will report the prior modification sequence number | integer | | Yes | | | ledger_entry_change | Code that describes the ledger entry change type that was applied to the ledger entry. | integer | 0 - Ledger Entry Created1 - Ledger Entry Updated2 - Ledger Entry Deleted3 - Ledger Entry State (value of the entry) | Yes | Valid entry change types are 0, 1, and 2 for ledger entries of type `liquidity_pools`. | | deleted | Indicates whether the ledger entry (liquidity pool) has been deleted or not. Once an entry is deleted, it cannot be recovered. Liquidity pools are deleted once all pool shares are withdrawn from the pool | boolean | | Yes | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | The table is partitioned on batch_run_date. It is recommended to always include the batch_run_date in the filter if possible to help reduce query cost. | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | asset_a_id | Unique identifier for asset_a_code, asset_a_issuer | integer | | No | | | asset_b_id | Unique identifier for asset_b_code, asset_b_issuer | integer | | No | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | --- ## Offers ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | offer_id, closed_at | | Partition Field(s) | batch_run_date (MONTH partition) | | Clustered Field(s) | selling_asset_id, buying_asset_id, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.offers) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | seller_id | The account address that is making this offer | string | | Yes | | | offer_id | The unique identifier for this offer | integer | | Yes | | | selling_asset_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | Yes | | | selling_asset_code | The 4 or 12 character code representation of the asset offered to be sold | string | | No | | | selling_asset_issuer | The account address of the original asset issuer that minted the asset which will be sold in exchange for another asset | string | | No | | | buying_asset_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | Yes | | | buying_asset_code | The 4 or 12 character code representation of the asset desired to be purchased | string | | No | | | buying_asset_issuer | The account address of the original asset issuer that minted the asset which will be bought in exchange for a currently held asset | string | | No | | | amount | The amount of selling that the account making this offer is willing to sell | float | | Yes | | | pricen | The numerator of the precise representation of the buy and sell price of assets on offer (The buy amount desired) | integer | | Yes | If an offer wants to sell 10 XLM in exchange for 1 USD, the numerator will be 1. | | priced | The denominator of the precise representation of the buy and sell price of assets on offer (The sell amount offered) | integer | | Yes | If an offer wants to sell 10 XLM in exchange for 1 USD, the denominator will be 10. | | price | How many units of buying it takes to get 1 unit of selling. This number is the decimal form of pricen / priced | float | | Yes | If an offer wants to sell 10 XLM in exchange for 1 USD, the price will be 0.1 | | flags | Denotes the enabling/disabling of certain asset issuer privileges | integer | 0 - None, Default1 - Passive (offer with this flag will not act on and take a reverse offer of equal price) | Yes | | | last_modified_ledger | The ledger sequence number when the ledger entry (this unique signer for the account) was modified. Deletions do not count as a modification and will report the prior modification sequence number | integer | | Yes | | | ledger_entry_change | Code that describes the ledger entry change type that was applied to the ledger entry. | integer | 0 - Ledger Entry Created1 - Ledger Entry Updated2 - Ledger Entry Deleted3 - Ledger Entry State (value of the entry) | Yes | Valid entry change types are 0, 1, and 2 for ledger entries of type `offers`. | | deleted | Indicates whether the ledger entry (offer id) has been deleted or not. Once an entry is deleted, it cannot be recovered | boolean | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | | sponsor | The account address that is sponsoring the base reserves for the offer | string | | No | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | The table is partitioned on batch_run_date. It is recommended to always include the batch_run_date in the filter if possible to help reduce query cost. | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | selling_asset_id | Unique identifier for selling_asset_code, selling_asset_issuer | integer | | No | | | buying_asset_id | Unique identifier for buying_asset_code, buying_asset_issuer | integer | | No | | --- ## Restored Key ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | ledger_key_hash, closed_at | | Partition Field(s) | closed_at (MONTH partition) | | Clustered Field(s) | last_modified_ledger, ledger_key_hash | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.restored_key) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | ledger_key_hash | Either contract_id or contract_code_hash | string | | Yes | | | ledger_entry_type | The type ledger entry for data stored such as contract data or liquidity pools | string | | Yes | | | last_modified_ledger | The ledger sequence number when the ledger entry (this unique signer for the account) was modified. Deletions do not count as a modification and will report the prior modification sequence number | integer | | Yes | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | --- ## Trustlines ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | account_id, asset_type, asset_issuer, asset_code, liquidity_pool_id, closed_at | | Partition Field(s) | batch_run_date (MONTH partition) | | Clustered Field(s) | account_id, asset_id, liquidity_pool_id, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.trust_lines) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | ledger_key | The unique ledger key when the trust line state last changed | string | | Yes | | | account_id | The account address | string | | Yes | | | asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | integer | credit_alphanum4credit_alphanum12native | Yes | | | asset_issuer | The account address of the original asset issuer that created the asset held by this account | string | | No | | | asset_code | The 4 or 12 character code representation of the asset held by this account | string | | No | | | liquidity_pool_id | If the asset held is part of a liquidity pool share, the unique pool id from which the asset balance originates | string | | No | | | balance | The number of units of an asset held by this account | float | | Yes | | | trust_line_limit | The maximum amount of this asset that this account is willing to accept. The limit is specified when opening a trust line | integer | | Yes | | | buying_liabilities | The sum of all buy offers owned by this account for non-native assets | float | | Yes | | | selling_liabilities | The sum of all sell offers owned by this account for non-native assets | float | | Yes | | | flags | Denotes the enabling and disabling of certain asset issuer privileges | integer | 0 - None, Default1 - Authorized2 - Authorized to Maintain Liabilities4 - Clawback Enabled | Yes | Flags are set on the issuer accounts for an asset. When user accounts trust an asset, the flags applied to the asset originate from this account | | last_modified_ledger | The ledger sequence number when the ledger entry (this unique signer for the account) was modified | integer | | Yes | | | ledger_entry_change | Code that describes the ledger entry change type that was applied to the ledger entry | integer | 0 - Ledger Entry Created1 - Ledger Entry Updated2 - Ledger Entry Deleted3 - Ledger Entry State | Yes | Valid entry change types are 0, 1, and 2 for ledger entries of type `trust_lines`. | | deleted | Indicates whether the ledger entry (trust line) has been deleted or not. Once an entry is deleted, it cannot be recovered | boolean | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | | batch_id | String representation of the run id for a given DAG in Airflow | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed | datetime | | Yes | The table is partitioned on batch_run_date. It is recommended to always include the batch_run_date in the filter if possible to help reduce query cost. | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database | timestamp | | Yes | | | sponsor | The account address that is sponsoring the base reserves for the trust line | string | | No | | | asset_id | Unique identifier for asset_code, asset_issuer | integer | | No | | --- ## TTL ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | key_hash, closed_at | | Partition Field(s) | closed_at (MONTH partition) | | Clustered Field(s) | last_modified_ledger, key_hash | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.ttl) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | key_hash | Ledger key hash used to identify expiring contract data or contract code ledger entries | string | | Yes | | | live_until_ledger_seq | Ledger sequence the contract or wasm will live until | integer | | Yes | | | last_modified_ledger | The ledger sequence number when the ledger entry (this unique signer for the account) was modified | integer | | Yes | | | ledger_entry_change | Code that describes the ledger entry change type that was applied to the ledger entry | integer | | Yes | | | deleted | Indicates whether the ledger entry (balance id) has been deleted or not. Once an entry is deleted, it cannot be recovered | boolean | | Yes | | | batch_id | String representation of the run id for a given DAG in Airflow | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed | datetime | | Yes | | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database | timestamp | | Yes | | | closed_at | The UNIX timestamp of the sequence number's age | timestamp | | Yes | | | ledger_sequence | The unsigned 32-bit ledger number of the sequence number's age | integer | | Yes | | --- ## Gold 🥇 Curated analytics with friendly aggregate tables. These tables are made up of various aggregations of Stellar data such as total network TVL. ## When to use These tables are best used for getting pre-aggregated metrics and insights for the Stellar network. One example is the `tvl_agg` table that contains the daily TVL for the Stellar network instead of needing to aggregate TVL from each individual account, trade, liquiditiy pool, etc... --- ## Asset Balances Daily Agg ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | day, asset_type, asset_code, asset_issuer | | Partition Field(s) | day (DAY partition) | | Clustered Field(s) | N/A | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.asset_balances__daily_agg) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | day | Date by which all metrics are aggregated. | DATE | | Yes | | | asset_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | STRING | | Yes | XLM is the native asset to the network. XLM has no asset code or issuer representation and will instead be displayed with an asset type of 'native' | | asset_code | The 4 or 12 character code representation of the asset on the network. | STRING | | Yes | Asset codes have no guarantees of uniqueness. The combination of asset code, issuer and type represents a distinct asset | | asset_issuer | The account address of the original asset issuer that created the asset. | STRING | | Yes | | | liquidity_pool_balance | The sum of balances across all liquidity pools for a given asset. | FLOAT | | Yes | | | offer_balance | The sum of balances across all selling liabilities(SDEX only) for a given asset. | FLOAT | | Yes | | | trustline_balance | The sum of trustline balance for a given asset. | FLOAT | | Yes | | | total_accounts_with_liquidity_pool_balance | The count of positive liquidity pool balance holders for a given asset. | INTEGER | | Yes | | | total_accounts_with_offer_balance | The count of positive offer balance holders for a given asset. | INTEGER | | Yes | | | total_accounts_with_trustline_balance | The count of positive trustline balance holders for a given asset. | INTEGER | | Yes | | --- ## Daily Fee Stats Agg ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | day_agg | | Partition Field(s) | N/A | | Clustered Field(s) | day_agg | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.daily_fee_stats_agg) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | day_agg | Day when the fee stats were aggregated. | DATE | | Yes | | | total_fee_charged | Sum of fee_charged across all transactions (Classic + Soroban) for the day. | INTEGER | | Yes | | | max_fee_charged | Maximum fee_charged across all transactions (Classic + Soroban) for the day. | INTEGER | | Yes | | | txn_count | Total number of transactions (Classic + Soroban) for the day. | INTEGER | | Yes | | | failed_txn_count | Number of failed transactions (Classic + Soroban) for the day. Failed transactions are still included in all fee aggregates because inclusion_fee_charged and non_refundable_resource_fee_charged are charged regardless of transaction success. | INTEGER | | No | | | total_effective_txn_operation_count | Total number of effective operations across all transactions for the day. Uses effective_txn_operation_count, which adds 1 for fee-bump transactions to account for the extra inner transaction. | INTEGER | | Yes | | | total_raw_txn_operation_count | Total number of raw operations (txn_operation_count) across all transactions for the day. Unlike effective operation count, this does NOT include fee-bump transactions. | INTEGER | | Yes | | | min_ledger_sequence | First (minimum) ledger sequence number for the day. | INTEGER | | Yes | | | max_ledger_sequence | Last (maximum) ledger sequence number for the day. | INTEGER | | Yes | | | total_ledgers | Total number of ledgers processed for the day, regardless of whether they contain Classic or Soroban transactions. Used as the denominator for total_pct_ledgers_in_surge. | INTEGER | | Yes | | | classic_txn_count | Number of Classic transactions for the day. Classic transactions have resource_fee = 0. | INTEGER | | No | | | classic_failed_txn_count | Number of failed Classic transactions for the day. | INTEGER | | No | | | classic_total_effective_operation_count | Total number of effective operations across Classic transactions for the day. Adds 1 for fee-bump transactions. | INTEGER | | No | | | classic_total_raw_operation_count | Total number of raw operations across Classic transactions for the day. Does NOT include fee-bump transactions. | INTEGER | | No | | | classic_sum_fee_charged | Sum of fee_charged across Classic transactions for the day. For Classic txns, fee_charged is the inclusion fee (no resource_fee component). | INTEGER | | No | | | classic_max_fee_charged | Maximum fee_charged across Classic transactions for the day. | INTEGER | | No | | | classic_sum_max_fee | Sum of COALESCE(new_max_fee, max_fee) across Classic transactions for the day. Represents total willingness-to-pay. | INTEGER | | No | | | classic_max_max_fee | Maximum of COALESCE(new_max_fee, max_fee) across Classic transactions for the day. | INTEGER | | No | | | classic_max_inclusion_fee_per_op | Maximum inclusion fee per operation (fee_charged / effective_txn_operation_count) across Classic transactions for the day. | FLOAT | | No | | | classic_min_inclusion_fee_per_op | Minimum inclusion fee per operation across Classic transactions for the day. | FLOAT | | No | | | classic_total_ledgers | Total number of ledgers containing at least one Classic transaction for the day. | INTEGER | | No | | | classic_surge_ledger_count | Number of ledgers where at least one Classic transaction experienced surge pricing for the day. | INTEGER | | No | | | classic_total_surge_txn_count | Total number of Classic transactions that experienced surge pricing across all ledgers for the day. | INTEGER | | No | | | classic_total_surge_operation_count | Total operations in Classic transactions that experienced surge pricing across all ledgers for the day. | INTEGER | | No | | | classic_pct_ledgers_in_surge | Percentage of Classic-containing ledgers that experienced surge pricing for the day. Calculated as 100 \* classic_surge_ledger_count / classic_total_ledgers. | FLOAT | | No | | | soroban_txn_count | Number of Soroban transactions for the day. Soroban transactions have resource_fee > 0. | INTEGER | | No | | | soroban_failed_txn_count | Number of failed Soroban transactions for the day. Even failed Soroban transactions are charged inclusion_fee_charged and non_refundable_resource_fee_charged, so they are included in all fee aggregates. | INTEGER | | No | | | soroban_total_effective_operation_count | Total number of effective operations across Soroban transactions for the day. Adds 1 for fee-bump transactions. | INTEGER | | No | | | soroban_total_raw_operation_count | Total number of raw operations across Soroban transactions for the day. Does NOT include fee-bump transactions. | INTEGER | | No | | | soroban_sum_fee_charged | Sum of fee_charged across Soroban transactions for the day. fee_charged = inclusion_fee_charged + non_refundable_resource_fee_charged + refundable_resource_fee_charged. | INTEGER | | No | | | soroban_max_fee_charged | Maximum fee_charged across Soroban transactions for the day. | INTEGER | | No | | | soroban_sum_inclusion_fee_charged | Sum of inclusion_fee_charged across Soroban transactions for the day. The base inclusion fee is 100 stroops per operation. If inclusion_fee_charged exceeds this base, the ledger experienced inclusion fee surge pricing. | INTEGER | | No | | | soroban_max_inclusion_fee_charged | Maximum inclusion_fee_charged across Soroban transactions for the day. | INTEGER | | No | | | soroban_sum_inclusion_fee_bid | Sum of inclusion_fee_bid across Soroban transactions for the day. inclusion_fee_bid = COALESCE(new_max_fee, max_fee) - resource_fee. | INTEGER | | No | | | soroban_max_inclusion_fee_bid | Maximum inclusion_fee_bid across Soroban transactions for the day. | INTEGER | | No | | | soroban_max_inclusion_fee_per_op | Maximum inclusion fee per operation (inclusion_fee_charged / effective_txn_operation_count) across Soroban transactions for the day. | FLOAT | | No | | | soroban_min_inclusion_fee_per_op | Minimum inclusion fee per operation across Soroban transactions for the day. | FLOAT | | No | | | soroban_sum_resource_fee | Sum of resource_fee (the pre-execution budget for Soroban resource consumption) across Soroban transactions for the day. | INTEGER | | No | | | soroban_max_resource_fee | Maximum resource_fee across Soroban transactions for the day. | INTEGER | | No | | | soroban_min_resource_fee | Minimum resource_fee across Soroban transactions for the day. | INTEGER | | No | | | soroban_sum_non_refundable_resource_fee_charged | Sum of non_refundable_resource_fee_charged across Soroban transactions for the day. Covers CPU instructions, read bytes, write bytes, and bandwidth. Charged based on declared resources regardless of tx success/failure. | INTEGER | | No | | | soroban_max_non_refundable_resource_fee_charged | Maximum non_refundable_resource_fee_charged across Soroban transactions for the day. | INTEGER | | No | | | soroban_min_non_refundable_resource_fee_charged | Minimum non_refundable_resource_fee_charged across Soroban transactions for the day. | INTEGER | | No | | | soroban_sum_refundable_resource_fee_charged | Sum of refundable_resource_fee_charged across Soroban transactions for the day. Covers rent, events, and return value. Based on actual usage; 0 for failed transactions. | INTEGER | | No | | | soroban_max_refundable_resource_fee_charged | Maximum refundable_resource_fee_charged across Soroban transactions for the day. | INTEGER | | No | | | soroban_min_refundable_resource_fee_charged | Minimum refundable_resource_fee_charged across Soroban transactions for the day. | INTEGER | | No | | | soroban_sum_resource_fee_refund | Sum of resource_fee_refund across Soroban transactions for the day. NOTE: Currently broken -- always 0. | INTEGER | | No | | | soroban_max_resource_fee_refund | Maximum resource_fee_refund across Soroban transactions for the day. NOTE: Currently broken -- always 0. | INTEGER | | No | | | soroban_min_resource_fee_refund | Minimum resource_fee_refund across Soroban transactions for the day. NOTE: Currently broken -- always 0. | INTEGER | | No | | | soroban_sum_rent_fee_charged | Sum of rent_fee_charged across Soroban transactions for the day. This is the portion of refundable_resource_fee_charged that went to ledger entry TTL extensions. | INTEGER | | No | | | soroban_max_rent_fee_charged | Maximum rent_fee_charged across Soroban transactions for the day. | INTEGER | | No | | | soroban_min_rent_fee_charged | Minimum rent_fee_charged across Soroban transactions for the day. | INTEGER | | No | | | soroban_total_ledgers | Total number of ledgers containing at least one Soroban transaction for the day. | INTEGER | | No | | | soroban_surge_ledger_count | Number of ledgers where at least one Soroban transaction experienced surge pricing for the day. | INTEGER | | No | | | soroban_total_surge_txn_count | Total number of Soroban transactions that experienced surge pricing across all ledgers for the day. | INTEGER | | No | | | soroban_total_surge_operation_count | Total operations in Soroban transactions that experienced surge pricing across all ledgers for the day. | INTEGER | | No | | | soroban_pct_ledgers_in_surge | Percentage of Soroban-containing ledgers that experienced surge pricing for the day. Calculated as 100 \* soroban_surge_ledger_count / soroban_total_ledgers. | FLOAT | | No | | | total_pct_ledgers_in_surge | Percentage of all ledgers for the day where at least one transaction (Classic or Soroban) experienced surge pricing. | FLOAT | | No | | | fee_pool | The cumulative fee pool balance (in stroops) at the end of the day, taken from the last ledger that closed on that day. This is a running total of all transaction fees ever collected by the network, not the fees for a single day. To see daily fees collected, use total_fee_charged instead. | INTEGER | | No | | | airflow_start_ts | Timestamp when the Airflow DAG run started. Used for pipeline metadata and debugging. | STRING | | No | | --- ## Hourly Fee Agg Account ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | hour_agg, fee_source_account | | Partition Field(s) | hour_agg (DAY partition) | | Clustered Field(s) | hour_agg, fee_source_account | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.hourly_fee_agg_account) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | hour_agg | Hour-truncated UTC timestamp of the aggregation window. Derived from `timestamp_trunc(closed_at, hour)`. | TIMESTAMP | | Yes | | | fee_source_account | The account that paid the transaction fee. For fee-bump transactions, this is the fee sponsor (fee_account). For non-fee-bump transactions, this is the transaction originator (txn_account). | STRING | | Yes | | | txn_count | Total number of transactions (Classic + Soroban) submitted by this fee_source_account in the hour. | INTEGER | | Yes | | | failed_txn_count | Number of failed transactions (Classic + Soroban) for this fee_source_account in the hour. Failed transactions are still charged fees and included in all fee aggregates. | INTEGER | | No | | | total_fee_charged | Sum of fee_charged across all transactions (Classic + Soroban) for this fee_source_account in the hour. This is the total fees actually paid. | INTEGER | | Yes | | | total_max_fee | Sum of the effective fee ceiling -- coalesce(new_max_fee, max_fee) -- across all transactions for this fee_source_account in the hour. For fee-bump transactions, new_max_fee is the actual ceiling used by the network. Represents total willingness-to-pay. | INTEGER | | Yes | | | fee_efficiency | Ratio of total_fee_charged to total_max_fee. Bounded (0, 1]. Values closer to 1.0 indicate the account is bidding close to what it actually pays; lower values indicate overbidding. | FLOAT | | No | | | total_effective_operation_count | Total effective operations across all transactions for this fee_source_account. Adds 1 to the operation count for fee-bump transactions. | INTEGER | | Yes | | | total_raw_operation_count | Total raw operations (txn_operation_count) across all transactions for this fee_source_account. Does NOT include fee-bump adjustment. | INTEGER | | Yes | | | classic_txn_count | Number of Classic transactions for this fee_source_account in the hour. Classic transactions have resource_fee = 0. | INTEGER | | No | | | classic_failed_txn_count | Number of failed Classic transactions for this fee_source_account in the hour. | INTEGER | | No | | | classic_total_fee_charged | Sum of fee_charged across Classic transactions for this fee_source_account. For Classic txns, fee_charged is the inclusion fee (no resource_fee component). NULL if the account had no Classic transactions in this hour. | INTEGER | | No | | | classic_total_max_fee | Sum of the effective fee ceiling -- coalesce(new_max_fee, max_fee) -- across Classic transactions for this fee_source_account. Represents Classic willingness-to-pay. NULL if the account had no Classic transactions in this hour. | INTEGER | | No | | | classic_total_effective_operation_count | Total effective operations across Classic transactions for this fee_source_account. Adds 1 for fee-bump transactions. NULL if the account had no Classic transactions in this hour. | INTEGER | | No | | | classic_surge_txn_count | Number of Classic transactions for this fee_source_account where fee_charged exceeded the base fee (effective_operation_count \* 100 stroops), indicating surge pricing. | INTEGER | | No | | | soroban_txn_count | Number of Soroban transactions for this fee_source_account in the hour. Soroban transactions have resource_fee > 0. | INTEGER | | No | | | soroban_failed_txn_count | Number of failed Soroban transactions for this fee_source_account in the hour. Even failed Soroban transactions are charged inclusion_fee_charged and non_refundable_resource_fee_charged. | INTEGER | | No | | | soroban_total_fee_charged | Sum of fee_charged across Soroban transactions for this fee_source_account. fee_charged = inclusion_fee_charged + non_refundable_resource_fee_charged + refundable_resource_fee_charged - resource_fee_refund. NULL if the account had no Soroban transactions in this hour. | INTEGER | | No | | | soroban_total_inclusion_fee_charged | Sum of inclusion_fee_charged across Soroban transactions for this fee_source_account. NULL if the account had no Soroban transactions in this hour. | INTEGER | | No | | | soroban_total_inclusion_fee_bid | Sum of inclusion_fee_bid across Soroban transactions for this fee_source_account. Represents total willingness-to-pay for inclusion. NULL if the account had no Soroban transactions in this hour. | INTEGER | | No | | | soroban_total_resource_fee | Sum of resource_fee (pre-execution budget) across Soroban transactions for this fee_source_account. NULL if the account had no Soroban transactions in this hour. | INTEGER | | No | | | soroban_total_non_refundable_resource_fee | Sum of non_refundable_resource_fee_charged across Soroban transactions. Covers CPU instructions, read bytes, write bytes, and bandwidth. Charged regardless of tx success/failure. NULL if the account had no Soroban transactions in this hour. | INTEGER | | No | | | soroban_total_refundable_resource_fee | Sum of refundable_resource_fee_charged across Soroban transactions. Covers rent, events, and return value. Based on actual usage; 0 for failed transactions. NULL if the account had no Soroban transactions in this hour. | INTEGER | | No | | | soroban_total_rent_fee | Sum of rent_fee_charged across Soroban transactions. The portion of refundable_resource_fee_charged that went to ledger entry TTL extensions. NULL if the account had no Soroban transactions in this hour. | INTEGER | | No | | | soroban_total_resource_fee_refund | Sum of resource_fee_refund across Soroban transactions. Represents the unused portion of resource_fee returned to the account after execution. NULL if the account had no Soroban transactions in this hour. | INTEGER | | No | | | soroban_total_effective_operation_count | Total effective operations across Soroban transactions for this fee_source_account. Adds 1 for fee-bump transactions. NULL if the account had no Soroban transactions in this hour. | INTEGER | | No | | | soroban_surge_txn_count | Number of Soroban transactions for this fee_source_account where inclusion_fee_charged exceeded the base fee (effective_operation_count \* 100 stroops), indicating surge pricing. | INTEGER | | No | | | airflow_start_ts | Timestamp when the Airflow DAG run started. Used for pipeline metadata and debugging. | STRING | | No | | --- ## Hourly Soroban Fee Agg Contract ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | hour_agg, contract_id | | Partition Field(s) | hour_agg (DAY partition) | | Clustered Field(s) | hour_agg, contract_id | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.hourly_soroban_fee_agg_contract) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | hour_agg | Hour-truncated UTC timestamp of the aggregation window. Derived from `timestamp_trunc(closed_at, hour)`. | TIMESTAMP | | Yes | | | contract_id | The Soroban smart contract address (strkey `C...`) attributed to the transaction. Derived from the operation's `details.type`: `invoke_contract` (the invoked contract), `create_contract` / `create_contract_v2` (the deployed contract), or `extend_footprint_ttl` / `restore_footprint` (the contract whose `ContractData` entry was in the footprint). Never NULL or empty — rows without a resolvable contract_id are filtered out upstream. | STRING | | Yes | | | txn_count | Total number of Soroban transactions invoking this contract in the hour. | INTEGER | | Yes | | | failed_txn_count | Number of failed Soroban transactions for this contract in the hour. Failed transactions are still charged inclusion_fee_charged and non_refundable_resource_fee_charged. | INTEGER | | No | | | unique_fee_source_accounts | Count of distinct fee-paying accounts for this contract in the hour. For fee-bump transactions this is the fee sponsor; for regular transactions this is the transaction originator. Useful for distinguishing between a single account driving a contract's fee volume vs. broad usage. | INTEGER | | Yes | | | total_fee_charged | Sum of fee_charged across all transactions invoking this contract in the hour. | INTEGER | | Yes | | | avg_fee_charged | Average fee_charged per transaction invoking this contract in the hour. | FLOAT | | No | | | max_fee_charged | Maximum fee_charged across transactions invoking this contract in the hour. | INTEGER | | No | | | total_max_fee | Sum of the effective fee ceiling -- coalesce(new_max_fee, max_fee) -- across transactions invoking this contract. For fee-bump transactions, new_max_fee is the actual ceiling used by the network; max_fee is the inner transaction's original max. Represents total willingness-to-pay. | INTEGER | | No | | | fee_efficiency | Ratio of total_fee_charged to total_max_fee. Bounded (0, 1]. Values closer to 1.0 indicate callers are bidding close to what they actually pay; lower values indicate overbidding. | FLOAT | | No | | | total_inclusion_fee_charged | Sum of inclusion_fee_charged across transactions invoking this contract. | INTEGER | | No | | | avg_inclusion_fee_charged | Average inclusion_fee_charged per transaction invoking this contract. | FLOAT | | No | | | total_inclusion_fee_bid | Sum of inclusion_fee_bid across transactions invoking this contract. Represents total willingness-to-pay for inclusion. | INTEGER | | No | | | total_resource_fee | Sum of resource_fee (pre-execution budget) across transactions invoking this contract. | INTEGER | | No | | | avg_resource_fee | Average resource_fee per transaction invoking this contract. | FLOAT | | No | | | total_non_refundable_resource_fee | Sum of non_refundable_resource_fee_charged across transactions. Covers CPU instructions, read bytes, write bytes, and bandwidth. Charged regardless of tx success/failure. | INTEGER | | No | | | total_refundable_resource_fee | Sum of refundable_resource_fee_charged across transactions. Covers rent, events, and return value. Based on actual usage; 0 for failed transactions. | INTEGER | | No | | | total_rent_fee | Sum of rent_fee_charged across transactions. The portion of refundable_resource_fee_charged that went to ledger entry TTL extensions. | INTEGER | | No | | | total_resource_fee_refund | Sum of resource_fee_refund across transactions. Represents the unused portion of resource_fee returned to the account after execution. | INTEGER | | No | | | surge_txn_count | Number of Soroban transactions for this contract where inclusion_fee_charged exceeded the base fee (effective_operation_count \* 100 stroops), indicating surge pricing. | INTEGER | | No | | | airflow_start_ts | Timestamp when the Airflow DAG run started. Used for pipeline metadata and debugging. | STRING | | No | | --- ## Ledger Fee Stats Agg ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | ledger_sequence | | Partition Field(s) | N/A | | Clustered Field(s) | day_agg, ledger_sequence | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.ledger_fee_stats_agg) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | day_agg | Day when the ledger fee stats were aggregated. | DATE | | Yes | | | ledger_sequence | The unique ledger sequence number. This is the grain of the table -- one row per ledger. | INTEGER | | Yes | | | total_fee_charged | Sum of fee_charged across all transactions (Classic + Soroban) in the ledger. This is the total fees actually paid. | INTEGER | | Yes | | | max_fee_charged | Maximum fee_charged across all transactions (Classic + Soroban) in the ledger. | INTEGER | | Yes | | | txn_count | Total number of transactions (Classic + Soroban) in the ledger. | INTEGER | | Yes | | | failed_txn_count | Number of failed transactions (Classic + Soroban) in the ledger. Failed transactions are still included in all fee aggregates because inclusion_fee_charged and non_refundable_resource_fee_charged are charged regardless of transaction success. | INTEGER | | No | | | total_effective_txn_operation_count | Total number of effective operations across all transactions in the ledger. Uses effective_txn_operation_count, which adds 1 for fee-bump transactions to account for the extra inner transaction. | INTEGER | | Yes | | | total_raw_txn_operation_count | Total number of raw operations (txn_operation_count) across all transactions in the ledger. Unlike effective operation count, this does NOT include fee-bump transactions. | INTEGER | | Yes | | | classic_txn_count | Number of Classic transactions in the ledger. Classic transactions have resource_fee = 0. | INTEGER | | No | | | classic_failed_txn_count | Number of failed Classic transactions in the ledger. | INTEGER | | No | | | classic_total_effective_operation_count | Total number of effective operations across Classic transactions in the ledger. Adds 1 for fee-bump transactions. | INTEGER | | No | | | classic_total_raw_operation_count | Total number of raw operations across Classic transactions in the ledger. Does NOT include fee-bump transactions. | INTEGER | | No | | | classic_sum_fee_charged | Sum of fee_charged across Classic transactions. For Classic txns, fee_charged is the inclusion fee (no resource_fee component). | INTEGER | | No | | | classic_max_fee_charged | Maximum fee_charged across Classic transactions in the ledger. | INTEGER | | No | | | classic_sum_max_fee | Sum of COALESCE(new_max_fee, max_fee) across Classic transactions. Represents total willingness-to-pay. | INTEGER | | No | | | classic_max_max_fee | Maximum of COALESCE(new_max_fee, max_fee) across Classic transactions in the ledger. | INTEGER | | No | | | classic_max_inclusion_fee_per_op | Maximum inclusion fee per operation (fee_charged / effective_txn_operation_count) across Classic transactions. Fee-bump txns add 1 to the operation count. | FLOAT | | No | | | classic_min_inclusion_fee_per_op | Minimum inclusion fee per operation across Classic transactions in the ledger. | FLOAT | | No | | | classic_surge_txn_count | Number of Classic transactions in the ledger where fee_charged exceeded the base fee (100 stroops per operation), indicating surge pricing. | INTEGER | | No | | | classic_surge_operation_count | Total operations in Classic transactions that experienced surge pricing in the ledger. | INTEGER | | No | | | classic_is_surge_ledger | True if any Classic transaction in the ledger experienced surge pricing. | BOOLEAN | | No | | | soroban_txn_count | Number of Soroban transactions in the ledger. Soroban transactions have resource_fee > 0. | INTEGER | | No | | | soroban_failed_txn_count | Number of failed Soroban transactions in the ledger. Even failed Soroban transactions are charged inclusion_fee_charged and non_refundable_resource_fee_charged, so they are included in all fee aggregates. | INTEGER | | No | | | soroban_total_effective_operation_count | Total number of effective operations across Soroban transactions in the ledger. Adds 1 for fee-bump transactions. | INTEGER | | No | | | soroban_total_raw_operation_count | Total number of raw operations across Soroban transactions in the ledger. Does NOT include fee-bump transactions. | INTEGER | | No | | | soroban_sum_fee_charged | Sum of fee_charged across Soroban transactions. fee_charged = inclusion_fee_charged + non_refundable_resource_fee_charged + refundable_resource_fee_charged. | INTEGER | | No | | | soroban_max_fee_charged | Maximum fee_charged across Soroban transactions in the ledger. | INTEGER | | No | | | soroban_sum_inclusion_fee_charged | Sum of inclusion_fee_charged across Soroban transactions. The base inclusion fee is 100 stroops per operation. If inclusion_fee_charged exceeds this base, the ledger experienced inclusion fee surge pricing. | INTEGER | | No | | | soroban_max_inclusion_fee_charged | Maximum inclusion_fee_charged across Soroban transactions in the ledger. | INTEGER | | No | | | soroban_sum_inclusion_fee_bid | Sum of inclusion_fee_bid across Soroban transactions. inclusion_fee_bid = COALESCE(new_max_fee, max_fee) - resource_fee. | INTEGER | | No | | | soroban_max_inclusion_fee_bid | Maximum inclusion_fee_bid across Soroban transactions in the ledger. | INTEGER | | No | | | soroban_max_inclusion_fee_per_op | Maximum inclusion fee per operation (inclusion_fee_charged / effective_txn_operation_count) across Soroban transactions in the ledger. | FLOAT | | No | | | soroban_min_inclusion_fee_per_op | Minimum inclusion fee per operation across Soroban transactions in the ledger. | FLOAT | | No | | | soroban_sum_resource_fee | Sum of resource_fee (the pre-execution budget for Soroban resource consumption) across Soroban transactions in the ledger. | INTEGER | | No | | | soroban_max_resource_fee | Maximum resource_fee across Soroban transactions in the ledger. | INTEGER | | No | | | soroban_min_resource_fee | Minimum resource_fee across Soroban transactions in the ledger. | INTEGER | | No | | | soroban_sum_non_refundable_resource_fee_charged | Sum of non_refundable_resource_fee_charged across Soroban transactions. Covers CPU instructions, read bytes, write bytes, and bandwidth. Charged based on declared resources regardless of tx success/failure. | INTEGER | | No | | | soroban_max_non_refundable_resource_fee_charged | Maximum non_refundable_resource_fee_charged across Soroban transactions in the ledger. | INTEGER | | No | | | soroban_min_non_refundable_resource_fee_charged | Minimum non_refundable_resource_fee_charged across Soroban transactions in the ledger. | INTEGER | | No | | | soroban_sum_refundable_resource_fee_charged | Sum of refundable_resource_fee_charged across Soroban transactions. Covers rent, events, and return value. Based on actual usage; 0 for failed transactions. | INTEGER | | No | | | soroban_max_refundable_resource_fee_charged | Maximum refundable_resource_fee_charged across Soroban transactions in the ledger. | INTEGER | | No | | | soroban_min_refundable_resource_fee_charged | Minimum refundable_resource_fee_charged across Soroban transactions in the ledger. | INTEGER | | No | | | soroban_sum_resource_fee_refund | Sum of resource_fee_refund across Soroban transactions. NOTE: Currently broken -- always 0. | INTEGER | | No | | | soroban_max_resource_fee_refund | Maximum resource_fee_refund across Soroban transactions. NOTE: Currently broken -- always 0. | INTEGER | | No | | | soroban_min_resource_fee_refund | Minimum resource_fee_refund across Soroban transactions. NOTE: Currently broken -- always 0. | INTEGER | | No | | | soroban_sum_rent_fee_charged | Sum of rent_fee_charged across Soroban transactions. This is the portion of refundable_resource_fee_charged that went to ledger entry TTL extensions. | INTEGER | | No | | | soroban_max_rent_fee_charged | Maximum rent_fee_charged across Soroban transactions in the ledger. | INTEGER | | No | | | soroban_min_rent_fee_charged | Minimum rent_fee_charged across Soroban transactions in the ledger. Will be 0 for ledgers where all Soroban transactions failed. | INTEGER | | No | | | soroban_surge_txn_count | Number of Soroban transactions in the ledger where inclusion_fee_charged exceeded the base fee (100 stroops per operation), indicating surge pricing. | INTEGER | | No | | | soroban_surge_operation_count | Total operations in Soroban transactions that experienced surge pricing in the ledger. | INTEGER | | No | | | soroban_is_surge_ledger | True if any Soroban transaction in the ledger experienced surge pricing. | BOOLEAN | | No | | | closed_at | Timestamp in UTC when the ledger closed and was committed to the network. Sourced from history_ledgers. | TIMESTAMP | | No | | | fee_pool | The cumulative fee pool balance (in stroops) as of this ledger's close, sourced from history_ledgers. This is a running total of all transaction fees ever collected by the network, not the fees for this individual ledger. | INTEGER | | No | | | airflow_start_ts | Timestamp when the Airflow DAG run started. Used for pipeline metadata and debugging. | STRING | | No | | --- ## Trade Agg ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | day_agg | | Partition Field(s) | day_agg (MONTH partition) | | Clustered Field(s) | asset_a, asset_b | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.trade_agg) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | day_agg | Date from which all metrics are aggregated. In the Trade Aggregations table, the day_agg represents the day in which the daily aggregations were built. | DATE | | Yes | | | asset_a_type | The identifier for type of asset code used for the sold asset within the trade. Notes: XLM is the native asset to the network. XLM has no asset code or issuer representation and will instead be displayed with an asset type of 'native'. | STRING | | Yes | | | asset_a_code | The 4 or 12 character code of the sold asset within a trade. Notes: Asset codes have no guarantees of uniqueness. The combination of asset code, issuer and type represents a distinct asset. | STRING | | No | | | asset_a_issuer | The account address of the original asset issuer for the sold asset within a trade. | STRING | | No | | | asset_a | Hashed id for the selling_asset. | INTEGER | | Yes | | | asset_b_type | The identifier for type of asset code used for the bought asset within the trade. Notes: XLM is the native asset to the network. XLM has no asset code or issuer representation and will instead be displayed with an asset type of 'native'. | STRING | | Yes | | | asset_b_code | The 4 or 12 character code of the bought asset within a trade. Notes: Asset codes have no guarantees of uniqueness. The combination of asset code, issuer and type represents a distinct asset. | STRING | | No | | | asset_b_issuer | The account address of the original asset issuer for the bought asset within a trade. | STRING | | No | | | asset_b | Hashed id for the buying_asset. | INTEGER | | Yes | | | trade_count_daily | The count of trades executed against the network in a day. | INTEGER | | Yes | | | asset_a_volume_daily | The total raw amount of the asset being sold in all trades within that day. | FLOAT | | Yes | | | asset_b_volume_daily | The total raw amount of the asset being bought in all trades within that day. | FLOAT | | Yes | | | avg_price_daily | The total amount of the asset being bought in all trades divided by the total amount of the asset being sold in all trades, within that day. | FLOAT | | Yes | | | high_price_daily | The highest price obtained from the ratio between denominator and numerator for that day, for the selling:buying asset price. | FLOAT | | Yes | | | low_price_daily | The lowest price obtained from the ratio between denominator and numerator for that day, for the selling:buying asset price. | FLOAT | | Yes | | | open_n_daily | The opening price of the numerator within that day, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | open_d_daily | The opening price of the denominator within that day, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | close_n_daily | The closing price of the numerator within that day, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | close_d_daily | The closing price of the denominator within that day, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | trade_count_weekly | The count of trades executed against the network during the past 7 days. | INTEGER | | Yes | | | asset_a_volume_weekly | The total amount of the asset being sold in all trades during the past 7 days. | FLOAT | | Yes | | | asset_b_volume_weekly | The total amount of the asset being bought in all trades during the past 7 days. | FLOAT | | Yes | | | avg_price_weekly | The total amount of the asset being bought in all trades divided by the total amount of the asset being sold in all trades, during the past 7 days. | FLOAT | | Yes | | | high_price_weekly | The highest price obtained from the ratio between denominator and numerator during the past 7 days, for the selling:buying asset price. | FLOAT | | Yes | | | low_price_weekly | The lowest price obtained from the ratio between denominator and numerator during the past 7 days, for the selling:buying asset price. | FLOAT | | Yes | | | open_n_weekly | The opening price of the numerator during the past 7 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | open_d_weekly | The opening price of the denominator during the past 7 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | close_n_weekly | The closing price of the numerator during the past 7 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | close_d_weekly | The closing price of the denominator during the past 7 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | trade_count_monthly | The count of trades executed against the network during the past 30 days. | INTEGER | | Yes | | | asset_a_volume_monthly | The total amount of the asset being sold in all trades during the past 30 days. | FLOAT | | Yes | | | asset_b_volume_monthly | The total amount of the asset being bought in all trades during the past 30 days. | FLOAT | | Yes | | | avg_price_monthly | The total amount of the asset being bought in all trades divided by the total amount of the asset being sold in all trades, during the past 30 days. | FLOAT | | Yes | | | high_price_monthly | The highest price obtained from the ratio between denominator and numerator during the past 30 days, for the selling:buying asset price. | FLOAT | | Yes | | | low_price_monthly | The lowest price obtained from the ratio between denominator and numerator during the past 30 days, for the selling:buying asset price. | FLOAT | | Yes | | | open_n_monthly | The opening price of the numerator during the past 30 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | open_d_monthly | The opening price of the denominator during the past 30 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | close_n_monthly | The closing price of the numerator during the past 30 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | close_d_monthly | The closing price of the denominator during the past 30 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | trade_count_yearly | The count of trades executed against the network during the past 365 days. | INTEGER | | Yes | | | asset_a_volume_yearly | The total amount of the asset being sold in all trades during the past 365 days. | FLOAT | | Yes | | | asset_b_volume_yearly | The total amount of the asset being bought in all trades during the past 365 days. | FLOAT | | Yes | | | avg_price_yearly | The total amount of the asset being bought in all trades divided by the total amount of the asset being sold in all trades, during the past 365 days. | FLOAT | | Yes | | | high_price_yearly | The highest price obtained from the ratio between denominator and numerator during the past 365 days, for the selling:buying asset price. | FLOAT | | Yes | | | low_price_yearly | The lowest price obtained from the ratio between denominator and numerator during the past 365 days, for the selling:buying asset price. | FLOAT | | Yes | | | open_n_yearly | The opening price of the numerator during the past 365 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | open_d_yearly | The opening price of the denominator during the past 365 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | close_n_yearly | The closing price of the numerator during the past 365 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | | close_d_yearly | The closing price of the denominator during the past 365 days, for the selling:buying asset prices ratio. | INTEGER | | Yes | | --- ## TVL Agg ## Table Metadata | Property | Configuration | | ------------------ | ------------- | | Natural Key(s) | day | | Partition Field(s) | N/A | | Clustered Field(s) | N/A | | Documentation | N/A | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | day | The date for the aggregation result. | date | | Yes | | | accounts_tvl_usd | The total value locked (TVL) denominated in USD for a given date. Aggregated across accounts selling liabilities | float | | Yes | | | trustlines_tvl_usd | The total value locked (TVL) denominated in USD for a given date. Aggregated across trustlines selling liabilities | float | | Yes | | | total_tvl_usd | The total value locked (TVL) denominated in USD for a given date. Aggregated across relevant ledger entries (e.g., accounts, trustlines). | float | | Yes | | --- ## Silver 🥈 Decoded, transformed, and filtered data. These tables transform the bronze tables into easier to use tables by joining and flattening tables and filtering out information for specific use cases. ## When to use These tables are best used for a more curated experience while still needing to search and filter for specific use cases such as information on a specific contract. One example is getting relevant Stellar events to calculate circulating supply by using the `token_transfers_raw` table. In theory you can get the same information from the bronze `history_contract_events` table but the `token_transfers_raw` table has already processed, filtered, and flattened the events relevant to SEP-41 events and classic operations. ## Slowly Changing Dimensions (SCD Type 2) Some silver tables are modeled as [SCD Type 2](https://github.com/stellar/stellar-dbt-public/blob/master/docs/snapshot.md) tables. This means they track the full history of changes to a record over time by keeping multiple versions with `valid_from` / `valid_to` ranges. These tables are especially useful for answering historical questions such as: - What was the state of a contract at a given point in time? - How did an account, trustline, or balance evolve across different periods? - Which liquidity pools were active at a given date, and how did their properties change over time? :::note[Daily Snapshot Frequency] Snapshots run on a daily basis, meaning they capture inter-day changes, but not multiple changes that occur within the same day. ::: --- ## Account Signers Current ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | account_id, signer, closed_at | | Partition Field(s) | N/A | | Clustered Field(s) | account_id, signer, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.account_signers_current) | ## Column Details Column details are equivalent to the bronze [Account Signers](../bronze/account-signers.mdx) table. Filters down to only latest state information. --- ## Accounts Current ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | account_id, closed_at | | Partition Field(s) | N/A | | Clustered Field(s) | account_id, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.accounts_current) | ## Column Details Column details are equivalent to the bronze [Accounts](../bronze/accounts.mdx) table. Filters down to only latest state information. --- ## Accounts Snapshot ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | account_id, valid_from | | Partition Field(s) | valid_to (MONTH partition) | | Clustered Field(s) | account_id | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.accounts_snapshot) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | account_id | The unique identifier of the account. | STRING | | Yes | | | balance | The number of units of XLM held by the account. | FLOAT | | Yes | The `accounts` table only reports monetary balances for XLM. Any other asset class is reported in the `trust_lines` table. | | buying_liabilities | The sum of all buy offers owned by this account for XLM only. | FLOAT | | Yes | The `accounts` table only reports monetary balances for XLM. Any other asset class is reported in the `trust_lines` table. For buy offers, the account must hold the amount of asset to complete the transaction. | | selling_liabilities | The sum of all sell offers owned by this account for XLM only. | FLOAT | | Yes | The `accounts` table only reports monetary balances for XLM. Any other asset class is reported in the `trust_lines` table. | | sequence_number | The account's current sequence number. The sequence number controls operations applied to an account. Operations must submit a unique sequence number that is incremented by 1. | INTEGER | | Yes | Natural Key. Required Field. | | num_subentries | The total number of ledger entries connected to this account. Ledger entries include: trustlines, offers, signers, and data entries. | INTEGER | | Yes | Each entry on a ledger takes up space, which is expensive to store. Minimum balance is calculated by (2 + num_subentries - num_sponsoring + num_sponsored) \* 0.5XLM. | | inflation_destination | Deprecated: The account address to receive an inflation payment when disbursed. | STRING | | Yes | Inflation was discontinued in 2019 by validator vote. | | flags | Denotes enabling/disabling of certain asset issuer privileges. | INTEGER | 0 - None, Default1 - Auth Required (all trustlines by default are untrusted and require manual trust established)2 - Auth Revocable (allows trustlines to be revoked if account no longer trusts asset)4 - Auth Immutable (all auth flags are read only when set)8 - Auth Clawback Enabled (asset can be clawed back from the user) | Yes | Flags can have values: 0=None, 1=Auth Required, 2=Auth Revocable, 4=Auth Immutable, 8=Auth Clawback Enabled. | | home_domain | URL of home domain linked to wallet. | STRING | | Yes | | | master_weight | The weight of the master key, which is the private key for this account. | INTEGER | Integers from 1 to 255 | Yes | If master key = 0, account is locked. | | threshold_low | The sum of the weight of all signatures required for low threshold operations. | INTEGER | | Yes | Low: Allow Trust, Set Trust Line Flags, Bump Sequence, Claim Claimable Balance. | | threshold_medium | The sum of the weight of all signatures required for medium threshold operations. | INTEGER | | Yes | Medium: Everything Else. | | threshold_high | The sum of the weight of all signatures required for high threshold operations. | INTEGER | | Yes | High: Account Merge, Set Options. | | last_modified_ledger | The ledger sequence number when the ledger entry was last modified. | INTEGER | | Yes | Natural Key. Cluster Field. Deletion does not count as modification. | | ledger_entry_change | Code describing the ledger entry change type applied. | INTEGER | 0 - Ledger Entry Created1 - Ledger Entry Updated2 - Ledger Entry Deleted3 - Ledger Entry State (value of the entry) | Yes | Values: 0=Created, 1=Updated (not valid for claimable balances), 2=Deleted. | | deleted | Indicates whether the ledger entry has been deleted. | BOOLEAN | | Yes | Once deleted, cannot be recovered. | | sponsor | The account address of the sponsor paying the reserves for this ledger entry. | STRING | | No | Sponsors can be accounts, signers, claimable balances, trust lines. | | num_sponsored | The number of reserves sponsored for this account. | INTEGER | | No | Defaults to 0. | | num_sponsoring | The number of reserves sponsored by this account. | INTEGER | | No | Defaults to 0. | | sequence_ledger | The unsigned 32-bit ledger number of the sequence number's age. | INTEGER | | No | Reflects the last time an account touched its sequence number. | | sequence_time | | TIMESTAMP | | No | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. | DATETIME | | Yes | Represents the interval of ledgers processed. | | closed_at | Timestamp in UTC when this ledger closed and committed. | TIMESTAMP | | Yes | Ledgers close ~every 5 seconds. | | ledger_sequence | The sequence number of this ledger. | INTEGER | | Yes | Cluster Field. Unique per ledger. | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. | TIMESTAMP | | Yes | Useful for data engineering, not for ad hoc analysis. | | airflow_start_ts | The timestamp when the airflow job starts. | STRING | | Yes | Used to detect backfill jobs. | | valid_from | The timestamp when this snapshot row was first inserted and became effective. | TIMESTAMP | | Yes | Helps track changes over time. | | valid_to | The timestamp when this row is no longer valid. | TIMESTAMP | | No | If null, the setting is currently active. | ## Example Business Questions 1. Point-in-time query (as-of balance) ```sql SELECT account_id, balance FROM `crypto-stellar.snapshots.accounts_snapshot` WHERE account_id = 'ACC123' DATE('2025-01-15') BETWEEN DATE(valid_from) AND COALESCE(DATE(valid_to), '9999-12-31') LIMIT 100 ``` 2. Active snapshot (current records only) ```sql SELECT account_id, balance, home_domain FROM `crypto-stellar.snapshots.accounts_snapshot` WHERE valid_to IS NULL LIMIT 100 ``` 3. Changes in February 2025 (partition pruning) ```sql SELECT account_id, COUNT(*) AS num_changes FROM `crypto-stellar.snapshots.accounts_snapshot` WHERE DATE_TRUNC(valid_to, MONTH) = '2025-02-01' GROUP BY account_id LIMIT 100 ``` --- ## Claimable Balances Current ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | balance_id, closed_at | | Partition Field(s) | N/A | | Clustered Field(s) | asset_id, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.claimable_balances_current) | ## Column Details Column details are equivalent to the bronze [Claimable Balances](../bronze/claimable-balances.mdx) table. Filters down to only latest state information. --- ## Config Settings Current ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | config_setting_id, closed_at | | Partition Field(s) | N/A | | Clustered Field(s) | last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.config_settings_current) | ## Column Details Column details are equivalent to the bronze [Config Settings](../bronze/config-settings.mdx) table. Filters down to only latest state information. --- ## Contract Code Current ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | contract_code_hash, closed_at | | Partition Field(s) | N/A | | Clustered Field(s) | last_modified_ledger, contract_code_hash | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.contract_code_current) | ## Column Details Column details are equivalent to the bronze [Contract Code](../bronze/contract-code.mdx) table. Filters down to only latest state information. --- ## Contract Data Current ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | ledger_key_hash, closed_at | | Partition Field(s) | N/A | | Clustered Field(s) | last_modified_ledger, contract_id | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.contract_data_current) | ## Column Details Column details are equivalent to the bronze [Contract Data](../bronze/contract-data.mdx) table. Filters down to only latest state information. --- ## Contract Data Snapshot ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | contract_id, ledger_key_hash, valid_from | | Partition Field(s) | valid_to (MONTH partition) | | Clustered Field(s) | contract_id, ledger_key_hash | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.contract_data_snapshot) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | contract_id | Soroban contract id. | STRING | | Yes | | | contract_key_type | Contract key type which is an ScVal. | STRING | ScValTypeScvContractInstance, ScValTypeScvLedgerKeyContractInstance, ScValTypeScvLedgerKeyNonce | No | | | contract_durability | Contract can either be temporary or persistent. | STRING | | No | | | asset_code | The 4 or 12 character code representation of the asset on the network. | STRING | | No | Asset codes have no guarantees of uniqueness. The combination of asset code, issuer and type represents a distinct asset. | | asset_issuer | The account address of the original asset issuer that created the asset. | STRING | | No | | | asset_type | The identifier for type of asset code, can be alphanumeric (4 or 12 characters) or the native asset to the network, XLM. | STRING | native, alphanum4, alphanum12 | No | XLM is the native asset. XLM has no asset code or issuer and is displayed with asset type = 'native'. | | balance_holder | The address/account that holds the balance of the asset in contract data. | STRING | | No | | | balance | The number of units of XLM held by the account. | STRING | | No | The `accounts` table only reports balances for XLM. Any other asset class is reported in `trust_lines`. | | last_modified_ledger | The ledger sequence number when the ledger entry was last modified. | INTEGER | | Yes | Natural Key. Cluster Field. Deletion does not count as modification. | | ledger_entry_change | Code describing the ledger entry change type applied. | INTEGER | 0 - Ledger Entry Created1 - Ledger Entry Updated2 - Ledger Entry Deleted3 - Ledger Entry State (value of the entry) | Yes | Not every entry can be updated. Updates not valid for claimable balances. | | ledger_sequence | | INTEGER | | Yes | | | ledger_key_hash | | STRING | | Yes | | | key | The encoded key used to identify a specific piece of contract data. Has two components: type and value. | JSON | | No | | | key_decoded | The human-readable/decoded version of the key. | JSON | | No | | | val | The encoded value associated with the key in the contract data. Has two components: type and value. | JSON | | No | | | val_decoded | The human-readable/decoded version of the value. | JSON | | No | | | contract_data_xdr | The XDR (External Data Representation) encoding of the contract data. | STRING | | No | XDR ensures interoperability across systems. | | closed_at | Timestamp in UTC when this ledger closed and committed. | TIMESTAMP | | Yes | Ledgers close ~every 5 seconds. | | deleted | Indicates whether the ledger entry has been deleted. | BOOLEAN | | Yes | Once deleted, cannot be recovered. History is maintained. | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. | DATETIME | | Yes | Proxy for closed_at for a ledger. | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted. | TIMESTAMP | | Yes | For data engineering purposes, not ad hoc analysis. | | airflow_start_ts | The timestamp when the airflow job starts. | STRING | | No | Used to detect backfill jobs. | | valid_from | The timestamp when this snapshot row was first inserted and became effective. | TIMESTAMP | | Yes | Tracks changes over time. | | valid_to | The timestamp when this row is no longer valid. | TIMESTAMP | | No | If null, the setting is currently active. | ## Example Business Questions 1. Point-in-time query (balance/value as of a given date) ```sql SELECT contract_id, ledger_key_hash, val_decoded FROM `crypto-stellar.snapshots.contract_data_snapshot` WHERE contract_id = 'C123' AND ledger_key_hash = 'abc123' DATE('2025-02-10') BETWEEN DATE(valid_from) AND COALESCE(DATE(valid_to), '9999-12-31') LIMIT 100 ``` 2. All active contracts (latest state only) ```sql SELECT contract_id, ledger_key_hash, val_decoded, balance, contract_durability FROM `crypto-stellar.snapshots.contract_data_snapshot` WHERE valid_to IS NULL LIMIT 100 ``` 3. Recent updates ```sql SELECT contract_id, ledger_key_hash, closed_at, ledger_entry_change FROM `crypto-stellar.snapshots.contract_data_snapshot` WHERE closed_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 2 DAY) LIMIT 100 ``` --- ## Enriched History Operations Soroban ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | op_id | | Partition Field(s) | closed_at (DAY partition) | | Clustered Field(s) | ledger_sequence, transaction_id, op_account_id, type | | Documentation | [dbt_docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.enriched_history_operations_soroban) | ## Column Details Column details are equivalent to the silver [Enriched History Operations](./enriched-history-operations.mdx) table. Filters down to only operation types 24, 25, and 26 --- ## Enriched History Operations ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | op_id | | Partition Field(s) | closed_at (DAY partition) | | Clustered Field(s) | ledger_sequence, transaction_id, op_account_id, type | | Documentation | [dbt_docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.enriched_history_operations) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Source Table | Notes | | --- | --- | --- | --- | --- | --- | --- | | account | The new resulting account address that is created and funded (create operation) The account address that is being removed and merged into another account (merge operation) | string | | | history_operations | Part of the original `details` object in the history_operations table | | amount | Float representation of the amount of an asset sent/offered/etc | float | | | history_operations | | | asset_code | The 4 or 12 character code representation of the asset on the network | string | | | history_operations | | | asset_issuer | The account address of the original asset issuer that created the asset | string | | | history_operations | | | asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | | history_operations | | | authorize | Indicates whether the trustline is authorized. 0 is the account is not authorized to transact with the asset in any way. 1 if the account is authorized to transact with the asset. 2 if the account is authorized to maintain orders, but not to perform other transactions. | boolean | | | history_operations | | | balance_id | The unique identifier of the claimable balance. The id is comprised of 8 character type code + SHA-256 hash of the history operation id that created the balance. The balance id can be joined back to the `claimable_balances` table to gather more details about the balance | string | | | history_operations | | | buying_asset_code | The 4 or 12 character code representation of the asset that is either bought or offered to buy in a trade | string | | | history_operations | | | buying_asset_issuer | The account address of the original asset issuer that created the asset bought or offered to buy | string | | | history_operations | | | buying_asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | | history_operations | | | from | The account address from which the payment originates (the sender account) | string | | | history_operations | | | funder | When a new account is created, an account address "funds" the new account | string | | | history_operations | | | high_threshold | The sum of the weight of all signatures that sign a transaction for the high threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | | history_operations | Each operation falls under a specific threshold category: low, medium or high. Thresholds define the level of privilege an operation needs in order to succeed (this is a security measure) Low Security: Allow Trust, Set Trust Line Flags, Bump Sequence and Claim Claimable Balance Medium Security: Everything Else High Security: Account Merge, Set Options | | home_domain | The home domain used for the stellar.toml file discovery | string | | | history_operations | | | inflation_dest | The account address specifying where to send inflation funds. The concept of inflation on the network has been discontinued | string | | | history_operations | Inflation was retired from the network in 2019. | | into | The account address receiving the deleted account's lumens. This is the account in which the intended deleted account will be merged | string | | | history_operations | | | limit | The upper bound amount of an asset that an account can hold | float | | | history_operations | | | low_threshold | The sum of the weight of all signatures that sign a transaction for the low threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | | history_operations | Each operation falls under a specific threshold category: low, medium or high. Thresholds define the level of privilege an operation needs in order to succeed (this is a security measure) Low Security: Allow Trust, Set Trust Line Flags, Bump Sequence and Claim Claimable Balance Medium Security: Everything Else High Security: Account Merge, Set Options | | master_key_weight | An accounts private key is called the master key. For signing transactions, the account holder can specify a weight for the master key, which contributes to thresholds validation when processing a transaction | integer | Integers from 1 to 255 | | history_operations | | | med_threshold | The sum of the weight of all signatures that sign a transaction for the medium threshold operation. The weight must exceed the set threshold for the operation to succeed. | integer | | | history_operations | Each operation falls under a specific threshold category: low, medium or high. Thresholds define the level of privilege an operation needs in order to succeed (this is a security measure) Low Security: Allow Trust, Set Trust Line Flags, Bump Sequence and Claim Claimable Balance Medium Security: Everything Else High Security: Account Merge, Set Options | | name | The manage data operation allows an account to write and store data directly on the ledger in a key value pair format. The name is the key for a data entry. | string | | | history_operations | | | offer_id | The unique id for the offer. This id can be joined with the `offers` table | integer | | | history_operations | | | path | Path payments maximize the best exchange rate path when sending money from one asset to another asset. The intermediary assets that this path hops through will be reported in the record. This feature is especially useful when the market between the original asset pair is illiquid | array[record] | | | history_operations | Up to 6 paths are permitted for a single payment. Example: sending EUR -> MXN could look like EUR -> BTC -> CNY -> XLM -> MXN to maximize the best exchange rate Payments are atomic, so if an exchange in the middle of a path payment fails, the entire payment will fail which means the user will keep their original funds. They will not be stuck with an intermediary asset in the event of payment failure. | | price | The ratio of selling asset to buying asset. This is a number representing how many units of a selling asset it takes to get 1 unit of a buying asset | float | | | history_operations | | | d | Precise representation of the buy and sell price of a trade. The `d` is the denominator. When taken with n/d you will get the price | integer | | | history_operations | | | n | Precise representation of the buy and sell prices of a trade. The `n` is the numerator. When taken with n/d you will get the price. | integer | | | history_operations | | | selling_asset_code | The 4 or 12 character code representation of the asset that is either sold or offered to sell in a trade | string | | | history_operations | | | selling_asset_issuer | The account address of the original asset issuer that created the asset sold or offered to sell | string | | | history_operations | | | selling_asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | | history_operations | | | set_flags | Array of numeric values of the flags set for a given trustline in the operation | array[integer] | 1 - Auth Required2 - Auth Revocable4 - Auth Immutable | | history_operations | | | set_flags_s | Array of string values of the flags set for a given trustline in the operation | array[string] | Auth RequiredAuth RevocableAuth Immutable | | history_operations | | | signer_key | The address of the signer which is no longer sponsored | string | | | history_operations | | | signer_weight | The weight of the new signer. For transactions, multiple accounts can sign a transaction from a source account. This weight contributes towards calculating whether the transaction exceeds the specified threshold weight to complete the transaction | integer | | | history_operations | | | source_amount | The originating amount sent designated in the source asset | float | | | history_operations | | | source_asset_code | The 4 or 12 character code representation of the asset that is originally sent | string | | | history_operations | | | source_asset_issuer | The account address of the original asset issuer that created the asset sent | string | | | history_operations | | | source_asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | | history_operations | | | source_max | The maxium amount to be sent, designated in the source asset | float | | | history_operations | | | starting_balance | The amount of XLM to send to the newly created account. The account starting balance will need to exceed the minimum balance necessary to hold an account on the Stellar Network | float | | | history_operations | | | to | The address of the account receiving the payment funds | string | | | history_operations | | | trustee | The issuing account address (only present for `credit` asset types) | string | | | history_operations | | | trustor | The trusting account address, or the account being authorized or unauthorized | string | | | history_operations | | | trustline_asset | The asset of the trustline which is no longer sponsored | string | | | history_operations | | | value | The manage data operation allows an account to write and store data directly on the ledger in a key value pair format. The value is the value of a key for a data entry. | string | | | history_operations | | | clear_flags | Array of numeric values of the flags cleared for a given trustline in the operation. If the flag was originally set, this will delete the flag | array[integer] | 1 - Auth Required2 - Auth Revocable4 - Auth Immutable | | history_operations | | | clear_flags_s | Array of string values of the flags cleared for a given trustline in the operation. If the flag was originally set, this will delete the flag | array[string] | Auth RequiredAuth RevocableAuth Immutable | | history_operations | | | destination_min | The minimum amount to be received, designated in the expected destination asset | string | | | history_operations | | | bump_to | The new desired value of the source account's sequence number | string | | | history_operations | | | sponsor | The account address of another account that maintains the minimum balance in XLM for the source account to complete operations | string | | | history_operations | | | sponsored_id | The account address of the account which will be sponsored | string | | | history_operations | | | begin_sponsor | The account address of the account which initiated the sponsorship | string | | | history_operations | | | authorize_to_maintain_liabilities | Indicates whether the trustline is authorized. 0 is the account is not authorized to transact with the asset in any way. 1 if the account is authorized to transact with the asset. 2 if the account is authorized to maintain orders, but not to perform other transactions. | boolean | | | history_operations | | | clawback_enabled | Indicates whether the asset can be clawed back by the asset issuer | boolean | | | history_operations | | | liquidity_pool_id | Unique identifier for a liquidity pool | string | | | history_operations | | | reserve_a_asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | | history_operations | | | reserve_a_asset_code | The 4 or 12 character code representation of the asset of one of the two asset pairs in a liquidity pool | string | | | history_operations | | | reserve_a_asset_issuer | The account address of the original asset issuer that created one of the two asset pairs in the liquidity pool | string | | | history_operations | | | reserve_a_max_amount | The maximum amount of reserve a that can be deposited into the pool. | float | | | history_operations | | | reserve_a_deposit_amount | The amount of reserve a that ended up actually deposited into the pool | float | | | history_operations | | | reserve_b_asset_type | The identifier for type of asset code, can be a alphanumeric with 4 characters, 12 characters or the native asset to the network, XLM. | string | credit_alphanum4credit_alphanum12native | | history_operations | | | reserve_b_asset_code | The 4 or 12 character code representation of the asset of one of the two asset pairs in a liquidity pool | string | | | history_operations | | | reserve_b_asset_issuer | The account address of the original asset issuer that created one of the two asset pairs in the liquidity pool | string | | | history_operations | | | reserve_b_max_amount | The maximum amount of reserve b that can be deposited into the pool. | float | | | history_operations | | | reserve_b_deposit_amount | The amount of reserve b that ended up actually deposited into the pool. | float | | | history_operations | | | min_price | The floating point value indicating the minimum exchange rate for this deposit operation. Reported as Reserve A / Reserve B | float | | | history_operations | | | min_price_r | A fractional representation of the prices of the two assets in a pool. The n is the numerator (value of asset a) and the d is the denominator (value of asset b) | array[record] | | | history_operations | | | max_price | The floating point value indicating the maximum exchange rate for this deposit operation. Reported as Reserve A / Reserve B | float | | | history_operations | | | max_price_r | A fractional representation of the prices of the two assets in a pool. The n is the numerator (value of asset a) and the d is the denominator (value of asset b) | array[record] | | | history_operations | | | shares_received | A floating point number representing the number of pool shares received for this deposit. A pool share is a compilation of both asset a and asset b reserves. It is not possible to own only asset a or asset b in a pool | float | | | history_operations | | | reserve_a_min_amount | The minimum amount of reserve a that can be withdrawn from the pool. | float | | | history_operations | | | reserve_b_min_amount | The minimum amount of reserve b that can be withdrawn from the pool. | float | | | history_operations | | | shares | The number of shares withdrawn from the pool. It is not possible to withdraw only asset a or asset b, equal value must be withdrawn from the pool | float | | | history_operations | | | reserve_a_withdraw_amount | The amount of reserve a that ended up actually withdrawn from the pool. | float | | | history_operations | | | reserve_b_withdraw_amount | The amount of reserve b that ended up actually withdrawn from the pool. | float | | | history_operations | | | op_application_order | The order number in the transaction set in which the operation is executed. The application order and transaction id is a natural key that comprises the (operation) id | integer | | | history_operations | | | op_id | Unique identifier for an operation | integer | | | history_operations | | | op_source_account | The account address that originates the operation | string | | | history_operations | | | op_source_account_muxed | If an account is multiplexed (muxed), the virtual account address that originates the operation | string | | | history_operations | | | transaction_id | The transaction identifier in which the operation executed. There can be up to 100 operations in a given transaction | integer | | | history_operations | | | type | The number indicating which type of operation this operation executes | integer | --- ## Evicted Keys Snapshot ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | ledger_key_hash, valid_from | | Partition Field(s) | valid_to (MONTH partition) | | Clustered Field(s) | ledger_key_hash | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.evicted_keys_snapshot) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | ledger_key_hash | Hash of the ledgerKey, which is a subset of the ledgerEntry. The subset of ledgerEntry fields depends on ledgerEntryType. | STRING | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed. | TIMESTAMP | | Yes | Ledgers are expected to close ~every 5 seconds. | | is_evicted | Status flag indicating whether an entry is evicted or not. | BOOLEAN | false, true | Yes | false = key has been restored. | | ledger_sequence | The sequence number of this ledger, representing its order in the Stellar blockchain. | INTEGER | | Yes | Cluster Field. Unique per ledger. | | valid_from | The timestamp when this snapshot row was first inserted and became effective. | TIMESTAMP | | Yes | Helps track changes over time. | | valid_to | The timestamp when this row is no longer valid. | TIMESTAMP | | No | If null, the setting is currently active. | ## Example Business Questions 1. Evicted keys as of today ```sql SELECT ledger_key_hash, is_evicted FROM `crypto-stellar.snapshots.evicted_keys_snapshot` WHERE valid_to IS NULL AND is_evicted = TRUE LIMIT 100 ``` 2. Point-in-time eviction status ```sql SELECT ledger_key_hash, is_evicted FROM `crypto-stellar.snapshots.evicted_keys_snapshot` WHERE ledger_key_hash = 'abc123' AND AND DATE('2025-03-10') BETWEEN DATE(valid_from) AND COALESCE(DATE(valid_to), '9999-12-31') LIMIT 100 ``` 3. Monthly Key Restorations ```sql WITH with_lag AS ( SELECT ledger_key_hash, valid_from, is_evicted, LAG(is_evicted) OVER ( PARTITION BY ledger_key_hash ORDER BY valid_from ) AS prev_status FROM `crypto-stellar.snapshots.evicted_keys_snapshot` ) SELECT DATE_TRUNC(valid_from, MONTH) AS month, COUNTIF(prev_status = TRUE AND is_evicted = FALSE) AS restorations_count FROM with_lag GROUP BY month ORDER BY month LIMIT 100 ``` --- ## Liquidity Pools Current ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | liquidity_pool_id, closed_at | | Partition Field(s) | N/A | | Clustered Field(s) | liquidity_pool_id, asset_a_id, asset_b_id, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.liquidity_pools_current) | ## Column Details Column details are equivalent to the bronze [Liquidity Pools](../bronze/liquidity-pools.mdx) table. Filters down to only latest state information. --- ## Liquidity Pools Snapshot ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | liquidity_pool_id, valid_from | | Partition Field(s) | valid_to (MONTH partition) | | Clustered Field(s) | liquidity_pool_id | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.liquidity_pools_snapshot) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | liquidity_pool_id | Unique identifier for a liquidity pool. | STRING | | Yes | Natural Key. Cluster Field. Cannot be duplicated for same asset pair. | | type | Mechanism that calculates pricing and division of shares for the pool. | STRING | constant_product | Yes | Initially only constant product pools are supported. See [CAP-38](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0038.md). | | fee | Basis points charged as trade fee. | INTEGER | Default = 30 | Yes | Fees are distributed immediately to accounts as transactions complete. | | trustline_count | Total number of accounts with trustlines authorized to the pool. | INTEGER | | Yes | Revoking authorization on an asset will withdraw accounts from all pools with that asset. | | pool_share_count | Total number of pool shares representing participation in the liquidity pool. | FLOAT | | Yes | Shares are not transferable. Can only be adjusted via deposit/withdraw. | | asset_a_type | Asset type for one side of the pool (sold asset). | STRING | credit_alphanum4credit_alphanum12native | Yes | XLM has type = 'native'. No asset code/issuer. | | asset_a_code | Asset code (4 or 12 chars) for sold asset in trade. | STRING | | No | Asset codes alone are not unique. Combination with issuer + type is unique. | | asset_a_issuer | Account address of the original issuer of the sold asset. | STRING | | No | | | asset_a_id | Farm Hash encoding of asset code + issuer + type for asset A. | INTEGER | | No | | | asset_a_amount | Raw number of tokens locked in the pool for asset A. | FLOAT | | Yes | | | asset_b_type | Asset type for the other side of the pool. | STRING | credit_alphanum4credit_alphanum12native | Yes | XLM has type = 'native'. | | asset_b_code | Asset code (4 or 12 chars) for other asset in trade. | STRING | | No | Asset codes alone are not unique. Combination with issuer + type is unique. | | asset_b_issuer | Account address of the original issuer of the other asset. | STRING | | No | | | asset_b_id | Farm Hash encoding of asset code + issuer + type for asset B. | INTEGER | | No | | | asset_b_amount | Raw number of tokens locked in the pool for asset B. | FLOAT | | Yes | | | last_modified_ledger | Ledger sequence number when the pool entry was last modified. | INTEGER | | Yes | Natural Key. Cluster Field. Deletion does not count as modification. | | ledger_entry_change | Code describing the ledger entry change type. | INTEGER | 0 - Ledger Entry Created1 - Ledger Entry Updated2 - Ledger Entry Deleted3 - Ledger Entry State (value of the entry) | Yes | Updates not valid for claimable balances. | | deleted | Indicates whether the liquidity pool entry was deleted. | BOOLEAN | true, false | Yes | Deleted entries cannot be recovered; history is maintained. | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | Start date for the batch interval. | DATETIME | | Yes | Proxy for closed_at. | | closed_at | Timestamp in UTC when ledger closed and committed. | TIMESTAMP | | Yes | Ledgers close ~every 5 seconds. | | ledger_sequence | Ledger sequence number. | INTEGER | | Yes | Represents order of ledger within Stellar blockchain. | | batch_insert_ts | Timestamp in UTC when batch was inserted into DB. | TIMESTAMP | | Yes | Used for engineering/debugging, not analysis. | | airflow_start_ts | Timestamp when airflow job started. | STRING | | Yes | Helps detect backfill loads. | | valid_from | Timestamp when this snapshot row became effective. | TIMESTAMP | | Yes | Helps track changes over time. | | valid_to | Timestamp when this row is no longer valid. | TIMESTAMP | | No | Null = still active. | ## Example Business Questions 1. What was the total liquidity (asset_a_amount + asset_b_amount) across all pools on a given date? ```sql SELECT DATE('2025-01-15') AS as_of_date, SUM(asset_a_amount + asset_b_amount) AS total_liquidity FROM `crypto-stellar.snapshots.liquidity_pools_snapshot` WHERE DATE(valid_from) <= DATE('2025-01-15') AND (DATE(valid_to) > DATE('2025-01-15') OR valid_to IS NULL) AND deleted = FALSE; ``` --- ## Offers Current ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | offer_id, closed_at | | Partition Field(s) | N/A | | Clustered Field(s) | selling_asset_id, buying_asset_id, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.offers_current) | ## Column Details Column details are equivalent to the bronze [Offers](../bronze/offers.mdx) table. Filters down to only latest state information. --- ## Token Transfers ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | N/A | | Partition Field(s) | closed_at (DAY partition) | | Clustered Field(s) | contract_id, asset | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/source/source.stellar_dbt_public.crypto_stellar.token_transfers_raw) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | transaction_hash | A hex-encoded SHA-256 hash of this transaction's XDR-encoded form. | STRING | | Yes | | | transaction_id | A unique identifier for this transaction. | INTEGER | | Yes | | | operation_id | A unique identifier for this transaction. | INTEGER | | No | | | event_topic | The action type applied to the token. | STRING | | Yes | | | from | The source address for the token transfer event amount. | STRING | | No | | | to | The destination address for the token transfer event amount. | STRING | | No | | | asset | ID field for the asset code/issuer pair. It is created by concatenating the asset code, ':' and asset_issuer fields. | STRING | | Yes | | | asset_type | The identifier for type of asset code, can be an alphanumeric with 4 characters, 12 characters or the native asset to the network (XLM). | STRING | | Yes | | | asset_code | The 4 or 12 character code representation of the asset on the network. | STRING | | No | | | asset_issuer | The account address of the original asset issuer that created the asset. | STRING | | No | | | amount | **DEPRECATED, prefer `amount_raw`.** The normalized float amount of the asset. Applies a fixed 7-decimal scale (`amount_raw` \* 0.0000001) regardless of the token's declared decimal precision, so it is incorrect for any non-7-decimal token. | FLOAT | | Yes | Use `amount_raw` and scale by the token's declared decimals. | | amount_raw | The raw stroop amount of the asset. | STRING | | Yes | | | contract_id | Soroban contract id. | STRING | | Yes | | | to_muxed | The multiplexed strkey representation of the `to` address. | STRING | | No | | | to_muxed_id | The multiplexed ID used to generate the multiplexed strkey representation of the `to` address. | STRING | | No | | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | The start date for the batch interval. When taken with the date in the batch_id, the date represents the interval of ledgers processed. The batch run date can be seen as a proxy of closed_at for a ledger. | datetime | | Yes | | | batch_insert_ts | The timestamp in UTC when a batch of records was inserted into the database. This field can help identify if a batch executed in real time or as part of a backfill | timestamp | | Yes | | | closed_at | Timestamp in UTC when this ledger closed and committed to the network. Ledgers are expected to close ~every 5 seconds | timestamp | | Yes | | | ledger_sequence | The sequence number of this ledger. It represents the order of the ledger within the Stellar blockchain. Each ledger has a unique sequence number that increments with every new ledger, ensuring that ledgers are processed in the correct order. | integer | | Yes | | --- ## Trustlines Current ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | account_id, asset_type, asset_issuer, asset_code, liquidity_pool_id, closed_at | | Partition Field(s) | N/A | | Clustered Field(s) | account_id, asset_id, liquidity_pool_id, last_modified_ledger | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.trust_lines_current) | ## Column Details Column details are equivalent to the bronze [Trustlines](../bronze/trustlines.mdx) table. Filters down to only latest state information. --- ## Trustlines Snapshot ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | account_id, asset_type, asset_issuer, asset_code, liquidity_pool_id, valid_from | | Partition Field(s) | valid_to (MONTH partition) | | Clustered Field(s) | account_id, asset_id | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.trustlines_snapshot) | ## Column Details | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | | ledger_key | Unique ledger key when the trust line state last changed. | STRING | | Yes | Natural Key. | | account_id | Unique identifier of the account. | STRING | | Yes | | | asset_type | Type of asset code: 4-char, 12-char, or `native` (XLM). | STRING | credit_alphanum4credit_alphanum12native | Yes | XLM has no issuer or asset code, shown as `native`. | | asset_issuer | Account address of the original asset issuer. | STRING | | No | | | asset_code | 4- or 12-character code of the asset. | STRING | | No | Asset uniqueness = `code + issuer + type`. | | asset_id | Encoded asset identifier. | INTEGER | | No | Derived from asset code + issuer + type. | | liquidity_pool_id | Unique identifier for a liquidity pool. | STRING | | Yes | Natural Key. Cluster Field. | | balance | Number of units of asset held by this account. | FLOAT | | Yes | | | trust_line_limit | Maximum amount of this asset that the account accepts. | INTEGER | | Yes | Set when opening trust line. | | buying_liabilities | Sum of buy offers owned by this account (XLM only). | FLOAT | | Yes | Other assets reported in `trust_lines`. | | selling_liabilities | Sum of sell offers owned by this account (XLM only). | FLOAT | | Yes | Other assets reported in `trust_lines`. | | flags | Issuer privilege flags applied to the asset. | INTEGER | 0 - None, Default1 - Authorized2 - Authorized to Maintain Liabilities4 - Clawback Enabled | Yes | Flags originate from issuer account. | | last_modified_ledger | Ledger sequence number when entry was last modified. | INTEGER | | Yes | Natural Key. Cluster Field. Not a proxy for deletion. | | ledger_entry_change | Code for ledger entry change type. | INTEGER | 0 - Ledger Entry Created1 - Ledger Entry Updated2 - Ledger Entry Deleted3 - Ledger Entry State (value of the entry) | Yes | Some ledger entries cannot be updated. | | deleted | Indicates if the ledger entry was deleted. | BOOLEAN | true, false | Yes | Deleted entries remain in history. | | sponsor | Account paying reserves for this ledger entry. | STRING | | No | Can sponsor accounts, signers, claimable balances, trust lines. | | batch_id | String representation of the run id for a given DAG in Airflow. Takes the form of `scheduled__-`. Batch ids are unique to the batch and help with monitoring and rerun capabilities | string | | Yes | | | batch_run_date | Start date of batch interval. | DATETIME | | Yes | Proxy for `closed_at`. | | closed_at | Ledger close timestamp in UTC. | TIMESTAMP | | Yes | Ledgers close ~every 5s. | | ledger_sequence | Sequence number of this ledger. | INTEGER | | Yes | Cluster Field. Unique, increments per ledger. | | batch_insert_ts | UTC timestamp when batch of records was inserted. | TIMESTAMP | | Yes | For engineering/backfill analysis only. | | airflow_start_ts | Airflow task start timestamp. | STRING | | Yes | | | valid_from | Timestamp when row became effective. | TIMESTAMP | | Yes | Tracks historical changes. | | valid_to | Timestamp when row is no longer valid. | TIMESTAMP | | No | Null = currently active. | ## Example Business Questions 1. What was the balance of each account’s trustlines on a given date? ```sql SELECT account_id, asset_code, asset_issuer, balance FROM `crypto-stellar.snapshots.trustlines_snapshot` WHERE DATE(valid_from) <= DATE('2025-01-15') AND (DATE(valid_to) > DATE('2025-01-15') OR valid_to IS NULL) AND deleted = FALSE AND liquidity_pool_id = '' LIMIT 100; ``` --- ## TTL Current ## Table Metadata | Property | Configuration | | --- | --- | | Natural Key(s) | key_hash, closed_at | | Partition Field(s) | N/A | | Clustered Field(s) | last_modified_ledger, key_hash | | Documentation | [dbt docs](http://www.stellar-dbt-docs.com/#!/model/model.stellar_dbt_public.ttl_current) | ## Column Details Column details are equivalent to the bronze [TTL](../bronze/ttl.mdx) table. Filters down to only latest state information. --- ## Data Lineage --- ## Data Model Diagram :::info For more detailed information see the [Full Hubble Data Model Diagram](https://dbdiagram.io/d/Hubble-Public-Data-Model-66671e056bc9d447b1501801) or the [Compressed Hubble Data Model Diagram](https://dbdiagram.io/d/Hubble-Public-Data-Model-Compressed-666730996bc9d447b151a181). ::: The following diagram shows the relationships between the history and state tables within Hubble. ![Hubble Data Model Diagram](/img/hubble/data_model_diagram.png) --- ## Developer Guide All you need to know about running a Hubble analytics platform. --- ## Backfill using JS UDF This document outlines methods to extract required fields from the XDR of raw data. Use Cases: - Backfix for a bugfix - Backfix for a new column added - Temporary extraction of fields not parsed by stellar-etl We'll take the example of extracting the `fee_account_muxed` field from a transaction envelope (`tx_meta` XDR). However, this method can be adapted to other fields as well. It is worth noting that most users will not need to standup and run their own Hubble. The Stellar Development Foundation provides public access to the data through the public datasets and tables in GCP BigQuery. Instructions on how to access this data can be found in the [Connecting](../../developer-guide/connecting-to-bigquery/README.mdx) section. We will use the [js-stellar-base](https://github.com/stellar/js-stellar-base) library to parse the XDR and employ JavaScript UDFs (User Defined Functions) in BigQuery to apply the transformation to the dataset. For a deeper understanding, check out the [Medium article](https://medium.com/analytics-vidhya/using-npm-library-in-google-bigquery-udf-8aef01b868f4) on using NPM libraries in Google BigQuery UDFs. ## Step 1: Setting Up JS UDF in BigQuery To set up the JS UDF in BigQuery, follow these steps: ### 1. Clone the `js-stellar-base` repository First, clone the repository to your local machine and install the dependencies: ```bash git clone https://github.com/stellar/js-stellar-base.git cd js-stellar-base yarn yarn build:prod ``` This process will generate the following file in the js-stellar-base/dist/ directory: - stellar-base.min.js ### 2. Upload the JS file to Google Cloud Storage (GCS) Once the build process is completed, upload the stellar-base.min.js file to a Google Cloud Storage bucket to be used in the UDF. ## Step 2: Writing the JavaScript Function Here is an example JavaScript function to extract the fee_account_muxed field from the transaction envelope (tx_meta): ```JavaScript let tx_meta = "AAAABQAAAQAAABYMYQ4r9W/uB9X6q6VU6feQhS2kQoRy9CjvwtYXdPRSih2hZeSSAAAAAAAAAZAAAAACAAAAAJwLL0Ul/CyRZdXuenmdXrzVyX9X56m4kYPYmgppVIj8AAAAZAAF9PwAAAABAAAAAAAAAAAAAAABAAAAAQAAAQAAAFj7+8N85JwLL0Ul/CyRZdXuenmdXrzVyX9X56m4kYPYmgppVIj8AAAAAAAAAADN5igtu93OKhkj2NrSHuPEJktU+0gJ0LiNavJirLAmRwAAAAAF9eEAAAAAAAAAAAFpVIj8AAAAQElnt70S4sGicHyhsN1S29DEREZ7i2HU96+8DfyshlFLCoQudDIxThnVEg2KQDrW61R19M7Ms9IAsznURc5y3wIAAAAAAAAAAaFl5JIAAABAIf9/ecA3id1mbHzJ2S9W5bRVqrjQr/c2+jHEuDNZevt3LDVSc+DmRMYie0eQ+vE7B3D+fRPb9yFzpfx4meTfBg=="; let txe = StellarBase.xdr.TransactionEnvelope.fromXDR(tx_meta, "base64"); let tx = txe.feeBump(); let sourceAccount = StellarBase.encodeMuxedAccountToAddress( tx.tx().feeSource(), ); console.log(sourceAccount); ``` This script will output the `fee_account_muxed` value: `MBX64B6V7KV2KVHJ66IIKLNEIKCHF5BI57BNMF3U6RJIUHNBMXSJEAAACYGGCDRL6UFO2` ## Step 3: Wrapping the JavaScript Function as a UDF ```sql CREATE TEMP FUNCTION getFeeBumpAccountIfExists(tx_meta STRING) RETURNS STRING LANGUAGE js OPTIONS ( library=["gs://stellar-test-js-udf/stellar-base.min.js"] -- Path to JS library in GCS ) AS r""" return StellarBase.encodeMuxedAccountToAddress( StellarBase.xdr.TransactionEnvelope.fromXDR(tx_meta, 'base64') .feeBump() .tx() .feeSource() ); """; WITH fee_bump_transactions AS ( SELECT batch_run_date, transaction_hash, tx_envelope AS tx_meta FROM `test_crypto_stellar.history_transactions` WHERE batch_run_date BETWEEN DATETIME("2024-07-01") AND DATETIME_ADD("2024-07-20", INTERVAL 1 MONTH) AND inner_transaction_hash IS NOT NULL -- filter in fee bump transactions ), calculated_fee_account AS ( SELECT batch_run_date, transaction_hash, getFeeBumpAccountIfExists(tx_meta) AS fee_account FROM fee_bump_transactions ), calculated_fee_muxed_account AS ( SELECT batch_run_date, transaction_hash, fee_account FROM calculated_fee_account WHERE fee_account LIKE 'M%' -- muxed accounts ) SELECT batch_run_date, transaction_hash, fee_account AS fee_account_muxed FROM calculated_fee_muxed_account ``` ### Sample Output for the JS UDF After running the above query, you should receive output similar to the following: | Row | transaction_hash | fee_account_muxed | | --- | --- | --- | | 1 | f5f5b0aaf758896ef8c5b4807f41c77d15c11977eecf2b0e4769d777324a2d11 | MCBD54KAHHA4AK4DOZWOSX5O5OZ4OI54N24QITDSFLPD7EG2WY2AMAAACYGGCDRL6UBUA | | 2 | a9e49dff6202663633b83f3645fbf8c2cfeb915db99b2b884a86791b9f8eae2f | MBX64B6V7KV2KVHJ66IIKLNEIKCHF5BI57BNMF3U6RJIUHNBMXSJEAAACYGGCDRL6UFO2 | | 3 | 00dba50c8689477e6990103338a0eb326725e07a7b7ff187359abf11c23c582a | MC5BEU3DCIMHOHRQDVDAPEPZGMBBALPJ3IQY23VTXC3454SQMNWVSAAACYGGCDRL6UX42 | | 4 | 2e1c53a9fe1d48ddc493febe467178994e669e3eebf3a4cca646b3cb666616de | MAMYAUW45TC54C3QORQP7OOFYKOXCJTXOG2WIV5LP2HDMR67MWP6IAAACYGGCDRL6VCZM | ## Step 4: Updating Column Values Using UDF You can also use this UDF to update values in a BigQuery table. Here’s an example of how to do this: ```sql CREATE TEMP FUNCTION getFeeBumpAccountIfExists(tx_meta STRING) RETURNS STRING LANGUAGE js OPTIONS ( library=["gs://stellar-test-js-udf/stellar-base.min.js"] ) AS r""" let txe = StellarBase.xdr.TransactionEnvelope.fromXDR(tx_meta, 'base64'); let tx = txe.feeBump(); let sourceAccount = StellarBase.encodeMuxedAccountToAddress(tx.tx().feeSource()); return sourceAccount """; MERGE `crypto_stellar.history_transactions` AS target USING ( WITH fee_bump_transactions AS ( SELECT batch_run_date, transaction_hash, tx_envelope AS tx_meta FROM `crypto_stellar.history_transactions` WHERE batch_run_date > '2020-08-03' AND batch_run_date < '2020-08-05' AND inner_transaction_hash IS NOT NULL ), calculated_fee_account AS ( SELECT batch_run_date, transaction_hash, getFeeBumpAccountIfExists(tx_meta) AS fee_account FROM fee_bump_transactions ), calculated_fee_muxed_account AS ( SELECT batch_run_date, transaction_hash, fee_account FROM calculated_fee_account WHERE fee_account LIKE 'M%' -- muxed accounts ) SELECT batch_run_date, transaction_hash, fee_account AS fee_account_muxed FROM calculated_fee_muxed_account ) AS source ON target.batch_run_date = source.batch_run_date AND target.transaction_hash = source.transaction_hash WHEN MATCHED THEN UPDATE SET target.fee_account_muxed = source.fee_account_muxed; ``` This query will update the `fee_account_muxed` field in the crypto_stellar.history_transactions table using values calculated from the UDF. ## Conclusion By following these steps, you can effectively extract and manipulate XDR data fields in BigQuery using JavaScript UDFs, making it easier to process and analyze your Stellar network data. You can also apply bugfix for existing dataset. --- ## Backfill Once your scheduling and orchestration are set up, you might encounter the following scenarios: | Use Case | Description | Solution | | --- | --- | --- | | Initial Backfill | You have just setup hubble and would like to ingest historical data | - Option 1. [Data Import](../../developer-guide/backfill/data-import.mdx) **Pros:** Cheap and fast - Option 2: Re-trigger DAGs for past dates **Cons:** Slow and expensive | | Bug Fix | You resolved a bug and need to re-ingest a specific data column/s or back fix a data column | - Option 1. [JS UDF](../../developer-guide/backfill/JS-UDF.mdx) **Pros:** Cheap and fast **Cons:** May need optimized query writing and running in batches - Option 2: Re-trigger DAGs for past dates **Cons:** Slow and expensive | | New data column extraction | You added a new data column/s as part of a feature request and need to backfill data for the newly added column/s | - Option 1. [JS UDF](../../developer-guide/backfill/JS-UDF.mdx) **Pros:** Cheap and fast **Cons:** May need optimized query writing and running in batches - Option 2: Re-trigger DAGs for past dates **Cons:** Slow and expensive | --- ## Data Import This document outlines methods to perform inital backfill when setting up hubble. # Use SDF ETL Data as a Source ## 1. Export Data to Cloud Storage and Load Data into BigQuery - Use the [EXPORT](https://cloud.google.com/bigquery/docs/reference/standard-sql/export-statements) command provided by Google Cloud Platform (GCP) to export your dataset in the required format (e.g., Avro, Parquet). - Use the [LOAD](https://cloud.google.com/bigquery/docs/reference/standard-sql/load-statements) command to load the exported files into your BigQuery dataset. #### Example: ```sql EXPORT DATA OPTIONS( uri='gs://my-bucket/history-transactions/*', format='PARQUET', overwrite=true) AS SELECT * FROM crypto-stellar.crypto_stellar.history_transactions; LOAD DATA INTO mydataset.transactions FROM FILES( format='PARQUET', uris = ['gs://my-bucket/history-transactions/*'] ) WITH PARTITION COLUMNS; ``` ## 2. Use BigQuery API / Console to Mirror SDF's Dataset The Stellar Development Foundation provides public access to fully transformed Stellar network data through the public datasets and tables in GCP BigQuery. Instructions on how to access this data can be found in the [Connecting](../../developer-guide/connecting-to-bigquery/README.mdx) section. Use the [Create Table Copy statement](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_table_copy) to copy data across datasets. ```sql CREATE [ OR REPLACE ] TABLE [ IF NOT EXISTS ] table_name COPY source_table_name ... [OPTIONS(table_option_list)] ``` # Use Galexie as a Source [Galexie](../../../../indexers/build-your-own/galexie/README.mdx) is a tool for extracting, processing, and exporting Stellar ledger metadata to external storage, creating a data lake of pre-processed ledger metadata. This is an upstream data source to Hubble, useful when you have a custom Stellar-ETL pipeline. **Steps** 1. Ensure you provide the correct value for datastore-path in Stellar-ETL [command flags](https://github.com/stellar/stellar-etl/tree/master?tab=readme-ov-file#common-flags). This represents the name of the bucket where your Galexie instance outputs ledger close metadata. 2. Set up an [orchestration system like Airflow](../../developer-guide/scheduling-and-orchestration/getting-started.mdx). 3. In your Airflow instance, trigger the history_table_export DAG for older dates. --- ## Connecting(Connecting-to-bigquery) BigQuery offers multiple connection methods to Hubble. This guide details three common methods: - [BigQuery UI](#bigquery-ui) - analysts that need to perform ad hoc analysis using SQL - [BigQuery SDK](#bigquery-sdk) - developers that need to integrate data into applications - [Looker Studio](#looker-studio) - business people that need to visualize data ## Prerequisites To access Hubble, you will need a Google Cloud Project with billing and the BigQuery API enabled. For more information, please follow the instructions provided by [Google Cloud](https://cloud.google.com/bigquery/docs/quickstarts/query-public-dataset-console). Google does provide a BigQuery Sandbox for free that allows users to explore datasets in a limited capacity. ## BigQuery UI 1. From a browser, open the [crypto-stellar.crypto_stellar](http://console.cloud.google.com/bigquery?ws=!1m4!1m3!3m2!1scrypto-stellar!2scrypto_stellar) dataset. 2. This will open the public dataset `crypto_stellar`, where you can browse its contents in the **Explorer** pane. 3. Click the **star** icon in the Explorer pane. This will favorite the dataset for you. More detailed information about starring resources can be found [here](https://cloud.google.com/bigquery/docs/bigquery-web-ui#star_resources). :::note Hubble cannot be found from the Explorer pane! You cannot search for the dataset. To view the dataset, you **must** use the [dataset link](https://console.cloud.google.com/bigquery?ws=!1m4!1m3!3m2!1scrypto-stellar!2scrypto_stellar). ::: Copy and paste the following example query in the Editor: ```sql select account_id, balance from `crypto-stellar.crypto_stellar.accounts_current` order by balance desc; ``` This query will return the XLM balances for all Stellar wallet addresses, ordered from largest to smallest amounts. ## BigQuery SDK There are multiple [BigQuery API Client Libraries](https://cloud.google.com/bigquery/docs/reference/libraries) available. The following example uses Python to access the Hubble dataset. Use [this guide](https://cloud.google.com/python/docs/setup) for help setting up a python development environment. Install the client library locally, and configure your environment to use your Google Cloud Project: ```bash # verify python version python3 --version # if you do not have pip, install it python -m pip install --upgrade pip # install bigquery client library pip install --upgrade google-cloud-bigquery gcloud config set project PROJECT_ID ``` Use the Python Interpreter to run the example below to list the tables available in Hubble: ```python from google.cloud import bigquery # Construct a BigQuery client object. client = bigquery.Client() dataset_id = 'crypto-stellar.crypto_stellar' # Make an API request tables = client.list_tables(dataset_id) # List the tables found in Hubble print(f'Tables contained in {dataset_id}':) for table in tables: print(f'{table.project}.{table.dataset_id}.{table.table_id}') ``` Run the example below to run a query and print the results: ```python from google.cloud import bigquery # Construct a BigQuery client object. client = bigquery.Client() query = """ SELECT account_id, balance, FROM `crypto-stellar.crypto_stellar.accounts_current` ORDER BY balance DESC LIMIT 10; """ # Make an API request query_job = client.query(query) print("The query data:") for row in query_job: # Row values can be accessed by field name or index. print(f'account_id={row[0]}, balance={row["balance"]}') ``` There are various ways to extract and load data using BigQuery. See the [BigQuery Client Documentation](https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client) for more information. ## Looker Studio [Looker Studio](https://cloud.google.com/looker-studio) is a business intelligence tool that can be used to connect to and visualize data from the Hubble dataset. To connect Hubble as a data source: 1. Open [Looker Studio](https://lookerstudio.google.com) 2. Click on **Create** > **Data Source** 3. Search for the BigQuery connector 4. _(Optional)_ Change the name of the data source at the top of the webpage 5. Click _Shared Projects_ > Select your Google Cloud Project 6. Enter `crypto-stellar` as the Shared Project name 7. Click on the Dataset `crypto_stellar` 8. Select the desired table to connect 9. Click `CONNECT` on the top right of the webpage. And you're connected! General information about Looker Studio can be found [here](https://support.google.com/looker-studio). General information about connecting data sources can be found [here](https://support.google.com/looker-studio/topic/6370331?hl=en&ref_topic=7441382&sjid=14945902445646860578-NA). --- ## Data Curation Running stellar-dbt-public to transform raw Stellar network data into something better. --- ## Architecture ## Architecture Overview In general stellar-dbt-public runs by: - Selecting a dbt model to run - Within the model run: - Sources are referenced and used to create staging tables - Staging tables then undergo various transformations and are stored in intermediate tables - Finishing touches and joins are done on the intermediate tables which produce the final analytics friendly mart tables We try to adhere to the best practices set by the [dbt docs](https://docs.getdbt.com/docs/build/projects) More detailed information about stellar-dbt-public and examples can be found in the [stellar-dbt-public](https://github.com/stellar/stellar-dbt-public/tree/master) repo. --- ## Getting Started [stellar-dbt-public GitHub repository](https://github.com/stellar/stellar-dbt-public/tree/master) ## Recommended Usage ### Import stellar-dbt-public as a dbt Package If you need to build your own models on top of stellar-dbt-public, you can import stellar-dbt-public as a dbt package into a separate dbt project. Example instructions: - Create a new file `packages.yml` in your dbt project (not the stellar-dbt-public project) with the yml below ``` packages: - git: "https://github.com/stellar/stellar-dbt-public.git" revision: v0.0.28 ``` - (Optional) Update your profiles.yml to include profile configurations for stellar-dbt-public ``` new_project: target: test outputs: test: project: dataset: stellar_dbt_public: target: test outputs: test: project: dataset: ``` - (Optional) Update your dbt_project.yml to include project configurations for stellar-dbt-public ``` name: 'stellar_dbt' version: '1.0.0' config-version: 2 profile: 'new_project' model-paths: ["models"] analysis-paths: ["analyses"] test-paths: ["tests"] seed-paths: ["seeds"] macro-paths: ["macros"] snapshot-paths: ["snapshots"] target-path: "target" clean-targets: - "target" - "dbt_packages" models: new_project: staging: +materialized: view intermediate: +materialized: ephemeral marts: +materialized: table stellar_dbt_public: staging: +materialized: ephemeral intermediate: +materialized: ephemeral marts: +materialized: table ``` - Models from the stellar-dbt-public package/repo will now be available in your new dbt project ## Building and Running Locally ### Clone the repo ``` git clone https://github.com/stellar/stellar-dbt-public ``` ### Install required python packages ``` pip install --upgrade pip && pip install -r requirements.txt ``` ### Install required dbt packages ``` dbt deps ``` ### Running dbt - There are many useful commands that come with dbt which can be found in the [dbt documentation](https://docs.getdbt.com/reference/dbt-commands#available-commands) - stellar-dbt-public is designed to use the `dbt build` command which will `run` the model and `test` the model table output - (Optional) run with the `--full-refresh` option ``` dbt build --full-refresh ``` - Subsequent runs can be run with incremental mode (only inserts the newest of data instead of rebuilding all of history every time) ``` dbt build ``` - You can also specify just a single model if you don't want to run all stellar-dbt-public models ``` dbt build --select ``` Please see the [stellar-dbt-public/modles/marts](https://github.com/stellar/stellar-dbt-public/tree/master/models/marts) directory to see a full list of the available models that dbt can run --- ## Overview(Data-curation) Data curation in Hubble is done through [stellar-dbt-public](https://github.com/stellar/stellar-dbt-public). stellar-dbt-public transforms raw Stellar network data from BigQuery datasets and tables into aggregates for more user friendly analytics. It is worth noting that most users will not need to standup and run their own stellar-dbt-public instance. The Stellar Development Foundation provides public access to fully transformed Stellar network data through the public datasets and tables in GCP BigQuery. Instructions on how to access this data can be found in the [Connecting](../../developer-guide/connecting-to-bigquery/README.mdx) section. ## Why Run stellar-dbt-public? Running stellar-dbt-public within your own infrastructure provides a number of benefits. You can: - Have full operational control without dependency on the Stellar Development Foundation for network data - Run modified ETL/ELT pipelines that fit your individual business needs --- ## Scheduling and Orchestration Stitching all the components together. --- ## Architecture(Scheduling-and-orchestration) ## Architecture Overview In general stellar-etl-airflow runs by: - Scheduling DAGs to run `stellar-etl` and upload the data outputted to BigQuery - Scheduling DAGs to run `stellar-dbt-public` using the data in BigQuery - We try to adhere to the best practices set by the [dbt docs](https://docs.getdbt.com/docs/build/projects) More detailed information about stellar-etl-airflow can be found in the [stellar-etl-airflow](https://github.com/stellar/stellar-etl-airflow/tree/master) repo. --- ## Getting Started(Scheduling-and-orchestration) [stellar-etl-airflow GitHub repository](https://github.com/stellar/stellar-etl-airflow/tree/master) ## GCP Account Setup [The SDF](../../../../../learn/glossary.mdx#stellar-development-foundation-sdf) runs Hubble in GCP using Composer and BigQuery. To follow the same deployment you will need to have access to a GCP project. Instructions can be found in the [Get Started](https://cloud.google.com/docs/get-started) documentation from Google. :::note BigQuery and Composer should be available by default. If they are not you can find instructions for enabling them in the [BigQuery](https://cloud.google.com/bigquery?hl=en) or [Composer](https://cloud.google.com/composer?hl=en) Google documentation. ::: ## Create GCP Composer Instance to Run Airflow Instructions on bringing up a GCP Composer instance to run Hubble can be found in the [Installation and Setup](https://github.com/stellar/stellar-etl-airflow?tab=readme-ov-file#installation-and-setup) section in the [stellar-etl-airflow](https://github.com/stellar/stellar-etl-airflow) repository. :::note Hardware requirements can be very different depending on the Stellar network data you require. The default GCP settings may be higher/lower than actually required. ::: ## Configuring GCP Composer Airflow There are two things required for the configuration and setup of GCP Composer Airflow: - Upload DAGs to the Composer Airflow Bucket - Configure the Airflow variables for your GCP setup For more detailed instructions please see the [stellar-etl-airflow Installation and Setup](https://github.com/stellar/stellar-etl-airflow?tab=readme-ov-file#installation-and-setup) documentation. ### Uploading DAGs Within the [stellar-etl-airflow](https://github.com/stellar/stellar-etl-airflow) repo there is an [upload_static_to_gcs.sh](https://github.com/stellar/stellar-etl-airflow/blob/master/upload_static_to_gcs.sh) shell script that will upload all the DAGs and schemas into your Composer Airflow bucket. This can also be done using the [gcloud CLI or console](https://cloud.google.com/storage/docs/uploading-objects) and manually selecting the dags and schemas you wish to upload. ### Configuring Airflow Variables Please see the [Airflow Variables Explanation](https://github.com/stellar/stellar-etl-airflow?tab=readme-ov-file#airflow-variables-explanation) documentation for more information about what should and needs to be configured. ## Running the DAGs To run a DAG all you have to do is toggle the DAG on/off as seen below ![Toggle DAGs](/img/hubble/airflow_dag_toggle.png) More information about each DAG can be found in the [DAG Diagrams](https://github.com/stellar/stellar-etl-airflow?tab=readme-ov-file#dag-diagrams) documentation. ## Available DAGs More information can be found [here](https://github.com/stellar/stellar-etl-airflow/blob/master/README.md#public-dags) ### History Table Export DAG [This DAG](https://github.com/stellar/stellar-etl-airflow/blob/master/dags/history_tables_dag.py): - Exports part of sources: ledgers, operations, transactions, trades, effects and assets from Stellar using the data lake of LedgerCloseMeta files - Optionally this can ingest data using captive-core but that is not ideal nor recommended for usage with Airflow - Inserts into BigQuery ### State Table Export DAG [This DAG](https://github.com/stellar/stellar-etl-airflow/blob/master/dags/state_table_dag.py) - Exports accounts, account_signers, offers, claimable_balances, liquidity pools, trustlines, contract_data, contract_code, config_settings and ttl. - Inserts into BigQuery ### DBT Enriched Base Tables DAG [This DAG](https://github.com/stellar/stellar-etl-airflow/blob/master/dags/dbt_enriched_base_tables_dag.py) - Creates the DBT staging views for models - Updates the enriched_history_operations table - Updates the current state tables - (Optional) warnings and errors are sent to slack. --- ## Overview(Scheduling-and-orchestration) Hubble uses [stellar-etl-airflow](https://github.com/stellar/stellar-etl-airflow) to schedule and orchestrate all its workflows. This includes the scheduling and running of stellar-etl and stellar-dbt. It is worth noting that most users will not need to standup and run their own Hubble. The Stellar Development Foundation provides public access to the data through the public datasets and tables in GCP BigQuery. Instructions on how to access this data can be found in the [Connecting](../../developer-guide/connecting-to-bigquery/README.mdx) section. ## Why Run stellar-etl-ariflow? Running stellar-etl-airflow within your own infrastructure provides a number of benefits. You can: - Have full operational control without dependency on the Stellar Development Foundation for network data - Run modified ETL/ELT pipelines that fit your individual business needs --- ## Source System Ingestion Running stellar-etl for Stellar network data ingestion. --- ## Architecture(Source-system-ingestion) ## Architecture Overview In general stellar-etl runs by: - Read raw data from the Stellar network - This can be done by running a stellar-etl export command to export data between a start and end ledger - stellar-etl has the ability to read from two different sources: - Captive-core directly to get LedgerCloseMeta - A data lake of compressed LedgerCloseMeta files from Ledger Exporter - Tranforms the LedgerCloseMeta XDR into an easy to parse JSON format - Optionally uploads the JSON files to GCS or any other cloud storage service More detailed information about stellar-etl and examples can be found in the [stellar-etl](https://github.com/stellar/stellar-etl/tree/master) repo. --- ## Getting Started(Source-system-ingestion) [stellar-etl GitHub repository](https://github.com/stellar/stellar-etl/tree/master) [stellar/stellar-etl docker images](https://hub.docker.com/r/stellar/stellar-etl) ## Recommended Usage Generally if you do not need to modify any of the stellar-etl code, it is recommended that you use the [stellar/stellar-etl docker images](https://hub.docker.com/r/stellar/stellar-etl). Example to run locally with docker: ``` docker run --platform linux/amd64 -ti stellar/stellar-etl:latest ``` ## Building and Running Locally ### Install Golang - Make sure your golang version >= `1.22.1` - Instructions to install golang can be found at [go.dev/doc/install](https://go.dev/doc/install) ### Clone the repo ``` git clone https://github.com/stellar/stellar-etl ``` ### Build stellar-etl - Run `go build` in the cloned stellar-etl repo ``` go build ``` ### Run stellar-etl - A `stellar-etl` executable should have been created in your stellar-etl repo - Example stellar-etl command: ``` ./stellar-etl export_ledgers -s 10 -e 11 ``` This should create a `exported_ledgers.txt` file with the output of ledgers from ledgers 10 to 11: ``` {"base_fee":100,"base_reserve":100000000,"closed_at":"2024-02-06T17:34:12Z","failed_transaction_count":0,"fee_pool":0,"id":42949672960,"ledger_hash":"f7c89b35c50f74dc69eacd9dda8e9ec9f1af36b6a2928b77619c1beb5f5ca8d4","ledger_header":"AAAAAIGFrRh+oCo2QcAjG6IzWTlil89DNwYIwx6PrrmehujNf44MwMJZxPz3DJYHciV9ligoKwbmeiue4eM29CRWBJgAAAAAZcJtlAAAAAAAAAABAAAAANVyadliUPdJbQeb4ug1Ejbv/+jTnC4Gv6uxQh8X/GccAAAAQBW0ICM/1C7CML6ngZijKycAOIhzwGN6yUsznHznfJunIDyLLVF9/oqvLzP1vaGOhBf3Rmtm5WgGVgeLjlyJSAHfP2GYBKkv20BXGS3EPddI6neK3FK8SYzoBSTAFLgRGfBr+YHFQTIEJ0Y81WEOYClgyjOER8vd4qMQb3gM9nRvAAAACg3gtrOnZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkBfXhAAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","max_tx_set_size":100,"operation_count":0,"previous_ledger_hash":"8185ad187ea02a3641c0231ba23359396297cf43370608c31e8faeb99e86e8cd","protocol_version":0,"sequence":10,"soroban_fee_write_1kb":0,"successful_transaction_count":0,"total_coins":1000000000000000000,"transaction_count":0,"tx_set_operation_count":"0"} {"base_fee":100,"base_reserve":100000000,"closed_at":"2024-02-06T17:34:17Z","failed_transaction_count":0,"fee_pool":0,"id":47244640256,"ledger_hash":"5b9ac11c6040f4e2fa6a120b3dee9a4b338b7a25bcb8437dab0c0a5c557a41f5","ledger_header":"AAAAAPfImzXFD3TcaerNndqOnsnxrza2opKLd2GcG+tfXKjUK858NP5gM0pneHF0nRowsJBAzMwWDx0+tmbYIZkIT+8AAAAAZcJtmQAAAAAAAAABAAAAANVyadliUPdJbQeb4ug1Ejbv/+jTnC4Gv6uxQh8X/GccAAAAQDhZKPKBdeD4Sthcu+EsuzEtSyiXzXkHboOsgYT1tuV/juZyKqgrsVmg+RmMoRun+NKCdcB8LV9gaehiFm+XDgnfP2GYBKkv20BXGS3EPddI6neK3FK8SYzoBSTAFLgRGfBr+YHFQTIEJ0Y81WEOYClgyjOER8vd4qMQb3gM9nRvAAAACw3gtrOnZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkBfXhAAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","max_tx_set_size":100,"operation_count":0,"previous_ledger_hash":"f7c89b35c50f74dc69eacd9dda8e9ec9f1af36b6a2928b77619c1beb5f5ca8d4","protocol_version":0,"sequence":11,"soroban_fee_write_1kb":0,"successful_transaction_count":0,"total_coins":1000000000000000000,"transaction_count":0,"tx_set_operation_count":"0"} ``` ## stellar-etl Commands ### export_ledgers ``` stellar-etl export_ledgers --start-ledger 1000 --end-ledger 500000 --output exported_ledgers.txt ``` This command exports ledgers within the provided range. ### export_transactions ``` stellar-etl export_transactions --start-ledger 1000 --end-ledger 500000 --output exported_transactions.txt ``` This command exports transactions within the provided range. ### export_operations ``` stellar-etl export_operations --start-ledger 1000 --end-ledger 500000 --output exported_operations.txt ``` This command exports operations within the provided range. ### export_effects ``` stellar-etl export_effects --start-ledger 1000 --end-ledger 500000 --output exported_effects.txt ``` This command exports effects within the provided range. ### export_assets ``` stellar-etl export_assets --start-ledger 1000 --end-ledger 500000 --output exported_assets.txt ``` Exports the assets that are created from payment operations over a specified ledger range. ### export_trades ``` stellar-etl export_trades --start-ledger 1000 --end-ledger 500000 --output exported_trades.txt ``` Exports trade data within the specified range to an output file ### export_diagnostic_events ``` stellar-etl export_diagnostic_events --start-ledger 1000 --end-ledger 500000 --output export_diagnostic_events.txt ``` Exports diagnostic events data within the specified range to an output file ### export_ledger_entry_changes ``` stellar-etl export_ledger_entry_changes --start-ledger 1000 --end-ledger 500000 --output exported_changes_folder/ ``` This command exports ledger changes within the provided ledger range. Note that this command will also exports every state change for each ledger entry type. [Information](https://github.com/stellar/stellar-etl?tab=readme-ov-file#export_ledger_entry_changes) on options to only output specifc ledger entry types. --- ## Overview(Source-system-ingestion) Stellar network data ingestion in Hubble is done through [stellar-etl](https://github.com/stellar/stellar-etl/tree/master). stellar-etl reads and transforms Stellar network data into OLAP friendly JSON files. | Use Case | Recommended Solution | Reason | | --- | --- | --- | | Need commonly required data fields and do not want to take burden of setting up entire pipeline | Use output from SDF's stellar-etl instance i.e. [Connect to Bigquery and access hubble](../../developer-guide/connecting-to-bigquery/README.mdx) | It is worth noting that most users will not need to standup and run their own stellar-etl instance. The Stellar Development Foundation provides public access to fully transformed Stellar network data through the public datasets and tables in GCP BigQuery. | | Need custom data but do not want to take burden of setting up entire pipeline | Use [JS-UDF](../../developer-guide/backfill/JS-UDF.mdx) | Stellar-ETL does not parse every single field available in raw XDR, but it does save the raw transaction meta. JS-UDF helps to extract a field directly from XDR | | Need custom and operationally independent data | Run [stellar-etl](https://github.com/stellar/stellar-etl/tree/master) | Running stellar-etl within your own infrastructure provides a number of benefits. You can: - Have full operational control without dependency on the Stellar Development Foundation for network data - Run modified ETL/ELT pipelines that fit your individual business needs | --- ## Visualization Visualizing Stellar network data. --- ## Getting Started(Visualization) This sections goes through using [Google's Looker Studio](https://lookerstudio.google.com/u/0/navigation/reporting) as a free and easy to use visualization tool that you can hook up your BigQuery Stellar network data to. There are many other free/paid visualization tools available. Hubble is compatible with any visualization tool with a BigQuery connector. ## Creating your first visualization - Follow [Google's Quick Start Guide](https://support.google.com/looker-studio/answer/9171315?hl=en) ## Hooking Up Data Sources The following will use the Stellar Development Foundations public datasets and tables as an example to hook up data sources to Looker Studio - Click `Create` in [Google's Looker Studio](https://lookerstudio.google.com/u/0/navigation/reporting) - Click `Data Source` - Find the `BigQuery` connector - Use the project `crypto-stellar` - Use the dataset `crypto_stellar` - Select the table of interest - Click `CONNECT` When you create a new report you should be able to now access data from `crypto-stellar.crypto_stellar.` ## Making your first pie chart - Click `Create` in [Google's Looker Studio](https://lookerstudio.google.com/u/0/navigation/reporting) - Click `Report` - Click `My data sources` - Click the data source you added above - A table of the data should appear in a new report - Click on the table - Click on `Chart` on the right sidebar - Click on the `Pie chart` image You have now created a new report with a pie chart. Looker Studio has many resources to help visualize and explore data. Learn more [here](https://support.google.com/looker-studio?sjid=9035399711189270749-NA#topic=6267740) --- ## Overview(Visualization) There are various ways to visulize data from Hubble. The following section will go through the steps to use [Google's Looker Studio](https://cloud.google.com/looker-studio?hl=en) to help visualize Stellar network data. --- ## APIs Overview Learn about the services that provide access to real-time Stellar network data | Features | RPC | Horizon | | ----------------------- | --- | ------- | | Real-time Data | ✅ | ✅ | | Historical Data | ❌ | ❌\* | | Smart Contracts | ✅ | ❌ | | Transaction Simulation | ✅ | ❌ | | Curated and Parsed Data | ❌ | ✅ | \*_Please note that Horizon can provide full historical data but is not the recommended tool for full historical data access. Please use [Hubble](../analytics/hubble/README.mdx) or [Galexie](../indexers/build-your-own/galexie/README.mdx) instead._ ## [RPC](./rpc/README.mdx) The RPC provides real-time access to the current state of the Stellar network, including account balances, smart contract states, and recent transaction queries (within a seven-day retention window), while also allowing transaction submission. It is designed to be simple, minimal, and scalable, making it ideal for applications and wallets that require live data availability. :::note RPC is the recommended API for accessing and interacting with Stellar network data in real-time. ::: ## [Horizon](./horizon/README.mdx) :::warning Horizon is nearing end-of-life and will eventually be deprecated in favor of Stellar RPC and [Portfolio APIs](../indexers/README.mdx#portfolio-apis). While it will continue to receive updates to maintain compatibility with upcoming protocol releases, it won't receive new feature development. ::: Horizon is an API for accessing and interacting with Stellar network data. --- ## Access Blockchain Data with Horizon API: Query Transactions, Accounts & More # Horizon Introduction :::info On August 1, 2024, the publicly accessible SDF-hosted Horizon had its historical data truncated to one year. That update optimized the performance of the publicly accessible Horizon and ensured a streamlined experience for all users. Consider third-party ecosystem providers of Horizon, which may provide a longer history retention window as well as other features. ::: Horizon provides an HTTP API to data in the Stellar network. It ingests and re-serves the data produced by the Stellar network in a form that is easier to consume by the average application relative to the performance-oriented data representations used by Stellar Core. This API serves the bridge between apps and [Stellar Core](../../../validators/README.mdx). Projects like wallets, decentralized exchanges, and asset issuers use Horizon to submit transactions, query an account balance, or stream events like transactions to an account. Horizon can be accessed via cURL, a browser, or one of the [Stellar SDKs](../../../tools/sdks/README.mdx). To reduce the complexity of your project, we recommend you use an SDK instead of making direct API calls. This guide describes how to administer a production Horizon instance (refer to the [Developers' Blog](https://stellar.org/blog/developers/a-new-sun-on-the-horizon) for some background on the performance and architectural improvements of this major version bump). For information about developing on the Horizon codebase, check out the [Development Guide](https://github.com/stellar/stellar-horizon/blob/main/DEVELOPING.md). Before we begin, it's worth reiterating the sentiment echoed in the [Core Node](../../../validators/README.mdx) documentation: **we do not endorse running Horizon backed by a standalone Stellar Core instance**, and especially not by a _validating_ Stellar Core. These are two separate concerns, and decoupling them is important for both reliability and performance. Horizon instead manages its own, pared-down version of Stellar Core optimized for its own subset of needs (we'll refer to this as a "Captive Core" instance). ## Why Run Horizon? Running Horizon within your own infrastructure provides a number of benefits. You can: - Have full operational control without dependency on the Stellar Development Foundation for network data and transaction submission to networks; - Run multiple instances for redundancy and scalability. The Stellar Development Foundation (SDF) runs two instances of Horizon: - [horizon-testnet.stellar.org](https://horizon-testnet.stellar.org) for interacting with the [testnet](../../../networks/README.mdx) - [horizon-futurenet.stellar.org](https://horizon-futurenet.stellar.org) for interacting with the [futurenet](../../..//README.mdx) ## In These Docs - [Admin Guide](./admin-guide/README.mdx): how to set up your own Horizon instance. - [Structure](./api-reference/structure/README.mdx): how Horizon is structured. - [Resources](./api-reference/resources/README.mdx): descriptions of resources and their endpoints. - [Aggregations](./api-reference/aggregations/README.mdx): descriptions of specialized endpoints. - [Errors](./api-reference/errors/README.mdx): potential errors and what they mean. --- ## Admin Guide All you need to know about setting up, running, and using Horizon. --- ## Configuring ## Prerequisites - You have identified the [installation](./installing.mdx) method for the host system: - For bare-metal, you have two executables installed on the host operation system path: `stellar-horizon` and `stellar-core`. - For running Horizon image with Docker daemon, you will use the [stellar/stellar-horizon](https://hub.docker.com/r/stellar/stellar-horizon) hosted on Docker Hub. You have already pulled the stellar/stellar-horizon image via `docker pull stellar/stellar-horizon:` onto host. This image contains the `stellar-horizon` and `stellar-core` within. - For Kubernetes with [Horizon Helm Chart](https://github.com/stellar/helm-charts/tree/main/charts/horizon), you have followed the [Install Horizon with Helm Chart](./installing.mdx#helm-chart-installation). - [Initialize database](#initialize-horizon-database) You are now ready to identify the configuration parameters needed to perform three important roles: - **Serving read-only API requests** via a regular web-based HTTP API; - **Ingesting ledgers** from Core nodes of the Stellar network to keep its world-view up to date; - **Submitting transactions** via a regular web-based HTTP API, forwarding the transaction submission request to the Stellar network. To perform these roles, you can choose from one of two deployment modes below (single instance deployment or multiple instance deployment). Each has its own configuration parameters. ## Single Instance Deployment Run `stellar-horizon` in a single o/s process and it will perform all three roles simultaneously. | environment variable | example | | ------------------------- | ----------------------------------- | | `DATABASE_URL` | postgres://localhost/horizon_pubnet | | `NETWORK` | pubnet | | `HISTORY_RETENTION_COUNT` | 518400 | ## Multiple Instance Deployment In this scalable deployment variant, you run multiple instances of `stellar-horizon`, each performing a subset of the roles. This allows you to horizontally scale each of the role functions independently. ### Ingestion Role Instance You must allocate **at least** one instance to perform ongoing ingestion to capture network activity. Set `HISTORY_RETENTION_COUNT` explicitly to limit storage of ingested network activity in the database — we recommend a sliding window of the last 30 days (518400 ledgers). By default (`HISTORY_RETENTION_COUNT=0`), Horizon retains all ingested history and never purges it. | environment variable | example | | ------------------------- | ----------------------------------- | | `DATABASE_URL` | postgres://localhost/horizon_pubnet | | `NETWORK` | pubnet | | `HISTORY_RETENTION_COUNT` | 518400 | | `DISABLE_TX_SUB` | true | ### API Role Instance You can run none or multiple instances to serve read-only API requests. Notice there is no need to define network settings here, as Horizon only reads from database. | environment variable | example | | -------------------- | ----------------------------------- | | `DATABASE_URL` | postgres://localhost/horizon_pubnet | | `INGEST` | false | | `DISABLE_TX_SUB` | true | ### Transaction Submission Role Instance You can run none or multiple instances to serve transaction submission requests. If you run an instance with transaction submission enabled, the Horizon deployment is required to have at least one instance perform the ingestion role on the same database. Horizon transaction submission depends on this **live** ingestion taking place against the database in order to confirm tx submission status. If ingestion is planned to be done on a separate instance, add `INGEST=false` on this instance, otherwise don't include the parameter, Horizon will default to `INGEST=true`. When a transaction submission enabled instance has `INGEST=true` effective, it will configure the related `STELLAR_CORE_URL` parameter automatically to use the internally launched captive core instance and the deployment does not need to set the configuration value explicitly. If setting `INGEST=false`, then **must** define the `STELLAR_CORE_URL` variable on this transaction submission enabled instance, since there will be no internally hosted captive core instance as part of ingestion available to reference, instead the `STELLAR_CORE_URL` provides the ability to define the URL of a core instance HTTP port which Horizon will send transaction submissions towards. | environment variable | example | | -------------------- | ----------------------------------- | | `DATABASE_URL` | postgres://localhost/horizon_pubnet | | `STELLAR_CORE_URL` | http://example.watcher.core:11626 | | `INGEST` | false | ## Notes ### Ingestion If you have configured your deployment to perform the ingestion role, then it is **strongly** recommended to review [Ingestion](./ingestion.mdx) first and [Filtering](./ingestion-filtering.mdx) second and factor that into configuration parameters to achieve best performance related to your application requirements before proceeding further. - Horizon will create a sub-directory under the current working directory of the o/s process to store captive core runtime data files. Refer to [Prerequisites](./prerequisites.mdx) for the type and amount of storage recommended. You can override this location with the optional `CAPTIVE_CORE_STORAGE_PATH` environment variable, set to a directory on the file system where captive core will store the runtime files. ### `DISABLE_TX_SUB` This config parameter is optional, set as FALSE by default. Controls whether Horizon will accept HTTP requests to the `/tx` API endpoint and forward to the network. Refer to [Channel Accounts](../../../../build/guides/transactions/channel-accounts.mdx) for some recommendations on optional client transaction submission optimizations. - When set to FALSE, it requires **live** ingestion process to be running on the same database because Horizon depends on new ledgers from the network to confirm a transaction submission status, Horizon will report a startup error if it detects no **live** ingestion. Requires `INGEST=true` or `STELLAR_CORE_URL` to be defined for access to a Core instance. - When transaction submission is disabled by setting it to TRUE, Horizon will return 405 on POSTs to /tx. ### `NETWORK` This config parameter is optional, can be one of Stellar's public networks, 'pubnet', or 'testnet'. Triggers Horizon to automatically set configurations for remaining Horizon settings and generate the correct core toml/cfg settings. If you only need Horizon to connect to one of those public Stellar networks, this will take care of all related configurations. - If you want to connect Horizon to a different Stellar network other than pubnet or testnet or override any of the defaults that `NETWORK` usage will initiate, the key environment variables that can be set are: `HISTORY_ARCHIVE_URLS`, `CAPTIVE_CORE_CONFIG_PATH`, `NETWORK_PASSPHRASE`, `CAPTIVE_CORE_STORAGE_PATH`, `STELLAR_CORE_URL`. ### `DB_URL` This config parameter is required, specifies the Horizon database. It's value follows this format: `dbname= user= password= host=` ### `LOG_LEVEL` This config parameter is optional, can be one of 'info', 'error', 'debug'. ### `HISTORY_RETENTION_COUNT` This config parameter is optional, it determines the maximum sliding window of historical network data to retain on the database from ingestion. The value is expressed as absolute ledger count, which is an indirect way to define a duration of time, each ledger being approximately 5 seconds. It is defaulted to 0, which means it will not purge any history from the database. To enact the recommended sliding window of one month, set this to 518400, which is the approximate number of ledgers in 30 days. Refer to [Compute Resources](./prerequisites.mdx) for how database storage space is closely related to this setting. ## Passing Configurations to Horizon The `stellar-horizon` binary searches process environment variables for configuration. Depending on how Horizon was installed, the method you perform to configure the process environment will differ: ### Bare-metal - Non-package manager: use O/S environment variables to pass configurations. There are many tools you can use to manage them, such as [direnv](http://direnv.net) or [dotenv](https://github.com/bkeepers/dotenv). - [Package manager](./installing.mdx#package-manager): the provided `stellar-horizon-cmd` wrapper will start a new process and create environment variables in the process from `/etc/default/stellar-horizon` and then launch the 'stellar-horizon'. To set configurations, edit the file at `/etc/default/stellar-horizon`. :::info This script invokes Horizon with the `stellar` user, so make sure that permissions for the user are set up accordingly. The current working directory should be writable for this user and the user should be able to execute the `stellar-horizon` and `stellar-core` binaries; etc. ::: ### Containerized - Non-Helm: pass all configuration parameters to the horizon docker image as [docker environment variables](https://docs.docker.com/engine/reference/commandline/run/#env). - Helm: pass all configuration parameters in the [Helm install command](https://helm.sh/docs/helm/helm_install) as a values file. ## Initialize Horizon Database Before running the Horizon server for the first time, you must initialize the Horizon database. This database will be used for all of the information produced by Horizon, most notably historical information about transactions that have occurred on the Stellar network. To prepare a database for Horizon's use, first ensure it is blank. It's easiest to create a new database on your PostgreSQL server specifically for Horizon's use. We recommend creating a new user(role) in postgres dedicated to Horizon's database and assigning that user(role) as the owner of this database. To illustrate an example using `psql`, first login to the database server using the `psql` command-line tool as a superuser, and then create the new user(role) and database for Horizon: ``` postgres=# postgres=# CREATE ROLE horizon WITH LOGIN; CREATE ROLE postgres=# postgres=# CREATE DATABASE horizon OWNER horizon; CREATE DATABASE postgres=# ``` Additionally, you can set a password on your new `horizon` postgres user with `ALTER USER`. Once completed, you can compose the full value of the configuration parameter for db access `DATABASE_URL="dbname=horizon user=horizon password= host="`. Next, execute the Horizon binary to install the schema onto the empty db from the command line. In this example, assume the current shell doesn't have `DATABASE_URL` in environment yet, so export it first into shell: ``` $ export DATABASE_URL="dbname=horizon user=horizon password= host=" $ stellar-horizon db init ``` ### Optional Postgres Configurations Based on performance observations over time, we recommend additional Postgres configuration settings(postgresql.conf), but these are not required: - Set `random_page_cost=1` if you are using SSD storage. With this setting, Query Planner will make a better use of indices, especially for `JOIN` queries. We've noticed a huge speed improvement for some queries with this setting. \_ To improve availability of ingestion, api, transaction submission servers it's recommended to set the following values: - `tcp_keepalives_idle`: 10 seconds - `tcp_keepalives_interval`: 1 second - `tcp_keepalives_count`: 5 With the config above, if there are no queries from a given client for 10 seconds, Postgres should start sending TCP keepalive packets. It will retry 5 times every second. If there is no response from the client after that time it will drop the connection. ## Next Step After configuration is complete, you are now ready to proceed to [Running Horizon](./running.mdx)! --- ## Ingestion Filtering ## Overview Ingestion Filtering enables Horizon operators to drastically reduce the storage footprint of the historical data in the Horizon database by white-listing Assets and/or Accounts that are relevant to their operations. ### Why is it useful: Previously, the only way to limit data storage was by limiting the temporal range of history via rolling retention (e.g. the last 30 days). The filtering feature allows users to store a longer historical timeframe in the Horizon database for only whitelisted assets, accounts, and their related historical entities (transactions, operations, trades, etc.). For further context, running an unfiltered `full` history Horizon instance currently requires over 30TB of disk space (as of June 2023) with storage growing at a rate of about 1TB/month. As a benchmark, filtering by even 100 of the most active accounts and assets reduces storage by over 90%. For the majority of applications which are interested in an even more limited set of assets and accounts, storage savings should be well over 99%. Other benefits include reducing operating costs for maintaining storage, improved DB health metrics and query performance. ### How does it work: Filtering feature operates during ingestion in **live** and **historical range** processes. It tells ingestion process to only accept incoming ledger transactions which match on a filter rule, any transactions which don't match on filter rules are skipped by ingestion and therefore not stored on database. Some key aspects to note about filtering behavior: - If both asset and account filters are enabled and each filter is provisioned with at least one rule, then transactions are stored in the database when any rule from either filter matches for the given transaction. - Filtering applies only to ingestion of historical data in the database, it does not affect how ingestion process maintains current state data stored in database, which is the last known ledger entry for each unique entity within accounts, trustlines, liquidity pools, offers. However, current state data consumes a relatively small amount of the overall storage capacity. - When filter rules are changed, they only apply to existing, running ingestion processes(**live** and **historical range**). They don't trigger any retro-active filtering or back-filling of existing historical data on the database. - When the filter rules are updated to include additional accounts or assets in the white-list, the related transactions from **live** ingestion will only appear in the historical database data once the filter rules have been updated using the Admin API. The same applies to **historical range** ingestion, where the new filter rules will only affect the data from the current ledger within its configured range at the time of the update. - Updating the filter rules to include additional accounts or assets does not trigger automatic back-filling related to new entites in the historical database. To include prior history of newly white-listed entites in the database you can manually run a new [Historical Ingestion Range](./ingestion.mdx#ingesting-historical-data) after updating the filter rules. - When the filter rules are updated to remove accounts or assets previously defined on white-list, the historical data in the database will not be retroactively purged or filtered based on the updated rules. The data is stored in the history tables for the lifetime of the database or until the `HISTORY_RETENTION_COUNT` is exceeded. Once the retention limit is reached, Horizon will purge all historical data related to older ledgers, regardless of any filtering rules. - Filtering will not affect the performance or throughput rate of an ingestion process, it will remain consistent whether filter rules are present or not. Filter rules define white-lists of the following supported entities: - Account id - Asset id (canonical) Given that all transactions related to the white listed entities are included, all historical time series data related to those transactions are saved in horizon's history db, including transaction itself, all operations in the transaction, and references to any ancillary entities from operations. ## Configuration: Filtering is enabled by default with no filter rules defined. When no filter rules are defined, it effectively means no filtering of ingested data occurs. To start filtering ingestion, need to define at least one filter rule: - enable Horizon admin port with environmental configuration parameter `ADMIN_PORT=XXXXX`, this will allow you to access the port. - define filter whitelists. submit Admin HTTP API requests to view and update the filter rules: Refer to the [Horizon Admin API Docs](https://github.com/stellar/stellar-horizon/blob/main/internal/httpx/static/admin_oapi.yml) which are also published on Horizon running instances as Open API 3.0 doc on the Admin Port when enabled at `http://localhost:/`. You can paste the contents from that url into any OAPI tool such as [Swagger](https://editor.swagger.io) which will render a visual explorer of the API endpoints. On the swagger editor you can also load the published Horizon admin.oapi.yml directly as a url, choose `File->Import URL`: ``` https://raw.githubusercontent.com/stellar/stellar-horizon/main/internal/httpx/static/admin_oapi.yml ``` Follow details and examples of request/response payloads to read and update the filter rules for these endpoints: ``` /ingestion/filters/account /ingestion/filters/asset ``` Choosing `Try it out` button from either endpoint will display `curl` examples of entire HTTP request. ## Sample Use Case: As an Asset Issuer, I have issued 4 assets and am interested in all transaction data related to those assets including customer Accounts that interact with those assets through the following operations: - Operations - Effects - Payments - Claimable balances - Trades I would like to store the full history of all transactions related from the genesis of those assets. ### Pre-requisites: You have installed Horizon with empty database and it has **live** ingestion enabled. ### Steps: 1. Configure a filter rule with 4 white-listed Assets by POST'ing the request to Horizon ADMIN API `:/ingestion/filters/asset`. ``` curl -X 'PUT' \ 'http://localhost:4200/ingestion/filters/asset' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "whitelist": [ "USDC:GAFRNZHK4DGH6CSF4HB5EBKK6KARUOVWEI2Y2OIC5NSQ4UBSN4DR456U", "DOTT:GAFRNZHK4DGH6CSF4HB5EBKK6KARUOVWEI2Y2OIC5NSQ4UBSN4DR456U", "ABCD:GAFRNZHK4DGH6CSF4HB5EBKK6KARUOVWEI2Y2OIC5NSQ4UBSN4DR456U", "EFGH:GAFRNZHK4DGH6CSF4HB5EBKK6KARUOVWEI2Y2OIC5NSQ4UBSN4DR456U" ], "enabled": true }' ``` 2. Since this is new horizon database, and first filter rules, there is nothing more to do, and effectively stop here. 3. However, for sake of exercise, suppose you already had Horizon running for a while and the database populated based on some filter rules, and these new rules were additional white-listings you just added. In this case, you choose whether you want to retro-actively back fill historical data on horizon database for these new white-listed entites from a prior time up to the present time, because they were originally dropped at prior ingestion time and not included on the database. If you decide you want to back fill, then you run a separate Horizon **historical range** ingestion process, refer to [Historical Ingestion Range](./ingestion.mdx#ingesting-historical-data) for steps: --- ## Ingestion Horizon API provides most of its utility through ingested data, and your Horizon server can be configured to listen for and ingest transaction results from the Stellar network. Ingestion enables API access to both current state (e.g. someone's balance) and historical state (e.g. someone's transaction history). ## Ingestion Types There are two primary ingestion use cases for Horizon operations: - Ingesting **live** data to stay up to date with the latest ledgers from the network, accumulating a sliding window of aged ledgers; - Ingesting **historical** data to retroactively add network data from a time range in the past to the database. ## Determine Storage Space You should think carefully about the historical timeframe of ingested data you'd like to retain in Horizon's database. The storage requirements for transactions on the Stellar network are substantial and are growing unbounded over time. This is something that you may need to continually monitor and reevaluate as the network continues to grow. We have found that most organizations need only a small fraction of recent historical data to satisfy their use cases. Through analyzing traffic patterns on SDF's Horizon instance, we see that most requests are for very recent data. To keep your storage footprint small, we recommend the following: - Use **live** ingestion, use **historical** ingestion only in limited exceptional cases. - If your application requires access to all network data, no filtering can be done, we recommend limiting historical retention of ingested data to a sliding window of 1 month (`HISTORY_RETENTION_COUNT=518400`). Note that you must set this explicitly: Horizon's default is `HISTORY_RETENTION_COUNT=0`, which retains all history and never purges ingested data. - If your application can work on a [filtered network dataset](./ingestion-filtering.mdx) based on specific accounts and assets, then we recommend applying ingestion filter rules. When using filter rules, it provides benefit of choice in longer historical retention timeframe since the filtering is reducing the overall database size to such a degree, historical retention(`HISTORY_RETENTION_COUNT`) can be set in terms of years rather than months or even disabled(`HISTORY_RETENTION_COUNT=0`). - If you cannot limit your history retention window to 30 days and cannot use filter rules, we recommend considering [Stellar Hubble Data Warehouse](../../../analytics/hubble/README.mdx) for any historical data. ### Ingesting Live Data This option is enabled by default and is the recommended mode of ingestion to run. It is controlled with environment configuration flag `INGEST`. Refer to [Configuration](./configuring.mdx) for how an instance of Horizon performs the ingestion role. For high availability requirements, **we recommend deploying more than one live ingesting instance**, as this makes it easier to avoid downtime during upgrades and adds resilience, ensuring you always have the latest network data (refer to [Ingestion Role Instance](./configuring.mdx#multiple-instance-deployment)). ### Ingesting Historical Data Import network data from a past date range into the database: ``` stellar-horizon db reingest range ``` Running any historical range of ingestion requires coordination with the data retention configuration chosen. When setting a temporal limit on history with `HISTORY_RETENTION_COUNT=`, the temporal limit takes precedence, and any data ingested beyond that limit will be automatically purged. Typically the only time you need to run historical ingestion is once when boot-strapping a system after first deployment, from that point forward **live** ingestion will keep the database populated with the expected sliding window of trailing historical data. Maybe one exception is if you think you have a gap in the database caused by the **live** ingestion being down, in which case you can run historical ingestion range to essentially gap fill. You can run historical ingestion in parallel in background while your main Horizon server separately performs **live** ingestion. If the range specified overlaps with data already in the database, it is ok and will simply be overwritten, effectively idempotent. #### Parallel Ingestion Workers You can parallelize the ingestion of target historical ledger range by dividing it into sequential slices of smaller ranges and run the db reingest range command for each sub-range in parallel as a separate process on the same or a different machine. The shorthand rule for best performance is to identify the number of CPU cores available per target machine, if multi-core, then add `--parallel-workers ` to the command, this will enable the command to further parallelize internally within a single process using multiple threads and sub-divided smaller ranges. ``` # target range 1 30000, on single machine with 1 CPU core horizon1> stellar-horizon db reingest range 1 30000 # target range 1 30000, on single machine with 4 CPU cores horizon1> stellar-horizon db reingest range 1 30000 --parallel-workers 4 # target range 1 30000, on two machines, each has 2 CPU cores horizon1> stellar-horizon db reingest range 1 15000 --parallel-workers 2 horizon2> stellar-horizon db reingest range 15001 30000 --parallel-workers 2 ``` ### Notes #### Some endpoints may report not available during **live** ingestion - Endpoints that display current state information from **live** ingestion may return `503 Service Unavailable`/`Still Ingesting` error. An example is the `/paths` endpoint (built using offers). Such endpoints will become available after **live** ingestion has finished network synchronization and catch up (usually within a couple of minutes). #### If more than five minutes has elapsed with no new ingested data: - Verify the host machine meets recommended [Prerequisites](./prerequisites.mdx). - Check Horizon log output. - If there are many `level=error` messages, it may point to an environmental issue, inability to access the database. - **Live** ingestion will emit two key log lines about once every 5 seconds based on latest ledger emitted from network. Tail the Horizon log output and grep for presence of these lines with a filter: ``` tail -f horizon.log | | grep -E 'Processed ledger|Closed ledger' ``` If you don't see output from this pipeline every couple of seconds for a new ledger then ingestion is not proceeding, look at full logs and see if any alternative messages are printing reasons to the contrary. May see lines mentioning 'catching up' When connecting to pubnet, as it can take up to 5 minutes for the captive core process started by Horizon to catch up to pubnet network. - Check RAM usage on the machine, it's possible that system ran low on RAM and is using swap memory which will result in slow performance. Verify host machine meets minimum RAM [prerequisites](./prerequisites.mdx). - Verify the read/write throughput speeds on the volume that current working directory for horizon process is using. Based on [prerequisites](./prerequisites.mdx), volume should have at least 10mb/s, one way to roughly verify this on host machine(linux/mac) command line: ``` sudo dd if=/dev/zero of=/tmp/test_speed.img bs=1G count=1 ``` #### Monitoring Ingestion Process For high-availability deployments, it is recommended to implement monitoring of ingestion process for visibility on performance/health. Refer to [Monitoring](./monitoring.mdx) for accessing logs and metrics from Horizon. Stellar publishes the example [Horizon Grafana Dashboard](https://grafana.com/grafana/dashboards/13793-stellar-horizon), which demonstrates queries against key horizon ingestion metrics, specifically look at the `Local Ingestion Delay [Ledgers]` and `Last ledger age` in the `Health Summary` panel. --- ## Installing To install Horizon in production or non-development environments, we recommend the following based on target infrastructure: ### Bare-Metal - If host is Debian Linux, install prebuilt binaries [from repositories](#package-manager) using a package manager. - For any other hosts, download [prebuilt release binaries](#prebuilt-releases) of Stellar Horizon and Core for host target architecture and operation system or [compile from the source](https://github.com/stellar/stellar-horizon/blob/main/DEVELOPING.md). ### Containerized - Non-Orchestrated: if the target deployment environment does not include a container orchestrator such as Kubernetes, then this means you intend to run the Horizon release image from [dockerhub.com/stellar/stellar-horizon](https://hub.docker.com/r/stellar/stellar-horizon) as a container directly with Docker daemon on host. Choose the tag of the Horizon image for the specific release version and then pull the image using `docker pull stellar/stellar-horizon:` to get it locally onto host. - Orchestrated: when the target environment has container orchestration, such as Kubernetes cluster, we recommend using the [Horizon Helm Chart](https://github.com/stellar/helm-charts/tree/main/charts/horizon) to manage the installation and deployment lifecycle of the Horizon image as container(s) on the cluster. To install Horizon in development environments, refer to the [Horizon README](https://github.com/stellar/stellar-horizon) from the source code repo for options available. ### Notes on Installation #### Package Manager SDF publishes new releases to its custom Ubuntu repositories. Follow [this guide](https://github.com/stellar/packages/blob/master/docs/adding-the-sdf-stable-repository-to-your-system.md#adding-the-sdf-stable-repository-to-your-system) to add the stable SDF repository to your host system. If you are interested in installing release candidate versions of software that have yet to reach stable, refer to [Adding the Bleeding Edge Testing Repository](https://github.com/stellar/packages/blob/master/docs/adding-the-sdf-stable-repository-to-your-system.md#adding-the-bleeding-edge-testing-repository). Lastly, [install package](https://github.com/stellar/packages/blob/master/docs/installing-individual-packages.md#installing-individual-packages) outlines the various commands that these packages make available. To proceed with installation: ```bash sudo apt update sudo apt install stellar-horizon stellar-core ``` #### Prebuilt Releases Refer to the list of [Horizon releases](https://github.com/stellar/stellar-horizon/releases) and [Core releases](https://github.com/stellar/stellar-core/releases). Copy the binaries to host PATH. #### Verify Bare-Metal Installations Run `stellar-horizon --help` from a terminal. If the help for Horizon is displayed, your installation was successful. Some shells (such as [zsh](https://www.zsh.org)) cache PATH lookups. You may need to clear your cache (by using `rehash` in zsh, for example) or restart your shell before trying to run the command above. #### Helm Chart Installation If the deployment can be done on Kubernetes, there is a [Horizon Helm Chart](https://github.com/stellar/helm-charts/blob/main/charts/horizon) available. Install the [Helm CLI tool](https://helm.sh/docs/intro/install), if you haven't already on your workstation, minimum of version 3. Next, add the Stellar repo to the helm client's list of repos and confirm that you can view the list of available chart versions for the repo: ```bash helm repo add stellar https://helm.stellar.org/charts helm repo update stellar helm search repo stellar/horizon --versions --devel ``` Wait to install the Horizon Helm Chart, it will be done after [Configuring](./configuring.mdx) is completed and in [Running](./running.mdx). If Kubernetes is not an option, the helm charts may still be good reference for showing how to configure and run the Horizon Docker container. Just run the helm command with `template` to display the generated Kubeneretes manifests, which demonstrate all the container configurations needed: ```bash git clone https://github.com/stellar/helm-charts; cd helm-charts helm template -f charts/horizon/values.yaml charts/horizon/ ``` ## Next Step After installation is complete, you are now ready to proceed to [Configuring Horizon](./configuring.mdx)! --- ## Monitoring ## Metrics Metrics are emitted from Horizon over HTTP in [the de facto text-based exposition format](https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#text-based-format). The Metrics are published on the _private_ `/metrics` path of Horizon Admin port, which is an optional service to be started and will be bound by the Horizon process onto the host machine loopback network(localhost or 127.0.0.1). To enable the Admin port, add environment configuration parameter `ADMIN_PORT=XXXXX`, the metrics endpoint will be reachable on the host machine as `localhost:/metrics`. You can verify this by pointing any browser that can reach this address, it will print out all metrics keys. ### Exporting Once the Admin port is enabled, the Horizon metrics endpoint can be 'scraped' by external monitoring infrastructure. Since the metrics output is encoded to [standard text-based format](https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#text-based-format) it will be compatible for usage with many types of monitoring infrastructure that interoperate with the same standard format. In the case of Horizon, metrics are published on the Admin HTTP port which is bound to the host machine's loop back network interface(127.0.0.1), so external monitoring systems or services cannot reach the port directly. To expose the metrics securely, we recommend following the exporter pattern, which is common metrics scraping strategy. One real-world example for exporting from a bare metal Horizon installation (Horizon has been installed directly onto an operating system), use [FluentBit and the Prometheus Exporter](https://docs.fluentbit.io/manual/pipeline/outputs/prometheus-exporter) on the same host machine as Horizon is running. FluentBit will perform a simple port forwarding pipeline on the host machine. Configure the input to be Horizon's `localhost:/metrics` and the output to be the `host` and `port` representing the target network interface and port on the host machine, and then configure your monitoring infrastructure to scrape that host address. In container-orchestrated environments such as Kubernetes, you can use the same exporter strategy. We assume you already have a metrics infrastructure deployment like Prometheus and Grafana setup on the cluster via the [Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator), and will just need to configure that infrastructure to scrape the Horizon pod based on the ADMIN_PORT. ### Data model There are numerous application metrics keys emitted by Horizon at runtime, encoded in four types of exposition formats: `counter`, `gauge`, `histogram`, `summary`. Each key is further qualified with labels for more granularity. To summarize, we can highlight the groupings of metrics keys by common function denoted in the prefix of their name: - `go_`: golang specfic runtime performance - `horizon_txsub_`: attributes of Horizon transaction submission sub system if enabled. - `horizon_stellar_core_`: runtime attributes of Stellar network reported by the captive core. - `horizon_order_book_`: runtime attributes of the in memory order book maintained by Horizon of the current Stellar network - `horizon_log_`: counters of how many log messages printed at each severity level - `horizon_ingest_`: performance measurements and stateful aspects of Horizon's internal ingestion sub system - `horizon_http_`: statistics and measurements of Horizon's HTTP API service, all aspects of request/response load and timings. - `horizon_history_`: statistics on Horizon ingested historical ledgers - `horizon_db_`: measurements on database performance, query times per endpoints, pooling stats - `process_`: generic host machine compute measurements And for each key, there will be a possibility of 0 or more labels, the serialized output(exposition) format follows this template: ``` {label_1="value",label_2="value",,,} ``` Rather than listing all individual metrics keys in docs, as they change often, the recommendation is to perform an HTTP GET against the Horizon metrics endpoint,`localhost:/metrics`, using any http client(browser, curl, wget, etc) and the response will have the metrics keys and additional meta information on each metric key for description and type(counter, gauge, histogram, summary), as an example for one key, `horizon_http_requests_duration_seconds`: ``` # HELP horizon_http_requests_duration_seconds HTTP requests durations, sliding window = 10m # TYPE horizon_http_requests_duration_seconds summary horizon_http_requests_duration_seconds{method="GET",route="/",status="200",streaming="false",quantile="0.5"} 0.000186958 horizon_http_requests_duration_seconds{method="GET",route="/",status="200",streaming="false",quantile="0.9"} 0.00043625 horizon_http_requests_duration_seconds{method="GET",route="/",status="200",streaming="false",quantile="0.99"} 0.000645 ... ``` ### Queries Build queries against the metrics data model to highlight the performance of a given Horizon deployment. Refer to Stellar's [Grafana Horizon Dashboard](https://grafana.com/grafana/dashboards/13793-stellar-horizon) for examples of metrics queries to derive application performance: - Number of requests per minute. - Number of requests per route (the most popular routes). - Average response time per route. - Maximum response time for non-streaming requests. - Number of streaming vs. non-streaming requests. - Number of rate-limited requests. - List of rate-limited IPs. - Unique IPs. - The most popular SDKs/apps sending requests to a given Horizon node. - Average ingestion time of a ledger. - Average ingestion time of a transaction. Choose the [revisions tab](https://grafana.com/grafana/dashboards/13793-stellar-horizon/?tab=revisions), and download the dashboard source file to have access to the Grafana dashboard source code and metrics queries that build each panel in dashboards. ### Alerts Once queries are developed on a Grafana dashboard, it enables a convenient follow-on step to add [alert rules](https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule) based on specific queries to trigger notifications when thresholds are exceeded. Here are some example alerts to consider with potential causes and solutions. | Alert | Cause | Solution | | --- | --- | --- | | Spike in number of requests | Potential DoS attack | network load balance or content switch configurations | | Ingestion is slow | host server compute resources are low | increase compute specs | | HTTP API responses are returning errors | host server compute resources are low or networking to DB is lost | check the [Horizon logs](#logs) to see what errors are being emitted, narrow down root cause from there | ## Logs Horizon will output logs to operating system's standard out. It will log on all aspects of runtime, including HTTP requests and ingestion. Typically, there are very few `warn` or `error` severity level messages emitted. The default severity level logged in Horizon is configured to `LOG_LEVEL=info`, this environment configuration parameter can be set to one of `trace, debug, info, warn, error`. The verbosity of log output is inverse of the severity level chosen. I.e. for most verbose logs use 'trace', for least verbose logs use 'error'. For production deployments, we recommend using the default severity setting of `info` level and choose a log capture strategy depending on the deployment. - Bare metal deployment direct to operating system, redirect the standard out from Horizon process to a file on disk and apply a log rotation tool on the file such as [logrotate](https://man7.org/linux/man-pages/man8/logrotate.8.html) to manage disk space usage. - Orchestrated deployment on Kubernetes, use an EFK/ELK stack on the cluster and it can be configured to capture the standard out from Horizon pod. ## Runtime Profiling Horizon is written in Golang, therefore it has been enabled to optionally emit the Golang runtime diagnostics and profiling output [pprof](https://go.dev/doc/diagnostics). The pprof HTTP endpoints are hosted on Horizon's admin HTTP port, it can be enabled by adding environment configuration parameter `ADMIN_PORT=XXXXX`, since the admin port binding is disabled by default. Two of the standard predefined profiles are published: `localhost:/debug/pprof/heap` - heap profiling `localhost:/debug/pprof/profile` - cpu profiling Use Go's pprof command line tool to access the published endpoints and visualize the profiled diagnostic data that is emitted. A brief example usage of the pprof tool from command line to get started, using `web` to display a graphical representation of current heap allocations: ``` $ go tool pprof http://localhost:6060/debug/pprof/heap Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap Saved profile in ./pprof/pprof.stellar-horizon.alloc_objects.alloc_space.inuse_objects.inuse_space.022.pb.gz File: stellar-horizon Type: inuse_space Entering interactive mode (type "help" for commands, "o" for options) (pprof) web ``` ## I'm Stuck! Help! If any of the above steps don't work or you are otherwise prevented from correctly setting up Horizon, please join our community and let us know. Either post a question at [our Stack Exchange](https://stellar.stackexchange.com) or chat with us on [Horizon Discord](https://discord.com/channels/897514728459468821/912466080960766012) to ask for help. --- ## Overview(Admin-guide) Horizon is a central component of the Stellar platform: it provides an HTTP API to data in the Stellar network. It ingests and re-serves the data produced by the Stellar network in a form that is easier to consume by the average application relative to the performance-oriented data representations used by Stellar Core. This guide describes how to administer a production Horizon instance (refer to the [Developers' Blog](https://stellar.org/blog/developers/a-new-sun-on-the-horizon) for some background on the performance and architectural improvements of this major version bump). For information about developing on the Horizon codebase, check out the [Development Guide](https://github.com/stellar/stellar-horizon/blob/main/internal/docs/DEVELOPING.md). Before we begin, it's worth reiterating the sentiment echoed in the [Run a Core Node](../../../../validators/README.mdx) guide: **we do not endorse running Horizon backed by a standalone Stellar Core instance**, and especially not by a _validating_ Stellar Core. These are two separate concerns, and decoupling them is important for both reliability and performance. Horizon instead manages its own, pared-down version of Stellar Core optimized for its own subset of needs (we'll refer to this as a "Captive Core" instance). ## Why Run Horizon? Running Horizon within your own infrastructure provides a number of benefits. You can: - Have full operational control without dependency on the Stellar Development Foundation for network data and transaction submission to networks; - Run multiple instances for redundancy and scalability. --- ## Prerequisites The Horizon service is responsible for synchronizing with the Stellar network and processing ledger data. To understand the scope of Horizon's services, please read the [configuring](./configuring.mdx) section before you move on to the prerequisites for computation. The Horizon service can be [installed](./installing.mdx) on bare metal or a virtual machine. It is natively supported on both Linux and Windows operating systems. ## Single Instance Deployment Model For a basic setup using the [Single Instance Deployment model](./configuring.mdx#single-instance-deployment), you will need a sum of two distinct compute profiles: - One for hosting the Horizon service - Another for hosting the PostgreSQL server ### Hardware requirements The minimum hardware specifications to effectively run Horizon are as follows: #### Horizon Compute Instance: | Node Type | CPU | RAM | Disk | AWS SKU | Google Cloud SKU | | --- | --- | --- | --- | --- | --- | | Horizon API Service | 4 vCPU | 16 GB | 100 GB SSD >= 3K IOPS | [c5d.xlarge] | [n4-standard-4] | _\* Assuming a 30-day retention window for data storage._ #### PostgreSQL Database Server Compute Instance: | Node Type | CPU | RAM | Disk | AWS SKU | Google Cloud SKU | | --- | --- | --- | --- | --- | --- | | Horizon PostgreSQL | 4 vCPU | 32 GB | 2 TB\* SSD (NVMe or Direct Attached Storage) >= 7K IOPS | [i4g.xlarge] | [c3-highmem-8] | _\* Assuming a 30-day retention window for data storage._ Please note that a minimum of PostgreSQL version 12 is required. These specifications assume a 30-day retention window for data storage. For a longer retention window, the system requirements will be higher. For more information about data ingestion, history retention, and managing storage, check the [ingestion](./ingestion.mdx) section. ## Multiple Instance Deployment To achieve high availability, redundancy, and high throughput, refer to the [scaling](./scaling.mdx) documentation. It provides a detailed overview of several different deployment strategies you can employ, depending on the SLA you need your Horizon instance to achieve. ## Network Access - Ensure that the Horizon instance can establish a connection with the PostgreSQL database instance. The default port for PostgreSQL is 5432. - A stable and fast network connection with the Internet is required for any Horizon instance running the ingestion role. This is to ensure it has efficient outbound connectivity to remote hosts in the [quorum set](../../../../validators/admin-guide/configuring.mdx#choosing-your-quorum-set) and [archive urls](../../../../validators/admin-guide/environment-preparation.mdx#history-archives) for the chosen Stellar network. During ingestion, the Horizon instance communicates with these hosts, receiving network transaction data through its local captive core sub-process. :::note Hardware requirements may increase as the Stellar network grows and/or if you're sharing resources or using custom configs. ::: [c5d.2xlarge]: https://aws.amazon.com/ec2/instance-types/c5/ [n4-standard-4]: https://cloud.google.com/compute/docs/general-purpose-machines#n4-standard [i4g.xlarge]: https://aws.amazon.com/ec2/instance-types/i4g/ [c3-highmem-8]: https://cloud.google.com/compute/docs/general-purpose-machines#c3_machine_types --- ## Running Once you have [established the Horizon database](./configuring.mdx#initialize-horizon-database) and have [identified the Horizon runtime config per host](./configuring.mdx#prerequisites), you're ready to run Horizon. ## Bare-metal installation Run the `stellar-horizon` binary with the [appropriate environment parameters](./configuring.mdx#passing-configurations-to-horizon) set (or `stellar-horizon-cmd serve` if you [installed via the package manager](./installing.mdx#package-manager), which will automatically import your configuration from `/etc/default/stellar-horizon`). ## Containerized installation You don't execute the Horizon binary directly, instead the [stellar/stellar-horizon](https://hub.docker.com/r/stellar/stellar-horizon) image has a pre-defined entrypoint that will start running Horizon at image startup time. The Horizon process will get all configuration settings from container environment variables. ### Docker daemon Use `docker run stellar/stellar-horizon: --env-file `, and specify each Horizon configuration flag identified during [Configuring](./configuring.mdx) as a separate line in `` of `HORIZON_CONFIG_PARAM=value`. ### Kubernetes using Helm Chart Ensure you have followed the [pre-requisite](./installing.mdx#helm-chart-installation) of installing the Helm CLI tool and added the Stellar chart repo to Helm client. The Horizon process [requires access to a Postgres 12 database](./prerequisites.mdx#postgresql-database-server-compute-instance). First use the common Kubernetes CLI tool `kubectl` from your workstation to create a Kubernetes secret on the intended namespace of the Kubernetes cluster which will hold the Horizon database URL. ```bash # copy your horizon DATABASE_URL into a secure file, no line breaks. echo -n 'database_url_here' > my_creds.txt # now generate the Kubernetes secret from the file kubectl create secret generic \ -n my-namepsace\ my-db-secret \ --from-file=DATABASE_URL=my_creds.txt ``` Now deploy Horizon onto the cluster using the Helm Chart: ```bash helm install my-horizon stellar/horizon \ --namespace my-horizon-namespace-on-cluster \ --set ingest.persistence.enabled=true \ --set web.replicaCount=1 \ --set web.enabled=true \ --set ingest.enabled=true \ --set ingest.replicaCount=1 \ --set web.existingSecret=my-db-secret \ --set global.image.horizon.tag=2.26.1 \ --set global.network=testnet \ --set ingest.existingSecret=my-db-secret \ --set ingest.horizonConfig.captiveCoreUseDb=true \ --set ingest.resources.limits.cpu=1 \ --set ingest.resources.limits.memory=6Gi ``` This example of Helm Chart usage highlights some key aspects: - Uses the `global.network=[testnet|pubnet]` parameter, this automates generation of all the Horizon configuration parameters specific to the network such as archive urls, captive core config, and other parameters mentioned in [Configuring](./configuring.mdx). - `global.image.horizon.tag` should be set to one of the Docker Hub tags published on [stellar/stellar-horizon](https://hub.docker.com/r/stellar/stellar-horizon) - Enables all roles on the deployment instance: ingesting and web API (includes transaction submission). If you choose to have a multi-instance deployment with each instance performing a single role of just web API or ingestion, then you will do two Helm installations, one for each role: `my-horizon-ingestion-installation` and `my-horizon-api-installation`. Each of these Helm installations will set `ingest.enabled`, `web.enabled`, `ingest.replicaCount`, `web.replicaCount` respectively for the role they are performing. - To customize further, the best approach is to download the [Horizon Helm Chart values.yaml](https://github.com/stellar/helm-charts/blob/main/charts/horizon/values.yaml), update the settings in your local copy of values.yaml, and pass to Helm install, rather than have many individual `--set` on Helm install: ```bash helm install myhorizon stellar/horizon \ --namespace my-horizon-namespace-on-cluster \ --values values.yaml ``` - Customizing network configuration parameters, If you want to connect to a network other than presets of `testnet` or `pubnet`, then you won't use `global.network`, instead, use local copy of [values.yaml](https://github.com/stellar/helm-charts/blob/main/charts/horizon/values.yaml) and set `ingest.coreConfig`, and refer to [\_core-config.tpl](https://github.com/stellar/helm-charts/blob/main/charts/horizon/templates/_core-config.tpl) for example of all the key/value pairs to include. - Minimum resource limits, verify whether `LimitRange` defaults are defined on the target namespace in Kubernetes for deployment, if so, ensure that the defaults provide at least minimum resource limits of `6Gi` of memory and `1` cpu. Otherwise, define the limits explicitly on the helm install via the `ingest.resources.limits.*` shown in example, to ensure the deployed pods have adequate resources. Once the Horizon process starts, it will emit logging to standard out. When run, you should see output similar to: ``` INFO[...] Starting horizon on :8000 pid=29013 ``` Note that the numbers may naturally be different for your installation. The log line above announces that Horizon is ready to serve client requests. Next, you can confirm that Horizon is responding correctly by loading the root resource. In the example above, that URL would be http://127.0.0.1:8000/, and simply running `curl http://127.0.0.1:8000/` would show you that the root resource loads correctly: ```json { "_links": { "account": { "href": "http://127.0.0.1:8000/accounts/{account_id}", "templated": true }, "accounts": { "href": "http://127.0.0.1:8000/accounts{?signer,sponsor,asset,cursor,limit,order}", "templated": true } } // etc. } ``` Refer to [Monitoring](./monitoring.mdx) for more details on Horizon runtime logging and metrics available. --- ## Scaling Horizon enables different logical tiers that can be scaled independently for increasing throughput, isolation, and availability. The following components can be independently scaled: - Web service API (serving) - Captive Core (ingestion and transaction submission) - Database (storage) ## Single Instance Deployment It is recommend to start with a [single instance deployment](./prerequisites.mdx), and scale up based on the needs of your particular use-case. This [deployment](./configuring.mdx#single-instance-deployment) is intended for use with minimal history retention (\<= 30 days) and minimal request volume. In this setup, a single instance of Horizon performs all three [roles](./configuring.mdx#multiple-instance-deployment); ingestion, transaction submission, and end-user API requests. ![](/assets/horizon-scaling/Topology-single.png) ## Scaling to Multiple Instances There are a few reasons you may choose to scale to multiple instances of Horizon. - Horizontally scaling enables you to serve more API requests and at a faster rate - Redundancy enables zero downtime in the cases where Horizon requires downtime on upgrade (migrations, state rebuilds, etc) - Protection against potential ingestion lag, which could result in downtime for end-users Multiple instances of Horizon can be configured to point to the same database, and the ingestion process will not perform redundant work in these cases. When scaling Horizon, it is worth it to note that Horizon's [rate limiting](../api-reference/structure/rate-limiting.mdx) should be disabled and rate limiting should be managed external to Horizon within infrastructure. Horizon's rate limiting implementation is managed in-memory, so does not work with multiple instances. ![](/assets/horizon-scaling/Topology-multiple.png) ## Logically Isolating Ingestion Ingestion is the process by which new ledgers are propagated into Horizon's database. It's health is critical, as degredations in performance can result in falling behind the last closed ledger, leaving your end-users unaware of the current state of the network, and unable to successfully submit new transactions. Any lag in ingestion would likely be considered downtime for your service Horizon allows you to independently configure the different [roles](./configuring.mdx#multiple-instance-deployment) that it performs, including ingestion. The below diagram illustrates how you could logically separate the instances serving API requests from the instances performing ingestion, and introduce a read-only replica database in order to further isolate these components. This setup has quite a few advantages: - Each "role" Horizon plays can be independently scaled - API instances are significantly ligher weight from a hardware requirements perspective, since they do not need to run captive core - API instances can be horizontally scaled or dynamically scaled, based on your specific end-user needs - Ingestion and its performance is isolated from API activity, so bursts in user activity cannot degrade it and cause ingestion lag. Ingestion health is critical, as degredations in performance can result in falling behind the last closed ledger, leaving your end-users unaware of the current state of the network, and unable to successfully submit new transactions The Horizon API role requires only read-only permissions to a database for all actions it performs. However, the API instances will need to delegate all transaction submission requests to an instance which runs captive core. Further database replicas could be added if necessary to support more requests. ![](/assets/horizon-scaling/Topology-ingestion-isolation.png) ## Logically Isolating Transaction Submission In the above example, ingestion is safely isolated from most API traffic, which has historically been the large majority of traffic. However, transaction submission still needs to be served by a core instance, and so API instances must passthrough their transaction submission requests to an ingesting instance. The below diagram illustrates how we could further isolate (and scale) transaction submission, by way of using core watcher instances, rather than Horizon instances running captive core. This allows us to further protect ingestion, preventing downtime and ingestion lag. It also makes it possible to horizontally scale transaction submission itself, independent of the rest of the API traffic. ![](/assets/horizon-scaling/Topology-txsub.png) --- ## Upgrading Here we'll describe the recommended steps for upgrading a Horizon 2.x installation. ### Pre-requisites - An existing Horizon deployment consisting of one or more instances of Horizon. - All instances are on same 2.x version to begin. - If [bare-metal](./installing.mdx#bare-metal) install, you have shell, or command line access to each host having a Horizon installation. - If [deployed direct on Docker daemon](./installing.mdx#containerized), you have command line access to the host that is running the Docker daemon. - If [deployed on Kubernetes with Helm chart](./installing.mdx#helm-chart-installation), you have kubectl and helm command line tools on your workstation and a user login with appropriate access levels to change resources in target namespace of Horizon deployment on the cluster. ### Assess current installation - Identify the list of all instances of Horizon that need to be upgraded. - Bare-metal installations: the list of hosts is managed by you. - Docker daemon deployments: the list of hosts and running containers is managed by you. - Kubernetes deployments: get the list of pods that are deployed from your prior Helm installation, they will have an annotation for `release=your_helm_horizon_installation_name`: ```bash kubectl get pods -l release=your_helm_horizon_installation_name -n ``` - Identify your current Horizon software version: - Obtain command line access to the operating system of each Horizon instance: - Bare-metal installations, this is typically ssh on Linux or powershell on Windows. - Docker daemon deployments, use `docker exec -it /bin/bash` - For Kubernetes deployments, use `kubectl exec -it -n -- /bin/bash` - On command line of each instance, run `stellar-horizon version` - All instances should report the same version, if not, the system may be inconsistent, use this upgrade as opportunity to establish consistency and get them all on same version. ### Determine the target version for upgrade Now that you know your current Horizon version, visit [Horizon Releases](https://github.com/stellar/stellar-horizon/releases) and choose the next greater version above your current version to upgrade. Follow steps [recommended by GitHub to compare releases](https://docs.github.com/en/repositories/releasing-projects-on-github/comparing-releases), click on the `Compare` dropdown of the chosen release, and then select your current release and GH will display the differences between versions, select the `Files changed` tab, and go to the `services/horizon/CHANGELOG.md`, it will highlight the new release notes for changes that have occurred between your current version and the new version you selected. Review this and look for any `Breaking Changes`, `State Rebuild` and `DB Schema Migration` sections for consideration, as the latter two will also mention expected time for the state rebuild or db migration to apply respectively. ### Install the new version Now that you have indentified the new version and are aware of the potential impacts from upgrading to new version based on release notes, such as state rebuilds and db migrations, you are informed and ready to proceed with upgrade. Upgrading production deployents should leverage a secondary, hot-backup deployment, also known as a [blue/green model](./scaling.mdx#scaling-to-multiple-instances) and perform the upgrade on the inactive deployment first. This will avoid downtime of system to your external users, as the upgrade takes place on the inactive deployment. A good strategy for upgrading Horizon and applicable to single or multi-instance deployments - shut all instances down, install new Horizon version on one of the ingesting instances first. The reason being Horizon software will only initate `State Rebuild` and `DB Schema Migration` actions related to an upgrade on an instance that it detects ingestion has been enabled with configuration parameter, `INGEST=true`. This lowers complexity for you during the upgrade as you only need to focus on one instance and it avoids potential concurrent Horizon ingestion processes attempting the same upgrade on the database. - Bare-metal installations, stop the Horizon process on all instances first, then shell into one instance that is configured for ingestion, and use apt package manager on linux. ```bash sudo apt update sudo apt install stellar-horizon=new_horizon_debian_pkg_version ``` Restart Horizon using the configuration already in place, but include `APPLY_MIGRATIONS=true` environment variable, this will trigger Horizon to automatically run any db migrations that it detects are needed. - Docker daemon deployments, stop all docker containers first, then choose one container that has ingestion enabled, set the new tag for the image based on release published on dockerhub - [stellar/stellar-horizon](https://hub.docker.com/r/stellar/stellar-horizon/tags), and restart the container in docker daemon, include `APPLY_MIGRATIONS=true` environment variable to the container envrionment, this will trigger Horizon to automatically run any db migrations that it detects are needed. - For Helm installations on Kubernetes, first use your helm cli tool to stop all Horizon instances by scaling all your Horizon installations(for ingest and web) down to 0 replicas, which you've created prior on [run steps](./running.mdx). ```bash helm upgrade all-my-horizon-installations \ --namespace my-horizon-namespace-on-cluster \ --set ingest.replicaCount=0 \ --set web.replicaCount=0 ``` Now, use helm to start just a single Horizon instance from a helm installation that has ingestion enabled on Kubernetes cluster, you will set the `global.image.horizon.tag` to the release tag published on [stellar/stellar-horizon](https://hub.docker.com/r/stellar/stellar-horizon/tags) ```bash helm upgrade my-horizon \ --namespace my-horizon-namespace-on-cluster \ --set global.image.horizon.tag=new_horizon_release_number \ --set ingest.horizonConfig.applyMigrations=True \ --set ingest.replicaCount=1 ``` ### Confirming the upgrade on single ingestion instance first If you have [monitoring](./monitoring.mdx) infrastructure in place, then you have two options for assessing the upgrade status: - View metrics outputs using grafana dashboards that leverage queries on the [Horizon metrics data model](./monitoring.mdx#data-model) to check key stats like ingestion and network ledgers are advancing and in step. - View the Horizon web server 'status' url path on the upgraded instance: ```bash curl http://localhost:8000/ ``` The response will be HTTP status code 200 and body of response will be a text based json data structure with diagnostic info on current Horizon software version, and ledger numbers for ingestion and network, refresh the url every 5 seconds or so, and should see the ingestion and network ledger numbers advancing and in step, indicating good connection to the network and ingestion. If metrics and/or the Horizon 'status' url respones don't indicate healthy status based on advancing ledger ingestion, two steps to triage further: - A delay in Horizon achieving healthy status after an upgrade is expected and legitmate for any upgrade cases where `State Rebuild` or `DB Migration` was noted in the release delta as part of prior [Determine the target version for upgrade step](#determine-the-target-version-for-upgrade). Typically the notes will also mention relative timeframe expectations for those to complete which can be factored in to how long to wait on delay. - Check the logs from the upgraded instance to confirm what's going on. Any `State Rebuild` or `DB Migration` initiated will be mentioned. For example, a db migration will be noted in logs with following lines for start and finish: ``` 2023/09/22 18:27:01 Applying DB migrations... 2023/09/22 18:27:01 successfully applied 5 Horizon migrations ``` ### Upgrade all remaining instances At this point, you have upgraded one ingesting instance to the new Horizon version, it has automatically updated the database if required and the instance is running with healthy status. Now, install the same Horizon software version on the remainder of instances, restarting each after the upgrade. For bare-metal and docker daemon installations that will likely be self explanatory on how to accomplish that for remainder of instances, on helm chart installations, run the helm upgrade again, setting the image tag and also restoring original `replicaCount`s: ```bash helm upgrade all-my-horizon-installations \ --namespace my-horizon-namespace-on-cluster \ --set ingest.replicaCount=1 \ --set web.replicaCount=1 \ --set global.image.horizon.tag=new_horizon_release_number \ --set ingest.horizonConfig.applyMigrations=False ``` For production deployments following the hot backup or blue/green model, this is the opportunity to confirm the inactive deployment has taken the upgrade correctly and stable, at which point, switch the load balancers now to forward traffic to the inactive deployment, making it the active deployment. Now, can take time to perform same upgrade on the other deployment which is now inactive. --- ## API Reference View all Horizon API information. --- ## Aggregations Endpoints that aggregate data about the ledger. | | | | ----------------------------------------------------- | --- | | [Order Books](./order-books/README.mdx) | | | [Paths](./paths/README.mdx) | | | [Trade Aggregations](./trade-aggregations/README.mdx) | | | [Fee Stats](./fee-stats/README.mdx) | | --- ## Fee Stats Fee stats are used to predict what fee to set for a transaction before submitting it to the network. | | | | --- | ---------------------------------------------- | | GET | [/fee-stats](../../retrieve-fee-stats.api.mdx) | --- ## The Fee Stats Object When Horizon returns fee stats, it uses the format below. :::note The `fee_charged` represents the actual fee paid for the transaction, while `max_fee` represents the maximum bid the transaction creator was willing to pay for the transaction. ::: - ATTRIBUTE - DATA TYPE - DESCRIPTION - last_ledger - string - The last ledger's sequence number. - last_ledger_base_fee - string - The base fee as defined in the last ledger. - ledger_capacity_usage - string - The average capacity usage over the last 5 ledgers (0 is no usage, 1.0 is completely full ledgers). - fee_charged - object - Information about the fee charged for transactions in the last 5 ledgers. - min - string - Minimum fee charged over the last 5 ledgers. - mode - string - Mode fee charged over the last 5 ledgers. - p10 - string - 10th percentile fee charged over the last 5 ledgers. - p20 - string - 20th percentile fee charged over the last 5 ledgers. - p30 - string - 30th percentile fee charged over the last 5 ledgers. - p40 - string - 40th percentile fee charged over the last 5 ledgers. - p50 - string - 50th percentile fee charged over the last 5 ledgers. - p60 - string - 60th percentile fee charged over the last 5 ledgers. - p70 - string - 70th percentile fee charged over the last 5 ledgers. - p80 - string - 80th percentile fee charged over the last 5 ledgers. - p90 - string - 90th percentile fee charged over the last 5 ledgers. - p95 - string - 95th percentile fee charged over the last 5 ledgers. - p99 - string - 99th percentile fee charged over the last 5 ledgers. - max_fee - object - Information about max fee bid for transactions over the last 5 ledgers. - min - string - Minimum (lowest) value of the maximum fee bid over the last 5 ledgers. - mode - string - Mode max fee over the last 5 ledgers. - p10 - string - 10th percentile max fee charged over the last 5 ledgers. - p20 - string - 20th percentile max fee charged over the last 5 ledgers. - p30 - string - 30th percentile max fee charged over the last 5 ledgers. - p40 - string - 40th percentile max fee charged over the last 5 ledgers. - p50 - string - 50th percentile max fee charged over the last 5 ledgers. - p60 - string - 60th percentile max fee charged over the last 5 ledgers. - p70 - string - 70th percentile max fee charged over the last 5 ledgers. - p80 - string - 80th percentile max fee charged over the last 5 ledgers. - p90 - string - 90th percentile max fee charged over the last 5 ledgers. - p95 - string - 95th percentile max fee charged over the last 5 ledgers. - p99 - string - 99th percentile max fee charged over the last 5 ledgers. ```json { "last_ledger": "28444678", "last_ledger_base_fee": "100", "ledger_capacity_usage": "0.2", "min_accepted_fee": "100", "mode_accepted_fee": "100", "p10_accepted_fee": "100", "p20_accepted_fee": "100", "p30_accepted_fee": "100", "p40_accepted_fee": "100", "p50_accepted_fee": "100", "p60_accepted_fee": "100", "p70_accepted_fee": "100", "p80_accepted_fee": "100", "p90_accepted_fee": "100", "p95_accepted_fee": "100", "p99_accepted_fee": "10000", "fee_charged": { "max": "100", "min": "100", "mode": "100", "p10": "100", "p20": "100", "p30": "100", "p40": "100", "p50": "100", "p60": "100", "p70": "100", "p80": "100", "p90": "100", "p95": "100", "p99": "100" }, "max_fee": { "max": "16000", "min": "100", "mode": "100", "p10": "100", "p20": "100", "p30": "100", "p40": "100", "p50": "100", "p60": "100", "p70": "100", "p80": "100", "p90": "100", "p95": "100", "p99": "10000" } } ``` --- ## Order Books An order book is a collection of offers for a specific pair of assets. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. Learn more about [order books](../../../../../../learn/glossary.mdx#decentralized-exchange). | | | | --- | --------------------------------------------------- | | GET | [/order_book](../../retrieve-an-order-book.api.mdx) | --- ## The Order Book Object When Horizon returns information about an order book, it uses the following format: - ATTRIBUTE - DATA TYPE - DESCRIPTION - bids - object - The prices and amounts for the buyside of the asset pair. - price_r - object - A precise representation of the bid price of the asset pair. - n - number - The numerator. - d - number - The denominator. - price - string - The bid price of the base asset denominated in the counter asset. A number representing the decimal form of `price_r`. - amount - string - The amount of counter asset that the account making this offer is willing to buy at this price. - asks - object - The prices and amounts for the sellside of the asset pair. - price_r - object - A precise representation of the ask price of the asset pair. - n - number - The numerator. - d - number - The denominator. - price - string - The ask price of the base asset denominated in the counter asset. A number representing the decimal form of `price_r`. - amount - string - The amount of counter asset that the account making this offer is willing to sell at this price. - base - object - Details about the base asset. - asset_type - string - The type for the base asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - asset_code - string - The code for the base asset. - asset_issuer - string - The Stellar address of the base asset’s issuer. - counter - object - Details about the counter asset. - asset_type - string - The type for the counter asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - asset_code - string - The code for the counter asset. - asset_issuer - string - The Stellar address of the counter asset’s issuer. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. ```json { "bids": [ { "price_r": { "n": 6014600, "d": 102275119 }, "price": "0.0588080", "amount": "0.1722469" }, { "price_r": { "n": 1250000, "d": 21831117 }, "price": "0.0572577", "amount": "0.2991796" } ], "asks": [ { "price_r": { "n": 118163, "d": 2000000 }, "price": "0.0590815", "amount": "8057.2710223" }, { "price_r": { "n": 60627, "d": 1000000 }, "price": "0.0606270", "amount": "10000.0000000" } ], "base": { "asset_type": "native" }, "counter": { "asset_type": "credit_alphanum4", "asset_code": "USD", "asset_issuer": "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX" } } ``` --- ## Paths Paths provide information about potential path payments. A path can be used to populate the necessary fields for a path payment operation. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. Learn more about the two types of path payment: [`path payment strict send`](../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#path-payment-strict-send) and [`path payment strict receive`](../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#path-payment-strict-receive) | | | | --- | --- | | GET | [/paths/strict-receive](../../list-strict-receive-payment-paths.api.mdx) | | GET | [/paths/strict-send](../../list-strict-send-payment-paths.api.mdx) | --- ## The Path Object When Horizon returns information about a path, it uses the following format: - ATTRIBUTE - DATA TYPE - DESCRIPTION - source_asset_type - string - The type for the source asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - source_asset_code - string - The code for the source asset. - source_asset_issuer - string - The Stellar address of the source asset’s issuer. - source_amount - string - An estimated cost for making a payment of destination_amount on this path. Suitable for use in the `sendMax` field of a path payment operation. - destination_asset_type - string - The type for the destination asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - destination_asset_code - string - The code for the destination asset. - destination_asset_issuer - string - The Stellar address of the destination asset’s issuer. - destination_amount - string - The destination amount specified in the search that found this path. - path - array of objects - The intermediary assets that this path hops through. - asset_code - string - The code for this intermediary asset. - asset_issuer - string - The Stellar address of the intermediary asset’s issuer. - asset_type - string - The type for the intermediary asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. ```json { "_embedded": { "records": [ { "source_asset_type": "credit_alphanum4", "source_asset_code": "USD", "source_asset_issuer": "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", "source_amount": "4.1900246", "destination_asset_type": "credit_alphanum4", "destination_asset_code": "BB1", "destination_asset_issuer": "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", "destination_amount": "5.0000000", "path": [ { "asset_type": "credit_alphanum4", "asset_code": "NGNT", "asset_issuer": "GAWODAROMJ33V5YDFY3NPYTHVYQG7MJXVJ2ND3AOGIHYRWINES6ACCPD" }, { "asset_type": "native" } ] }, { "source_asset_type": "native", "source_amount": "162.0291692", "destination_asset_type": "credit_alphanum4", "destination_asset_code": "BB1", "destination_asset_issuer": "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", "destination_amount": "5.0000000", "path": [ { "asset_type": "credit_alphanum4", "asset_code": "EURT", "asset_issuer": "GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S" } ] }, { "source_asset_type": "native", "source_amount": "162.3284659", "destination_asset_type": "credit_alphanum4", "destination_asset_code": "BB1", "destination_asset_issuer": "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", "destination_amount": "5.0000000", "path": [ { "asset_type": "credit_alphanum4", "asset_code": "USD", "asset_issuer": "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX" }, { "asset_type": "credit_alphanum4", "asset_code": "EURT", "asset_issuer": "GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S" } ] }, { "source_asset_type": "native", "source_amount": "367.4324508", "destination_asset_type": "credit_alphanum4", "destination_asset_code": "BB1", "destination_asset_issuer": "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", "destination_amount": "5.0000000", "path": [ { "asset_type": "credit_alphanum4", "asset_code": "BTC", "asset_issuer": "GAUTUYY2THLF7SGITDFMXJVYH3LHDSMGEAKSBU267M2K7A3W543CKUEF" } ] } ] } } ``` --- ## Trade Aggregations A trade aggregation represents aggregated statistics on an asset pair (base and counter) for a specific time period. Trade aggregations are useful to developers of trading clients and provide historical trade data. | | | | --- | ------------------------------------------------------------ | | GET | [/trade_aggregations](../../list-trade-aggregations.api.mdx) | --- ## The Trade Aggregation Object When Horizon returns information about a trade aggregation, it uses the following format: - ATTRIBUTE - DATA TYPE - DESCRIPTION - timestamp - string - Start time for this trade aggregation. Represented as milliseconds since epoch. - trade_count - integer - Total number of trades aggregated. - base_volume - string - Total volume of base asset. - counter_volume - string - Total volume of counter asset. - avg - string - Weighted average price of counter asset in terms of base asset. - high - string - The highest price for this time period. - high_r - object - The highest price for this time period as a rational number. - n - number - The numerator. - d - number - The denominator. - low - string - The lowest price for this time period. - low_r - object - The lowest price for this time period as a rational number. - n - number - The numerator. - p - number - The denominator. - open - string - The price as seen on first trade aggregated. - open_r - object - The price as seen on first trade aggregated as a rational number. - n - number - The numerator. - p - number - The denominator. - close - string - The price as seen on last trade aggregated. - close_r - object - The price as seen on last trade aggregated as a rational number. - n - number - The numerator. - p - number - The denominator. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/trade_aggregations?base_asset_type=native\u0026counter_asset_code=NGNT\u0026counter_asset_issuer=GAWODAROMJ33V5YDFY3NPYTHVYQG7MJXVJ2ND3AOGIHYRWINES6ACCPD\u0026counter_asset_type=credit_alphanum4\u0026limit=200\u0026order=asc\u0026resolution=3600000\u0026start_time=1582156800000\u0026end_time=1582178400000" }, "next": { "href": "https://horizon-testnet.stellar.org/trade_aggregations?base_asset_type=native\u0026counter_asset_code=NGNT\u0026counter_asset_issuer=GAWODAROMJ33V5YDFY3NPYTHVYQG7MJXVJ2ND3AOGIHYRWINES6ACCPD\u0026counter_asset_type=credit_alphanum4\u0026end_time=1582178400000\u0026limit=200\u0026order=asc\u0026resolution=3600000\u0026start_time=1582178400000" }, "prev": { "href": "" } }, "_embedded": { "records": [ { "timestamp": 1582156800000, "trade_count": 9, "base_volume": "3487.4699458", "counter_volume": "88675.3982178", "avg": "25.4268566", "high": "25.7603393", "high_r": { "N": 257603393, "D": 10000000 }, "low": "25.3804530", "low_r": { "N": 25380453, "D": 1000000 }, "open": "25.3990186", "open_r": { "N": 2500000, "D": 98429 }, "close": "25.7090558", "close_r": { "N": 1250000, "D": 48621 } }, { "timestamp": 1582160400000, "trade_count": 1, "base_volume": "0.1058787", "counter_volume": "2.7155348", "avg": "25.6476024", "high": "25.6476019", "high_r": { "N": 100000, "D": 3899 }, "low": "25.6476019", "low_r": { "N": 100000, "D": 3899 }, "open": "25.6476019", "open_r": { "N": 100000, "D": 3899 }, "close": "25.6476019", "close_r": { "N": 100000, "D": 3899 } }, { "timestamp": 1582164000000, "trade_count": 15, "base_volume": "3992.1321821", "counter_volume": "99702.0620798", "avg": "24.9746395", "high": "25.6764460", "high_r": { "N": 5000000, "D": 194731 }, "low": "24.9291379", "low_r": { "N": 249291379, "D": 10000000 }, "open": "25.6764460", "open_r": { "N": 5000000, "D": 194731 }, "close": "25.0055050", "close_r": { "N": 5001101, "D": 200000 } }, { "timestamp": 1582167600000, "trade_count": 4, "base_volume": "278.4950271", "counter_volume": "7047.6740325", "avg": "25.3062832", "high": "25.3844475", "high_r": { "N": 5000000, "D": 196971 }, "low": "25.2923181", "low_r": { "N": 252923181, "D": 10000000 }, "open": "25.2923181", "open_r": { "N": 252923181, "D": 10000000 }, "close": "25.3844475", "close_r": { "N": 5000000, "D": 196971 } }, { "timestamp": 1582174800000, "trade_count": 1, "base_volume": "9.9379734", "counter_volume": "252.7685174", "avg": "25.4346140", "high": "25.4346140", "high_r": { "N": 254346140, "D": 10000000 }, "low": "25.4346140", "low_r": { "N": 254346140, "D": 10000000 }, "open": "25.4346140", "open_r": { "N": 254346140, "D": 10000000 }, "close": "25.4346140", "close_r": { "N": 254346140, "D": 10000000 } } ] } } ``` --- ## Retrieve Related Operations This endpoint represents successful operations referencing a given claimable balance and can be used in streaming mode. Streaming mode allows you to listen for new operations referencing this claimable balance as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known operation unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream operations created since your request time. Request --- ## Retrieve Related Transactions This endpoint represents successful transactions referencing a given claimable balance and can be used in streaming mode. Streaming mode allows you to listen for new transactions referencing this claimable balance as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known transaction unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream transactions created since your request time. Request --- ## Errors(Errors) After processing a request, Horizon returns a success or error response to the client. A success response will return a Status Code of 200, and an error response will return a Status Code in the range of 4XX - 5XX along with additional information about why the request could not complete successfully. There are two categories of errors: [HTTP Status Codes](./http-status-codes/README.mdx) and [Result Codes](./result-codes/README.mdx). Result Codes only follow a Transaction Failed (400) HTTP Status Code. | | | | --- | --- | | [HTTP Status Codes](./http-status-codes/README.mdx) | Errors that occur at the Horizon Server level. | | [Result Codes](./result-codes/README.mdx) | Errors that occur at the Stellar Core level. | --- ## Error Handling It’s important to anticipate errors your users may encounter as you develop on Stellar. In many tutorials throughout our developer documentation, we leave out error handling code to focus on the example. In this section, we will do the opposite and talk specifically about the errors. By the end of this section, you should be able to categorize errors and understand the best way to handle them in your application. Many actions interact with the Stellar network through the Horizon API, and these possible actions fall into two main categories: 1. Queries (any `GET` request, like to `/accounts`) 2. Transaction submissions (a `POST /transactions`, `POST /transactions_async`). There are many possible error codes when executing these actions, and you can typically handle these error codes using the following strategies: - Request adjustments: adjusting the request to resolve structural errors with queries or transaction submissions. Suppose you’ve included a bad parameter, malformed your XDR, or otherwise didn’t follow the endpoint’s specification. In these cases, resolve the error by referencing the details or result codes of the error response. - Polling and retrying: this is the recommended way to work around latency or congestion issues encountered along the pipeline between your computer and the Stellar network, which can sometimes happen due to the nature of distributed systems. ## Error Handling for Queries Many `GET` requests have specific parameter requirements, and while the SDKs can help enforce them, you can still pass invalid arguments (for example, an asset string that isn’t [SEP-11](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0011.md#asset-trustlineasset) compatible) that error out every time. In this scenario, there’s nothing you can do aside from following the API specification. The `extras` field of the error response will often clue you in on where to look and what to look for. ```bash curl -s https://horizon-testnet.stellar.org/claimable_balances/0000 | jq '.extras' { "invalid_field": "id", "reason": "Invalid claimable balance ID" } ``` Note that the SDKs make it a point to distinguish an invalid request (as above) versus a missing resource (a `404 Not Found`) (for example, the generic `NetworkError` versus a `NotFoundError` in the JavaScript SDK), where the latter might not be considered an error depending on your situation. ## Error Handling for Transaction Submissions Horizon currently supports two types of transaction submission endpoints: 1. `/transactions_async`: Horizon submits a transaction to Stellar-Core in an asynchronous manner and passes the relevant Stellar-Core response back immediately to the client. It is then the client's responsibility to poll for the status of that transaction. 2. `/transactions`: Horizon submits a transaction to Stellar-Core and then waits for it to be ingested into its database. It will either return a success (200), failure (400) or a timeout response (504), after which it is the client's responsibility to poll for the status of the transaction. Note that polling the transaction hash will return a 404 until it gets included in the ledger, or it fails to do so. In both cases, you will get a response when Horizon has ingested it into the database. There are some resolution strategies that are common between the 2 endpoints while others strategies are more endpoint specific. ### Request Adjustments Certain transaction submission failures also need adjustments to succeed. - If the XDR is malformed, or the transaction is otherwise invalid, you’ll encounter a `400 Bad Request` (for example, an invalid source account). Both transactions and their operations can be easily malformed or invalid: look at the `extras.result_codes` field for details and cross-reference them with the appropriate result codes documentation to determine specifics. - Transaction fees are also a safe adjustment by modifying the fees via a [fee-bump transaction](../../../../../build/guides/transactions/fee-bump-transactions.mdx) if you get a `tx_insufficient_fee` error. Refer to the [Insufficient Fees and Surge Pricing](#insufficient-fees-and-surge-pricing) section later in this document for more information on managing fees and strategies around it. ### Polling and Retrying Transactions #### Async Transaction Submission Submissions using the `/transactions_async` endpoint return an immediate response back from Stellar-Core. There are different actions that clients can take based on the specific `tx_status` returned: 1. `PENDING`: The submission is successful but the transaction is still waiting to be included in a ledger. You should use the [GET /transactions/:transaction_hash](../retrieve-a-transaction.api.mdx) endpoint to poll for the submitted transaction and check if it makes into a ledger. Note that even though the submission was successful, it can still fail to get included in the ledger. 2. `DUPLICATE`: The submission was a duplicate of a previously submitted transaction. This could happen if the client resubmitted the same transaction multiple times. 3. `ERROR`: The submission did not go through due to an error in Stellar-Core. Take a look at the attached error message for more details, modify your transaction if necessary, and resubmit. 4. `TRY_AGAIN_LATER`: This indicates that the Stellar-Core instance is currently unable to process submission of this particular transaction. Clients should wait for sometime before resubmitting the transaction. This could happen due to different reasons: - There is another transaction from same source account in memory - It has been rejected due to too low inclusion fee and has been resubmitted too soon ```js let server = sdk.Server("https://horizon-testnet.stellar.org"); let contractId = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"; let contract = new StellarSdk.Contract(contractId); // Right now, this is just the default fee for this example. const fee = StellarSdk.BASE_FEE; let transaction = new StellarSdk.TransactionBuilder(account, { fee }) .setNetworkPassphrase(StellarSdk.Networks.TESTNET) .setTimeout(30) // valid for the next 30s // Add an operation to call increment() on the contract .addOperation(contract.call("increment")) .build(); // Sign this transaction with the secret key // NOTE: signing is transaction is network specific. Test network transactions // won't work in the public network. To switch networks, use the Network object // as explained above (look for StellarSdk.Network). let sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSecretKey); transaction.sign(sourceKeypair); server.submitAsyncTransaction(transaction).then((result) => { console.log("hash:", result.hash); console.log("status:", result.tx_status); console.log("errorResultXdr:", result.error_result_xdr); }); // Add a small sleep duration before polling the transaction. time.sleep(5 * time.Second); server .transactions() .transaction(result.hash) .call() .then((txResult) => { console.log("Transaction status:", txResult); }); ``` #### Synchronous Transaction Submission Due to the blocking nature of this endpoint, things are a little different compared to the asynchronous strategy. There are 3 possible scenarios that clients can encounter: 1. The submission is successful and Horizon returns the transaction response back. This is the happy path and clients do not need to do anything but wait for Horizon's response. 2. Stellar-Core sends back an `ERROR` response from the submission. Clients should consult the attached error message and retry the submission again. 3. Timeouts: Horizon may respond with a `504` HTTP code. This response is not an error but a warning that your transaction hasn't been accepted by the network yet. There could be many possible reasons for the timeout, the most common of which is network congestion, but it could also be due to other transient issues. 1. **Polling the transaction hash**: Use the transaction hash in the timeout response and poll the [GET /transactions/:transaction_hash](../retrieve-a-transaction.api.mdx) endpoint to see if it successfully makes it into a ledger. 2. **Resubmitting the transaction**: Before attempting any resubmissions, you need to make sure your transaction has timed out based on the time bounds you specified. After your transaction has expired, you can confirm it by polling the transaction again and getting a `tx_too_late` response from Horizon. Rebuild the transaction by updating the timebounds and resubmit the transaction. :::caution Do note that resubmitting a transaction is only safe when it is unchanged - same operations, signatures, sequence number, etc... Be careful when working around an error that does require changes to the transaction. It can cause duplicate transactions, which can cause problems - double payments, incorrect trustlines, and more. If you continue to face timeouts on retries, consider using a fee-bump transaction to get into the ledger (after the initial transaction's timebound expires) or increasing the maximum fee you’re willing to pay. Read up on [Surge Pricing and Fee Strategies](../../../../../learn/fundamentals/fees-resource-limits-metering.mdx) for more details. ::: ### Example: Using Time Bounds Timebounds are optional but **highly** recommended as they put a definitive time limit on the transaction's finality - after it times out, you will know for sure whether it made it into a ledger. For example, you submit a transaction, and it enters the queue of the Stellar network, but Horizon crashes while giving you a response. Uncertain about the transaction status, you resubmit the transaction (with no changes!) until either (a) Horizon comes back up to give you a reply or (b) your time bounds are exceeded. There are only two possible results to this scenario: either the transaction makes it into the ledger (exactly once) and Horizon gives you the response, or the transaction never makes it out of the queue, and you receive the corresponding `tx_too_late` response. Example implementation: ```js let server = Horizon.Server("https://horizon-testnet.stellar.org"); function submitTransaction(tx, timeout) { if (!tx.timeBounds || tx.timeBounds.maxTime === 0) { throw new Error("Always set a reasonable timebound!"); } const expiration = parseInt(tx.timeBounds.maxTime); return server.submitTransaction(tx).catch(function (error) { if (isNonRetryErrorCase(error)) { // ...do other error handling... return; } // the tx no longer has a chance of making it into a ledger if (Date.now() >= expiration) { return new Error("The transaction timed out."); } timeout = timeout || 1; // start the (linear) back-off process return sleep(timeout).then(function () { return submitTransaction(tx, timeout + 5); }); }); } ``` We assume the existence of a sleep implementation similar to the one [here](https://stackoverflow.com/a/39914235). Be sure to integrate backoff into your retry mechanism. In our example error-handling code above, we implement a simple linear backoff, but there are [plenty of recommendations](https://backoff-utils.readthedocs.io/en/latest/strategies.html#why-are-backoff-strategies-useful) for various other strategies. Backoff is important both for maintaining performance and avoiding rate-limiting issues. ### Example: Invalid Sequence Numbers These errors typically occur when you have an outdated view of an account. This could be because multiple devices are using this account, you have concurrent submissions happening, or other reasons. The solution is relatively simple: retrieve the account details and try again with an updated sequence number. ```js // suppose `account` is an outdated `AccountResponse` object let tx = sdk.TransactionBuilder(account, ...)/* etc */.build(); server.submitTransaction(tx).catch(function (error) { if (error.response && error.status == 400 && error.extras && error.extras.result_codes.transaction == sdk.TX_BAD_SEQ) { return server.loadAccount(account.accountId()) .then(function (response) { let tx = sdk.TransactionBuilder(response, ...)/* etc */.build() return server.submitTransaction(tx); }); } // ...other error conditions... }) ``` Despite the solution’s simplicity, things can go wrong fast if you don’t understand why the error occurred. Suppose you submit transactions from multiple places in your application simultaneously, and your user spammed a _Send Payment_ button a few times in their impatience. If you send the exact same payment transaction for each tap, naturally, only one will succeed. The others will fail with an invalid sequence number (`tx_bad_seq`), and if you resubmit blindly with an updated sequence number (as we do above), these payments will also succeed, resulting in more than one payment being made when only one was intended. So **be very careful when resubmitting transactions** that have been modified to work around an error. ## Managing specific Errors Here, we will cover specific errors commonly encountered during transaction submission and direct you to the appropriate resolution. | Result | Code | Description | | --- | --- | --- | | `FAILED` | -1 | One of the operations failed (see [List of Operations](../../../../../learn/fundamentals/transactions/list-of-operations.mdx) for errors) | | `TOO_EARLY` | -2 | Ledger `closeTime` before `minTime` value in the transaction | | `TOO_LATE` | -3 | Ledger `closeTime` after `maxTime` value in the transaction | | `MISSING_OPERATION` | -4 | No operation was specified | | `BAD_SEQ` | -5 | Sequence number does not match source account | | `BAD_AUTH` | -6 | Too few valid signatures / wrong network | | `INSUFFICIENT_BALANCE` | -7 | Fee would bring account below minimum balance; see our section on [Lumens](../../../../../learn/fundamentals/lumens.mdx#minimum-balance) for more info | | `NO_ACCOUNT` | -8 | Source account not found | | `INSUFFICIENT_FEE` | -9 | Fee is too small; see our section on [Fees](../../../../../learn/fundamentals/fees-resource-limits-metering.mdx) for more info | | `BAD_AUTH_EXTRA` | -10 | Unused signatures attached to transaction | | `INTERNAL_ERROR` | -11 | An unknown error occurred | | `NOT_SUPPORTED` | -12 | The transaction type is not supported | | `FEE_BUMP_INNER_FAILED` | -13 | The inner transaction of a fee-bump transaction failed | | `BAD_SPONSORSHIP` | -14 | The sponsorship is not confirmed | ### Insufficient fees and surge pricing See the [Fees section](../../../../../learn/fundamentals/fees-resource-limits-metering.mdx) ### Rate limiting Horizon may rate limit requests and return `429 Too Many Requests` error when exceeding the rate limits. If using your own instance, you may want to either increase or disable rate limiting. If you're using a third-party Horizon instance, you may want to deploy your own to have more control over this configuration or send requests less frequently. ### Insufficient XLM balance Any transaction that would reduce an account’s balance to less than the minimum will be rejected with an `INSUFFICIENT_BALANCE` error. Likewise, lumen selling liabilities that would reduce an account’s balance to less than the minimum plus lumen selling liabilities will be rejected with an `INSUFFICIENT_BALANCE` error. For more on minimum balances, see our [Lumens section](../../../../../learn/fundamentals/lumens.mdx#minimum-balance). --- ## HTTP Status Codes Some errors only occur at the Horizon level and are not thrown in Stellar Core. These are usually issues with how the transaction was formed or a conflict between the transaction’s composition and how the Horizon server is setup. Stellar uses conventional HTTP response codes to indicate the success or failure of an API request. In general: - Codes in the 2xx range indicate success. - Codes in the 4xx range indicate an error that failed given the information provided - Codes in the 5xx range indicate an error with the Horizon server. There are two types of Status Codes: [Standard Status Codes](./standard.mdx) and [Horizon-Specific Status Codes](./horizon-specific/README.mdx). | | | | --- | --- | | [Standard Status Codes](./standard.mdx) | Generic HTTP responses. | | [Horizon-Specific Status Codes](./horizon-specific/README.mdx) | Errors that are unique to Horizon. | --- ## Horizon-Specific Status Codes These responses are specific to how a Horizon server is set up and how transactions should be formed. | | | | ---------------------------------------------------- | --- | | [Transaction Failed](./transaction-failed.mdx) | | | [Transaction Malformed](./transaction-malformed.mdx) | | | [Before History](./before-history.mdx) | | | [Stale History](./stale-history.mdx) | | | [Timeout](./timeout.mdx) | | --- ## Before History A Horizon server may be configured to only keep a portion of the stellar network’s history stored within its database. The `before_history` error returns a [`410` error code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410) and occurs a client requests a piece of information (such as a page of transactions or a single operation) that the server can positively identify as falling outside the range of recorded history. ```json { "type": "https://stellar.org/horizon-errors/before_history", "title": "Data Requested Is Before Recorded History", "status": 410, "detail": "This horizon instance is configured to only track a portion of the Stellar network's latest history. This request is asking for results prior to the recorded history known to this horizon instance." } ``` --- ## Stale History A Horizon server may be configured to reject historical requests when the history is known to be further out of date than the configured threshold. In such cases, the `stale_history` error occurs and returns a [`503` error code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503). To resolve this error (provided you are the Horizon instance’s operator), please ensure that the ingestion system is running correctly and importing new ledgers. ```json { "type": "https://stellar.org/horizon-errors/stale_history", "title": "Historical DB Is Too Stale", "status": 503, "detail": "This horizon instance is configured to reject client requests when it can determine that the history database is lagging too far behind the connected instance of stellar-core. If you operate this server, please ensure that the ingestion system is properly running." } ``` --- ## Timeout The `timeout` error returns a [`504` error code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) and occurs when either: - Horizon has not received a confirmation from the Stellar Core server that the transaction you are trying to submit to the network was included in a ledger in a timely manner, or - Horizon has not sent a response to a reverse-proxy before a specified amount of time has elapsed. The former case may happen because there was no room for your transaction for 3 consecutive ledgers. This is because Stellar Core removes each submitted transaction from a queue. To solve this you can: - Keep resubmitting the same transaction (with the same sequence number) and wait until it finally is added to a new ledger, or - Increase the fee in order to prioritize the transaction. ```json { "type": "https://stellar.org/horizon-errors/timeout", "title": "Timeout", "status": 504, "detail": "Your request timed out before completing. Please try your request again. If you are submitting a transaction make sure you are sending exactly the same transaction (with the same sequence number)." } ``` --- ## Transaction Failed The `transaction_failed` error returns a [`400` error code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400) and occurs when a client submits a transaction that was well-formed but was not included in the ledger due to some other failure. For example, a transaction may fail if: 1. The source account for transaction cannot pay the minimum fee. 2. The sequence number is incorrect. 3. One of the contained operations has failed, such as a payment operation that overdraws on the paying account. In almost every case, this error indicates that the transaction submitted in the initial request will never succeed. There is one exception: a transaction that fails with the `tx_bad_seq` result code (as expressed in the `result_code` field of the error) may become valid in the future if the sequence number it used was too high. ```json { "type": "https://stellar.org/horizon-errors/transaction_failed", "title": "Transaction Failed", "status": 400, "detail": "The transaction failed when submitted to the Stellar network. The `extras.result_codes` field on this response contains further details. Descriptions of each code can be found at: https://stellar.org/developers/guides/concepts/list-of-operations.html", "extras": { "envelope_xdr": "AAAAAgAAAADdfhHDs4Vaug6p8Oxb1QRjNRdJt3pYKKBVhFHrEgd9QAAAAAoAEi4YAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAESB31AAAAAQFhc/liVXbLk3NtB2BtweFJ064JdDIfrTSrqKMhb1oIRK+0PSyvjzZTkRCJmQY3bHNXYNuepa2TF7aBdibrb1gI=", "result_codes": { "transaction": "tx_insufficient_fee" }, "result_xdr": "AAAAAAAAAAr////3AAAAAA==" } } ``` --- ## Transaction Malformed The `transaction_malformed` error returns a [`400` error code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400) and occurs when a client submits a malformed transaction. There are many ways in which a transaction could be malformed, including: 1. You submitted an empty string. 2. Your base64-encoded string is invalid. 3. Your [XDR](../../../structure/xdr.mdx) structure is invalid. 4. You have leftover bytes in your [XDR](../../../structure/xdr.mdx) structure. ```json { "type": "https://stellar.org/horizon-errors/transaction_malformed", "title": "Transaction Malformed", "status": 400, "detail": "Horizon could not decode the transaction envelope in this request. A transaction should be an XDR TransactionEnvelope struct encoded using base64. The envelope read from this request is echoed in the `extras.envelope_xdr` field of this response for your convenience.", "extras": { "envelope_xdr": "BBBBBPORy3CoX6ox2ilbeiVjBA5WlpCSZRcjZ7VE9Wf4QVk7AAAAZAAAQz0AAAACAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAA85HLcKhfqjHaKVt6JWMEDlaWkJJlFyNntUT1Z/hBWTsAAAAAAAAAAAL68IAAAAAAAAAAARN17BEAAABAA9Ad7OKc7y60NT/JuobaHOfmuq8KbZqcV6G/es94u9yT84fi0aI7tJsFMOyy8cZ4meY3Nn908OU+KfRWV40UCw==" } } ``` --- ## Standard Status Codes These responses are Internet protocol codes that describe basic issues with a submitted transaction. - STATUS CODE - VALUE - DESCRIPTION - OK - 200 - The request has succeeded. - Bad Request - 400 - The request as invalid in some way. - Not Found - 404 - The requested resource does not exist. - Not Implemented - 404 - The request does not have an acceptable response content-type. - Not Acceptable - 406 - The request does not have an acceptable response content-type. - Internal Server Error - 500 - Something went wrong on the Horizon server’s end. ```json { "type": "https://stellar.org/horizon-errors/bad_request", "title": "Bad Request", "status": 400, "detail": "The request you sent was invalid in some way", "extras": { "invalid_field": "limit", "reason": "unparseable value" } } ``` --- ## Error Response When any error occurs, Horizon responds with a JSON document with the below attributes. - ATTRIBUTE - DATA TYPE - DESCRIPTION - type - URL - The type of Status Code returned. - title - string - A short title describing the Status Code, which can be used to look up more information about a error. - status - number - A short title describing the Status Code, which can be used to look up more information about a error. - detail - string - A short title describing the Status Code, which can be used to look up more information about a error. - extras - map - If the Status Code is `Transaction Failed`, this extras field displays the Result Code returned by Stellar Core describing why the transaction failed. - envelope_xdr - string - A base64-encoded representation of the TransactionEnvelope XDR whose failure triggered this response. - result_xdr - string - A base64-encoded representation of the TransactionResult XDR returned by stellar-core when submitting this transaction. - result_codes.transaction - string - The transaction Result Code returned by Stellar Core, which can be used to look up more information about an error in the docs. - result_codes.operations - array - An array of operation Result Codes returned by Stellar Core, which can be used to look up more information about an error in the docs. ```json { "type": "https://stellar.org/horizon-errors/transaction_failed", "title": "Transaction Failed", "status": 400, "detail": "The transaction failed when submitted to the Stellar network. The `extras.result_codes` field on this response contains further details. Descriptions of each code can be found at: https://stellar.org/developers/learn/concepts/list-of-operations.html", "extras": { "envelope_xdr": "AAAAANPRjCD1iCti3hovsrrz6aSAjmp263grVr6+mI3SQSkcAAAAZAAPRLgAAAADAAAAAAAAAAAAAAABAAAAAQAAAACuSL9OciKkFztj4d3zuadl20HHObu+7qJenBxHPrMayQAAAAUAAAABAAAAANPRjCD1iCti3hovsrrz6aSAjmp263grVr6+mI3SQSkcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtJBKRwAAABA1N0iqDAgqS6+3RIZGoNB9OXrY3wd/nLruXYi+eiTt4jn94fLVLwAw6jJCaK+qxStwO7c4kP6u5k0RPbuYC55CT6zGskAAABAiUGCNCS4pGlfcRmi82kbralzcFlTQAFzLyfUrYGn3RtQ4p/7TUwAqIanVoWGfEqzIJo64ZT+mYtJ72BfI+FiDg==", "result_codes": { "transaction": "tx_failed", "operations": ["op_no_source_account"] }, "result_xdr": "AAAAAAAAAGT/////AAAAAf////4AAAAA" } } ``` --- ## Result Codes Result Codes describe why a transaction or operation failed in Stellar Core and are communicated in the “extras” field of a Horizon response when the “Transaction Failed” Status Code is returned. In the “extras” field, the errors returned are referred to as “Result Codes” and are Horizon’s abstraction of “Stellar Protocol Codes”, which are more specific codes available in the XDR. Result Codes are Horizon’s way of normalizing Stellar Protocol Codes. There are three types of Result Codes: [Transaction Result Codes](./transactions.mdx), [Operation Result Codes](./operations.mdx), and [Operation-Specific Result Codes](./operation-specific/README.mdx). | | | | --- | --- | | [Transaction Result Codes](./transactions.mdx) | Generic errors about transaction failures. | | [Operation Result Codes](./operations.mdx) | Generic errors about operation failures. | | [Operation-Specific Result Codes](./operation-specific/README.mdx) | Errors specific to each operation type. | --- ## Operation-Specific Result Codes These are Result Codes that communicate success (200) or failure (400) responses that are specific to each operation type. Each of Stellar's operations have unique causes of failure, and these result codes will help you diagnose and address the origins of issues. | | | | ---------------------------------------------------------------- | --- | | [Create Account](./create-account.mdx) | | | [Payment](./payment.mdx) | | | [Path Payment Strict Receive](./path-payment-strict-receive.mdx) | | | [Path Payment Strict Send](./path-payment-strict-send.mdx) | | | [Manage Sell Offer](./manage-sell-offer.mdx) | | | [Manage Buy Offer](./manage-buy-offer.mdx) | | | [Create Passive Sell Offer](./create-passive-sell-offer.mdx) | | | [Set Options](./set-options.mdx) | | | [Change Trust](./change-trust.mdx) | | | [Allow Trust](./allow-trust.mdx) | | | [Account Merge](./account-merge.mdx) | | | [Manage Data](./manage-data.mdx) | | | [Bump Sequence](./bump-sequence.mdx) | | For operations that aren't in this list, you can usually refer to their corresponding entries in the [List of Operations](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx) which will enumerate the failure cases and corresponding result codes. --- ## Account Merge Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Account Merge` operation. Learn more about the [`Account Merge` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#account-merge). - OpSuccess - ACCOUNT_MERGE_SUCCESS - Account succesfully merged. - OpMalformed - ACCOUNT_MERGE_MALFORMED - The operation is malformed because the source account cannot merge with itself. The destination must be a different account. - op_no_account - ACCOUNT_MERGE_NO_ACCOUNT - The destination account does not exist. - op_immutable_set - ACCOUNT_MERGE_IMMUTABLE_SET - The source account has AUTH_IMMUTABLE flag set. - op_has_sub_entries - ACCOUNT_MERGE_HAS_SUB_ENTRIES - The source account still has non-signer subentries (trustlines, offers, or data entries). Signers are not a blocker — they are removed automatically as part of the merge. - op_seq_num_too_far - ACCOUNT_MERGE_SEQNUM_TOO_FAR - Source account sequence number is too high. - op_dest_full - ACCOUNT_MERGE_DEST_FULL - The destination account cannot receive the balance of the source account and still satisfy its lumen buying liabilities. - op_is_sponsor - ACCOUNT_MERGE_IS_SPONSOR - The source account cannot be merged because it is sponsoring reserves. This covers two cases: it is already sponsoring reserves for other accounts (`numSponsoring > 0`), which must be revoked first; or it has an open is-sponsoring-future-reserves relationship earlier in the same transaction, which must be ended with `EndSponsoringFutureReserves` (there may be no created reserve to revoke in this case). Merely being sponsored by another account is not a blocker. --- ## Allow Trust Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Allow Trust` operation. Learn more about the [`Allow Trust` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#allow-trust). - OpSuccess - ALLOW_TRUST_SUCCESS - Trust trust operation was successful. - OpMalformed - ALLOW_TRUST_MALFORMED - The asset specified in type is invalid. In addition, this error happens when the native asset is specified. - op_no_trustline - ALLOW_TRUST_NO_TRUST_LINE - The trustor does not have a trustline with the issuer performing this operation. - op_not_required - ALLOW_TRUST_TRUST_NOT_REQUIRED - The source account (issuer performing this operation) does not require trust. In other words, it does not have to have the flag AUTH_REQUIRED_FLAG set. - op_cant_revoke - ALLOW_TRUST_CANT_REVOKE - The source account is trying to revoke the trustline of the trustor, but it cannot do so. - op_self_not_allowed - ALLOW_TRUST_SELF_NOT_ALLOWED - The source account attempted to allow a trustline for itself, which is not allowed because an account cannot create a trustline with itself. - op_low_reserve - ALLOW_TRUST_LOW_RESERVE - Claimable balances can't be created on revoke due to low reserves --- ## Bump Sequence Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Bump Sequence` operation. Learn more about the [`Bump Sequence` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#bump-sequence). - OpSuccess - BUMP_SEQUENCE_SUCCESS - Sequence number has been bumped. - op_bad_seq - BUMP_SEQUENCE_BAD_SEQ - The specified bumpTo sequence number is not a valid sequence number. It must be between 0 and INT64_MAX (9223372036854775807 or 0x7fffffffffffffff). --- ## Change Trust Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Change Trust` operation. Learn more about the [`Change Trust` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#change-trust) - OpSuccess - CHANGE_TRUST_SUCCESS - Trust was successfully changed - OpMalformed - CHANGE_TRUST_MALFORMED - The input to this operation is invalid. - OpNoIssuer - CHANGE_TRUST_NO_ISSUER - The issuer of the asset cannot be found. - op_invalid_limit - CHANGE_TRUST_INVALID_LIMIT - The limit is not sufficient to hold the current balance of the trustline and still satisfy its buying liabilities. - OpLowReserve - CHANGE_TRUST_LOW_RESERVE - This account does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every new trustline added to an account, the minimum reserve of XLM that account must hold increases. - op_self_not_allowed - CHANGE_TRUST_SELF_NOT_ALLOWED - The source account attempted to create a trustline for itself, which is not allowed. --- ## Create Account Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Create Account` operation, which often fails because the new account does not meet the minimum reserve. Learn more about the [`Create Account` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#create-account). - RESULT CODE - STELLAR PROTOCOL CODE - DESCRIPTION - OpSuccess - CREATE_ACCOUNT_SUCCESS - The inner object result is valid, the operation was a success, and an acount was created. - OpMalformed - CREATE_ACCOUNT_MALFORMED - The destination was invalid. - OpUnderfunded - CREATE_ACCOUNT_UNDERFUNDED - The source account performing the command does not have enough funds to give the destination account the necessary mininum reserve and still maintain its own minimum reserve. - OpLowReserve - CREATE_ACCOUNT_LOW_RESERVE - The operation would create an account below the minimum reserve. - OpAlreadyExists - CREATE_ACCOUNT_ALREADY_EXIST - The destination account already exists. --- ## Create Passive Sell Offer Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Create Passive Sell Offer` operation. Learn more about the [`Create Passive Sell Offer` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#create-passive-sell-offer). - OpSuccess - CREATE_PASSIVE_SELL_OFFER_SUCCESS - The passive sell offer was successfully placed. - OpMalformed - MANAGE_SELL_OFFER_MALFORMED - The input is incorrect and would result in an invalid offer. - op_sell_no_trust - MANAGE_SELL_OFFER_SELL_NO_TRUST - The account creating the offer does not have a trustline for the asset it is selling. - op_buy_no_trust - MANAGE_SELL_OFFER_BUY_NO_TRUST - The account creating the offer does not have a trustline for the asset it is buying. - sell_not_authorized - MANAGE_SELL_OFFER_SELL_NOT_AUTHORIZED - The account creating the offer is not authorized to sell this asset. - buy_not_authorized - MANAGE_SELL_OFFER_BUY_NOT_AUTHORIZED - The account creating the offer is not authorized to buy this asset. - OpLineFull - MANAGE_SELL_OFFER_LINE_FULL - The account creating the offer does not have sufficient limits to receive buying and still satisfy its buying liabilities. - OpUnderfunded - MANAGE_SELL_OFFER_UNDERFUNDED - The account creating the offer does not have sufficient limits to send selling and still satisfy its selling liabilities. Note that if selling XLM then the account must additionally maintain its minimum XLM reserve, which is calculated assuming this offer will not completely execute immediately. - op_cross_self - MANAGE_SELL_OFFER_CROSS_SELF - The account has opposite offer of equal or lesser price active, so the account creating this offer would immediately cross itself. - op_sell_no_issuer - MANAGE_SELL_OFFER_SELL_NO_ISSUER - The issuer of selling asset does not exist. - buy_no_issuer - MANAGE_SELL_OFFER_BUY_NO_ISSUER - The issuer of buying asset does not exist. - op_offer_not_found - MANAGE_SELL_OFFER_NOT_FOUND - An offer with that offerID cannot be found. - OpLowReserve - MANAGE_SELL_OFFER_LOW_RESERVE - The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. --- ## Manage Buy Offer Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Manage Buy Offer` operation. Learn more about the [`Manage Buy Offer` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#manage-buy-offer). - OpSuccess - MANAGE_BUY_OFFER_SUCCESS - The offer was successfully placed. - OpMalformed - MANAGE_BUY_OFFER_MALFORMED - The input is incorrect and would result in an invalid offer. - op_sell_no_trust - MANAGE_BUY_OFFER_SELL_NO_TRUST - The account creating the offer does not have a trustline for the asset it is selling. - op_buy_no_trust - MANAGE_BUY_OFFER_BUY_NO_TRUST - The account creating the offer does not have a trustline for the asset it is buying. - sell_not_authorized - MANAGE_BUY_OFFER_SELL_NOT_AUTHORIZED - The account creating the offer is not authorized to sell this asset. - buy_not_authorized - MANAGE_BUY_OFFER_BUY_NOT_AUTHORIZED - The account creating the offer is not authorized to buy this asset. - OpLineFull - MANAGE_BUY_OFFER_LINE_FULL - The account creating the offer does not have sufficient limits to receive buying and still satisfy its buying liabilities. - OpUnderfunded - MANAGE_BUY_OFFER_UNDERFUNDED - The account creating the offer does not have sufficient limits to send selling and still satisfy its selling liabilities. Note that if selling XLM then the account must additionally maintain its minimum XLM reserve, which is calculated assuming this offer will not completely execute immediately. - op_cross_self - MANAGE_BUY_OFFER_CROSS_SELF - The account has opposite offer of equal or lesser price active, so the account creating this offer would immediately cross itself. - op_sell_no_issuer - MANAGE_BUY_OFFER_SELL_NO_ISSUER - The issuer of selling asset does not exist. - buy_no_issuer - MANAGE_BUY_OFFER_BUY_NO_ISSUER - The issuer of buying asset does not exist. - op_offer_not_found - MANAGE_BUY_OFFER_NOT_FOUND - An offer with that offerID cannot be found. - OpLowReserve - MANAGE_BUY_OFFER_LOW_RESERVE - The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. --- ## Manage Data Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Manage Data` operation. Learn more about the [`Manage Data` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#manage-data). - OpSuccess - MANAGE_DATA_SUCCESS - Manage data operation has executed successfully. - op_not_supported_yet - MANAGE_DATA_NOT_SUPPORTED_YET - The network hasn’t moved to this protocol change yet. This failure means the network doesn’t support this feature yet. - op_data_name_not_found - MANAGE_DATA_NAME_NOT_FOUND - Trying to remove a Data Entry that isn’t there. This will happen if Name is set (and Value isn’t) but the Account doesn’t have a DataEntry with that Name. - op_low_reserve - MANAGE_DATA_LOW_RESERVE - This account does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every new DataEntry added to an account, the minimum reserve of XLM that account must hold increases. - op_data_invalid_name - MANAGE_DATA_INVALID_NAME - Name not a valid string. --- ## Manage Sell Offer Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Manage Sell Offer` operation. Learn more about the [`Manage Sell Offer` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#manage-sell-offer). - OpSuccess - MANAGE_SELL_OFFER_SUCCESS - The offer was successfully placed. - OpMalformed - MANAGE_SELL_OFFER_MALFORMED - The input is incorrect and would result in an invalid offer. - op_sell_no_trust - MANAGE_SELL_OFFER_SELL_NO_TRUST - The account creating the offer does not have a trustline for the asset it is selling. - op_buy_no_trust - MANAGE_SELL_OFFER_BUY_NO_TRUST - The account creating the offer does not have a trustline for the asset it is buying. - sell_not_authorized - MANAGE_SELL_OFFER_SELL_NOT_AUTHORIZED - The account creating the offer is not authorized to sell this asset. - buy_not_authorized - MANAGE_SELL_OFFER_BUY_NOT_AUTHORIZED - The account creating the offer is not authorized to buy this asset. - OpLineFull - MANAGE_SELL_OFFER_LINE_FULL - The account creating the offer does not have sufficient limits to receive buying and still satisfy its buying liabilities. - OpUnderfunded - MANAGE_SELL_OFFER_UNDERFUNDED - The account creating the offer does not have sufficient limits to send selling and still satisfy its selling liabilities. Note that if selling XLM then the account must additionally maintain its minimum XLM reserve, which is calculated assuming this offer will not completely execute immediately. - op_cross_self - MANAGE_SELL_OFFER_CROSS_SELF - The account has opposite offer of equal or lesser price active, so the account creating this offer would immediately cross itself. - op_sell_no_issuer - MANAGE_SELL_OFFER_SELL_NO_ISSUER - The issuer of selling asset does not exist. - buy_no_issuer - MANAGE_SELL_OFFER_BUY_NO_ISSUER - The issuer of buying asset does not exist. - op_offer_not_found - MANAGE_SELL_OFFER_NOT_FOUND - An offer with that offerID cannot be found. - OpLowReserve - MANAGE_SELL_OFFER_LOW_RESERVE - The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. --- ## Path Payment Strict Receive Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Path Payment Strict Receive` operation. Learn more about the [`Path Payment Strict Receive` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#path-payment-strict-receive). - RESULT CODE - STELLAR PROTOCOL CODE - DESCRIPTION - OpSuccess - PATH_PAYMENT_STRICT_RECEIVE_SUCCESS - The payment was successfully completed. - OpMalformed - PATH_PAYMENT_STRICT_RECEIVE_MALFORMED - The input for this path payment is invalid. - OpUnderfunded - PATH_PAYMENT_STRICT_RECEIVE_UNDERFUNDED - The source account (sender) does not have enough lumens to send the payment amount while maintaining its own minimum reserve. - OpSrcNoTrust - PATH_PAYMENT_STRICT_RECEIVE_SRC_NO_TRUST - The source account is missing the appropriate trustline. - OpSrcNotAuthorized - PATH_PAYMENT_STRICT_RECEIVE_SRC_NOT_AUTHORIZED - The source account is not authorized to send this asset. - OpNoDestination - PATH_PAYMENT_STRICT_RECEIVE_NO_DESTINATION - The destination account does not exist. - OpNoTrust - PATH_PAYMENT_STRICT_RECEIVE_NO_TRUST - The destination account does not have a trustline for the asset being sent. - OpNotAuthorized - PATH_PAYMENT_STRICT_RECEIVE_NOT_AUTHORIZED - The destination account is not authorized to hold this asset. - OpLineFull - PATH_PAYMENT_STRICT_RECEIVE_LINE_FULL - The destination account (receiver) does not have sufficient limits to receive amount and still satisfy its buying liabilities. - OpNoIssuer - PATH_PAYMENT_STRICT_RECEIVE_NO_ISSUER - The issuer of one of the assets is missing. - OpTooFewOffers - PATH_PAYMENT_STRICT_RECEIVE_TOO_FEW_OFFERS - There is no path of offers connecting the send asset and destination asset. Stellar only considers paths of length 5 or shorter. - OpCrossSelf - PATH_PAYMENT_STRICT_RECEIVE_OFFER_CROSS_SELF - This path payment would cross one of its own offers. - OpOverSourceMax - PATH_PAYMENT_STRICT_RECEIVE_OVER_SENDMAX - The paths that could send destination amount of destination asset would exceed send max. --- ## Path Payment Strict Send Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Path Payment Strict Send` operation. Learn more about the [`Path Payment Strict Send` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#path-payment-strict-send). - RESULT CODE - STELLAR PROTOCOL CODE - DESCRIPTION - OpSuccess - PATH_PAYMENT_STRICT_SEND_SUCCESS - The payment was successfully completed. - OpMalformed - PATH_PAYMENT_STRICT_SEND_MALFORMED - The input for this path payment is invalid. - OpUnderfunded - PATH_PAYMENT_STRICT_SEND_UNDERFUNDED - The source account (sender) does not have enough lumens to send the payment amount while maintaining its own minimum reserve. - OpSrcNoTrust - PATH_PAYMENT_STRICT_SEND_SRC_NO_TRUST - The source account is missing the appropriate trustline. - OpSrcNotAuthorized - PATH_PAYMENT_STRICT_SEND_SRC_NOT_AUTHORIZED - The source account is not authorized to send this asset. - OpNoDestination - PATH_PAYMENT_STRICT_SEND_NO_DESTINATION - The destination account does not exist. - OpNoTrust - PATH_PAYMENT_STRICT_SEND_NO_TRUST - The destination account does not have a trustline for the asset being sent. - OpNotAuthorized - PATH_PAYMENT_STRICT_SEND_NOT_AUTHORIZED - The destination account is not authorized to hold this asset. - OpLineFull - PATH_PAYMENT_STRICT_SEND_LINE_FULL - The destination account (receiver) does not have sufficient limits to receive amount and still satisfy its buying liabilities. - OpNoIssuer - PATH_PAYMENT_STRICT_SEND_NO_ISSUER - The issuer of one of the assets is missing. - OpTooFewOffers - PATH_PAYMENT_STRICT_SEND_TOO_FEW_OFFERS - There is no path of offers connecting the send asset and destination asset. Stellar only considers paths of length 5 or shorter. - OpCrossSelf - PATH_PAYMENT_STRICT_SEND_OFFER_CROSS_SELF - This path payment would cross one of its own offers. - OpUnderDestMin - PATH_PAYMENT_STRICT_SEND_UNDER_DESTMIN - The paths that could send destination amount of destination asset would fall short of destination min. --- ## Payment Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Payment` operation, which often fails because the receiving account does not trust the issuer of the asset being sent. Learn more about the [`Payment` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#payment). - RESULT CODE - STELLAR PROTOCOL CODE - DESCRIPTION - OpSuccess - PAYMENT_SUCCESS - The payment was successfully completed. - OpMalformed - PAYMENT_MALFORMED - The input to the payment is invalid. - OpUnderfunded - PAYMENT_UNDERFUNDED - The source account (sender) does not have enough lumens to send the payment amount while maintaining its own minimum reserve. - OpSrcNoTrust - PAYMENT_SRC_NO_TRUST - The source account does not have a trustline for the asset it is tring to send. - OpSrcNotAuthorized - PAYMENT_SRC_NOT_AUTHORIZED - The source account is not authorized to send this asset. - OpNoDestination - PAYMENT_NO_DESTINATION - The destination account does not exist. - OpNoTrust - PAYMENT_NO_TRUST - The destination account does not have a trustline for the asset being sent. - OpNotAuthorized - PAYMENT_NOT_AUTHORIZED - The destination account is not authorized to hold this asset. - OpLineFull - PAYMENT_LINE_FULL - The destination account (receiver) does not have sufficient limits to receive amount and still satisfy its buying liabilities. - OpNoIssuer - PAYMENT_NO_ISSUER - The issuer of the asset does not exist. --- ## Set Options Result Codes These are result codes that communicate success (200) or failure (400) specific to the `Set Options` operation. Learn more about the [`Set Options` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#set-options). - OpSuccess - SET_OPTIONS_SUCCESS - Options successfully set. - OpLowReserve - SET_OPTIONS_LOW_RESERVE - This account does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every new signer added to an account, the minimum reserve of XLM that account must hold increases. - op_too_many_signers - SET_OPTIONS_TOO_MANY_SIGNERS - 20 is the maximum number of signers an account can have, and adding another signer would exceed that. - op_bad_flags - SET_OPTIONS_BAD_FLAGS - The flags set and/or cleared are invalid by themselves or in combination. - op_invalid_inflation - SET_OPTIONS_INVALID_INFLATION - The destination account set in the inflation field does not exist. - op_cant_change - SET_OPTIONS_CANT_CHANGE - This account can no longer change the option it wants to change. - op_unknown_flag - SET_OPTIONS_UNKNOWN_FLAG - The account is trying to set a flag that is unknown. - op_threshold_out_of_range - SET_OPTIONS_THRESHOLD_OUT_OF_RANGE - The value for a key weight or threshold is invalid. - op_bad_signer - SET_OPTIONS_BAD_SIGNER - Any additional signers added to the account cannot be the master key. - op_invalid_home_domain - SET_OPTIONS_INVALID_HOME_DOMAIN - Home domain is malformed. --- ## Operation Result Codes These are Result Codes that communicate success (200) or failure (400) at the operation level: no source account, too many subentries, etc. - RESULT CODE - STELLAR PROTOCOL CODE - DESCRIPTION - op_inner - opINNER - The inner object result is valid and the operation was a success. - op_bad_auth - opBAD_AUTH - There are too few valid signatures, or the transaction was submitted to the wrong network. - op_no_source_account - opNO_ACCOUNT - The source account was not found. - op_not_supported - opNOT_SUPPORTED - The operation is not supported at this time. - op_too_many_subentries - opTOO_MANY_SUBENTRIES - Max number of subentries (1000) already reached - op_exceeded_work_limit - opEXCEEDED_WORK_LIMIT - Operation did too much work ```json { "type": "https://stellar.org/horizon-errors/transaction_failed", "title": "Transaction Failed", "status": 400, "detail": "The transaction failed when submitted to the Stellar network. The `extras.result_codes` field on this response contains further details. Descriptions of each code can be found at: https://stellar.org/developers/learn/concepts/list-of-operations.html", "extras": { "envelope_xdr": "AAAAANPRjCD1iCti3hovsrrz6aSAjmp263grVr6+mI3SQSkcAAAAZAAPRLgAAAADAAAAAAAAAAAAAAABAAAAAQAAAACuSL9OciKkFztj4d3zuadl20HHObu+7qJenBxHPrMayQAAAAUAAAABAAAAANPRjCD1iCti3hovsrrz6aSAjmp263grVr6+mI3SQSkcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtJBKRwAAABA1N0iqDAgqS6+3RIZGoNB9OXrY3wd/nLruXYi+eiTt4jn94fLVLwAw6jJCaK+qxStwO7c4kP6u5k0RPbuYC55CT6zGskAAABAiUGCNCS4pGlfcRmi82kbralzcFlTQAFzLyfUrYGn3RtQ4p/7TUwAqIanVoWGfEqzIJo64ZT+mYtJ72BfI+FiDg==", "result_codes": { "transaction": "tx_failed", "operations": ["op_no_source_account"] }, "result_xdr": "AAAAAAAAAGT/////AAAAAf////4AAAAA" } } ``` --- ## Transaction Result Codes These are Result Codes that communicate success (200) or failure (400) at the transaction level: bad sequence numbers, insufficient balances, insufficient fees, etc. - RESULT CODE - STELLAR PROTOCOL CODE - DESCRIPTION - tx_success - txSUCCESS - The transaction succeeded. - tx_failed - txFAILED - One of the operations failed (none were applied). - tx_too_early - txTOO_EARLY - The ledger closeTime was before the minTime. - tx_too_late - txTOO_LATE - The ledger closeTime was after the maxTime. - tx_missing_operation - txMISSING_OPERATION - No operation was specified - tx_bad_seq - txBAD_SEQ - sequence number does not match source account - tx_bad_auth - txBAD_AUTH - too few valid signatures / wrong network - tx_insufficient_balance - txINSUFFICIENT_BALANCE - fee would bring account below reserve - tx_no_source_account - txNO_ACCOUNT - source account not found - tx_insufficient_fee - txINSUFFICIENT_FEE - fee is too small - tx_bad_auth_extra - txBAD_AUTH_EXTRA - unused signatures attached to transaction - tx_internal_error - txINTERNAL_ERROR - an unknown error occured ```json { "type": "https://stellar.org/horizon-errors/transaction_failed", "title": "Transaction Failed", "status": 400, "detail": "The transaction failed when submitted to the Stellar network. The `extras.result_codes` field on this response contains further details. Descriptions of each code can be found at: https://stellar.org/developers/learn/concepts/list-of-operations.html", "extras": { "envelope_xdr": "AAAAANPRjCD1iCti3hovsrrz6aSAjmp263grVr6+mI3SQSkcAAAAZAAPRLgAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAArki/TnIipBc7Y+Hd87mnZdtBxzm7vu6iXpwcRz6zGskAAAAAAAAAAAAHoSAAAAAAAAAAAdJBKRwAAABANWeKuRYFmBm1lrMQqMvhbSouwL270SnxcTtv1XI4Y+uVe4yw4Jq7/43EoxwLbRh/pC3V4WfOZRzDqwsTyEztAA==", "result_codes": { "transaction": "tx_bad_seq" }, "result_xdr": "AAAAAAAAAAD////7AAAAAA==" } } ``` --- ## List All Offers This endpoint lists all currently open offers and can be used in streaming mode. Streaming mode allows you to listen for new offers as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known offer unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream offers created since your request time. When filtering by buying or selling arguments, you must use a combination of selling_asset_type, selling_asset_issuer, and selling_asset_code for the selling asset, or a combination of buying_asset_type, buying_asset_issuer, and buying_asset_code for the buying asset. Request --- ## List All Trades This endpoint lists all trades and can be used in streaming mode. Streaming mode allows you to listen for new trades as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known trade unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream trades created since your request time. When filtering for a specific orderbook, you must use use all six of these arguments: base_asset_type, base_asset_issuer, base_asset_code, counter_asset_type, counter_asset_issuer, and counter_asset_code. If the base or counter asset is XLM, you only need to indicate the asset type as native and do not need to designate the code or the issuer. Request --- ## Retrieve an Account's Data This endpoint represents a single data for a given account. Request --- ## Retrieve an Account's Effects This endpoint returns the effects of a specific account and can be used in streaming mode. Streaming mode allows you to listen for new effects for this account as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known effect unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream effects created since your request time. Request --- ## Retrieve an Offer The single offer endpoint provides information on a specific [offer](https://developers.stellar.org/docs/learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools#orders). Request --- ## Retrieve an Account's Offers This endpoint represents all offers a given account has currently open and can be used in streaming mode. Streaming mode allows you to listen for new offers for this account as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known offer unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream offers created since your request time. Request --- ## Retrieve an Account's Operations This endpoint represents successful operations for a given account and can be used in streaming mode. Streaming mode allows you to listen for new operations for this account as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known operation unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream operations created since your request time. Request --- ## Retrieve an Account's Payments This endpoint represents successful payments for a given account and can be used in streaming mode. Streaming mode allows you to listen for new payments for this account as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known payment unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream payments created since your request time. Request --- ## Retrieve an Account's Trades This endpoint represents all trades for a given account and can be used in streaming mode. Streaming mode allows you to listen for trades for this account as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known trade unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream trades created since your request time. Request --- ## Retrieve an Offer's Trades This endpoint represents all trades for a given offer and can be used in streaming mode. Streaming mode allows you to listen for trades for this offer as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known trade unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream trades created since your request time. Request --- ## Retrieve an Account's Transactions This endpoint represents successful transactions for a given account and can be used in streaming mode. Streaming mode allows you to listen for new transactions for this account as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known transaction unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream transactions created since your request time. Request --- ## List all Accounts This endpoint lists accounts by one of four filters : signer, asset, liquidity pool or sponsor. Request --- ## List all Assets This endpoint lists all [assets](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/assets). Request --- ## List All Claimable Balances This endpoint lists all available [claimable balances](https://developers.stellar.org/docs/build/guides/transactions/claimable-balances). Request --- ## List All Effects This endpoint lists all effects and can be used in streaming mode. Streaming mode allows you to listen for new effects as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known effect unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream effects created since your request time. Request --- ## List All Ledgers This endpoint lists all ledgers and can be used in streaming mode. Streaming mode allows you to listen for new ledgers as they close. If called in streaming mode, Horizon will start at the earliest known ledger unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream ledgers since your request time. Request --- ## List All Operations This endpoint lists all Successful operations and can be used in streaming mode. Streaming mode allows you to listen for new operations as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known operation unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream operations created since your request time. Request --- ## List All Payments This endpoint lists all Successful payment-related operations and can be used in streaming mode. Streaming mode allows you to listen for new payments as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known payment unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream payments created since your request time. Operations that can be returned by this endpoint include: create_account, payment, path_payment_strict_recieve, path_payment_strict_send, and account_merge . Request --- ## List All Transactions This endpoint lists all Successful transactions and can be used in streaming mode. Streaming mode allows you to listen for new transactions as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known transaction unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream transactions created since your request time. Request --- ## List Liquidity Pools This endpoint lists all available liquidity pools. Request --- ## List Strict Receive Payment Paths The [strict receive payment path](https://developers.stellar.org/docs/data/apis/horizon/api-reference/resources/operations/object/path-payment-strict-receive) endpoint lists the paths a payment can take based on the amount of an asset you want the recipient to receive. The destination asset amount stays constant, and the type and amount of an asset sent varies based on offers in the order books. For this search, Horizon loads a list of assets available to the sender (based on `source_account` or `source_assets`) and displays the possible paths from the different source assets to the destination asset. Only paths that satisfy the `destination_amount` are returned. Request --- ## List Strict Send Payment Paths The [strict send payment path](https://developers.stellar.org/docs/data/apis/horizon/api-reference/resources/operations/object/path-payment-strict-send) endpoint lists the paths a payment can take based on the amount of an asset you want to send. The source asset amount stays constant, and the type and amount of an asset received varies based on offers in the order books. For this search, Horizon loads a list of assets that the recipient can receive (based on `destination_account` or `destination_assets`) and displays the possible paths from the different source assets to the destination asset. Only paths that satisfy the `source_amount` are returned. Request --- ## List Trade Aggregations This endpoint displays [trade](https://developers.stellar.org/docs/learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools#orders) data based on filters set in the arguments. This is done by dividing a given time range into segments and aggregating statistics, for a given asset pair (base, counter) over each of these segments. The duration of the segments is specified with the `resolution` parameter. The start and end of the time range are given by `startTime` and `endTime` respectively, which are both rounded to the nearest multiple of `resolution` since epoch. The individual segments are also aligned with multiples of `resolution` since epoch. If you want to change this alignment, the segments can be `offset` by specifying the offset parameter. Request --- ## Retrieve Related Operations(Api-reference) This endpoint represents successful operations referencing a given liquidity pool and can be used in streaming mode. Streaming mode allows you to listen for new operations referencing this liquidity pool as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known operation unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream operations created since your request time. Request --- ## Retrieve Related Transactions(Api-reference) This endpoint represents successful transactions referencing a given liquidity pool and can be used in streaming mode. Streaming mode allows you to listen for new transactions referencing this liquidity pool as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known transaction unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream transactions created since your request time. Request --- ## Resources Data on the Stellar ledger is organized according to resources. Each resource has several different endpoints. | | | | ---------------------------------------------------- | --- | | [Ledgers](./ledgers/README.mdx) | | | [Transactions](./transactions/README.mdx) | | | [Operations](./operations/README.mdx) | | | [Effects](./effects/README.mdx) | | | [Accounts](./accounts/README.mdx) | | | [Offers](./offers/README.mdx) | | | [Claimable Balances](./claimablebalances/README.mdx) | | | [Trades](./trades/README.mdx) | | | [Assets](./assets/README.mdx) | | | [Liquidity Pools](./liquiditypools/README.mdx) | | --- ## Accounts(Accounts) Users interact with the Stellar network through accounts. Everything else in the ledger—assets, offers, trustlines, etc.—are owned by accounts, and accounts must authorize all changes to the ledger through signed transactions. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. Learn more about [accounts](../../../../../../learn/glossary.mdx#account). | | | | --- | --- | | GET | [/accounts](../../list-all-accounts.api.mdx) | | GET | [/accounts/:account_id](../../retrieve-an-account.api.mdx) | | GET | [/accounts/:account_id/transactions](../../get-transactions-by-account-id.api.mdx) | | GET | [/accounts/:account_id/operations](../../get-operations-by-account-id.api.mdx) | | GET | [/accounts/:account_id/payments](../../get-payments-by-account-id.api.mdx) | | GET | [/accounts/:account_id/effects](../../get-effects-by-account-id.api.mdx) | | GET | [/accounts/:account_id/offers](../../get-offers-by-account-id.api.mdx) | | GET | [/accounts/:account_id/trades](../../get-trades-by-account-id.api.mdx) | | GET | [/accounts/:account_id/data](../../get-data-by-account-id.api.mdx) | --- ## The Account Object When Horizon returns information about an account, it uses the following format: - ATTRIBUTE - DATA TYPE - DESCRIPTION - id - string - A unique identifier for this account. - account_id - string - This account's public key encoded in a base32 string representation. - sequence - number - This account's current sequence number. For use when submitting this account's next transaction. - sequence_ledger - number - The unsigned 32-bit ledger number of the sequence number's [age](../../../../../../learn/glossary.mdx#sequence-number). - sequence_time - string - The unsigned 64-bit UNIX timestamp of the sequence number's [age](../../../../../../learn/glossary.mdx#sequence-number). - subentry_count - number - The number of subentries on this account. - home_domain - string - The domain that hosts this account's `stellar.toml` file. - last_modified_ledger - number - The ID of the last ledger that included changes to this account. - num_sponsoring - number - The number of reserves sponsored by this account. - num_sponsored - number - The number of reserves sponsored for this account. - sponsor - string (optional) - The account ID of the sponsor who is paying the reserves for this account. - thresholds - object - Operations have varying levels of access. This field specifies thresholds for different access levels, as well as the weight of the master key. - low_threshold - number - The weight required for a valid transaction including the Allow Trust and Bump Sequence operations. - med_threshold - number - The weight required for a valid transaction including the Create Account, Payment, Path Payment, Manage Buy Offer, Manage Sell Offer, Create Passive Sell Offer, Change Trust, Inflation, and Manage Data operations. - high_threshold - number - The weight required for a valid transaction including the Account Merge and Set Options operations. - flags - object - Flags denote the enabling/disabling of certain asset issuer privileges. - auth_immutable - boolean - If set to `true`, none of the following flags can be changed. - auth_required - boolean - If set to `true`, anyone who wants to hold an asset issued by this account must first be approved by this account. - auth_revocable - boolean - If set to `true`, this account can freeze the balance of a holder of an asset issued by this account. - auth_clawback_enabled - boolean - If set to `true`, trustlines created for assets issued by this account have [clawbacks](../../../../../../learn/glossary.mdx#clawback) enabled. - balances - object - The assets this account holds. - balance - string - The number of units of an asset held by this account. - buying_liabilities - string (optional) - The sum of all buy offers owned by this account for this asset. - selling_liabilities - string (optional) - The sum of all sell offers owned by this account for this asset. - limit - number (optional) - The maximum amount of this asset that this account is willing to accept. Specified when opening a trustline. - asset_type - string - Either `native`, `credit_alphanum4`, `credit_alphanum12`, or `liquidity_pool_shares`. - asset_code - string (optional) - The code for this asset. - asset_issuer - string (optional) - The Stellar address of this asset's issuer. - sponsor - string (optional) - The account ID of the sponsor who is paying the reserves for this trustline. - signers - array of objects - The public keys and associated weights that can be used to authorize transactions for this account. Used for multi-sig. - public_key - string - **REMOVED in 0.17.0: USE `key` INSTEAD.** - weight - number - The numerical weight of a signer. Used to determine if a transaction meets the `threshold` requirements. - sponsor - string (optional) - The account ID of the sponsor who is paying the reserves for this signer. - key - string - A hash of characters dependent on the signer type. - type - string - The type of hash for this signer. - ed25519_public_key - skip - A normal Stellar public key. - sha256_hash - skip - The SHA256 hash of some arbitrary `x`. Adding a signature of this type allows anyone who knows x to sign a transaction from this account. _Note: Once this transaction is broadcast, x will be known publicly._ - preauth_tx - skip - The hash of a pre-authorized transaction. This signer is automatically removed from the account when a matching transaction is properly applied. - data - object - An array of account data fields. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/accounts/GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U" }, "transactions": { "href": "https://horizon-testnet.stellar.org/accounts/GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U/transactions{?cursor,limit,order}", "templated": true }, "operations": { "href": "https://horizon-testnet.stellar.org/accounts/GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U/operations{?cursor,limit,order}", "templated": true }, "payments": { "href": "https://horizon-testnet.stellar.org/accounts/GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U/payments{?cursor,limit,order}", "templated": true }, "effects": { "href": "https://horizon-testnet.stellar.org/accounts/GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U/effects{?cursor,limit,order}", "templated": true }, "offers": { "href": "https://horizon-testnet.stellar.org/accounts/GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U/offers{?cursor,limit,order}", "templated": true }, "trades": { "href": "https://horizon-testnet.stellar.org/accounts/GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U/trades{?cursor,limit,order}", "templated": true }, "data": { "href": "https://horizon-testnet.stellar.org/accounts/GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U/data/{key}", "templated": true } }, "id": "GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U", "account_id": "GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U", "sequence": "24739097524306468", "subentry_count": 3, "inflation_destination": "GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U", "home_domain": "tempo.eu.com", "last_modified_ledger": 23569316, "num_sponsoring": 0, "num_sponsored": 0, "thresholds": { "low_threshold": 5, "med_threshold": 0, "high_threshold": 0 }, "flags": { "auth_required": false, "auth_revocable": true, "auth_immutable": false, "auth_clawback_enabled": true }, "balances": [ { "balance": "1.0000005", "limit": "922337203685.4775807", "buying_liabilities": "0.0000000", "selling_liabilities": "0.0000000", "last_modified_ledger": 22651481, "is_authorized": true, "is_clawback_enabled": false, "asset_type": "credit_alphanum4", "asset_code": "EURT", "asset_issuer": "GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S" }, { "balance": "0.0000000", "limit": "922337203685.4775807", "buying_liabilities": "0.0000000", "selling_liabilities": "0.0000000", "last_modified_ledger": 7877447, "is_authorized": false, "is_clawback_enabled": false, "asset_type": "credit_alphanum4", "asset_code": "PHP", "asset_issuer": "GBUQWP3BOUZX34TOND2QV7QQ7K7VJTG6VSE7WMLBTMDJLLAW7YKGU6EP" }, { "balance": "0.0000000", "limit": "922337203685.4775807", "buying_liabilities": "0.0000000", "selling_liabilities": "0.0000000", "last_modified_ledger": 20213845, "is_authorized": true, "is_clawback_enabled": false, "asset_type": "credit_alphanum4", "asset_code": "NGN", "asset_issuer": "GCC4YLCR7DDWFCIPTROQM7EB2QMFD35XRWEQVIQYJQHVW6VE5MJZXIGW" }, { "balance": "198.8944970", "buying_liabilities": "0.0000000", "selling_liabilities": "0.0000000", "asset_type": "native" } ], "signers": [ { "weight": 10, "key": "GDI73WJ4SX7LOG3XZDJC3KCK6ED6E5NBYK2JUBQSPBCNNWEG3ZN7T75U", "type": "ed25519_public_key" } ], "data": {}, "paging_token": "" } ``` --- ## Assets Assets are representations of value issued on the Stellar network. An asset consists of a type, code, and issuer. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. Learn more about [assets](../../../../../../learn/glossary.mdx#asset). | | | | --- | ---------------------------------------- | | GET | [/assets](../../list-all-assets.api.mdx) | --- ## The Asset Object When Horizon returns information about an asset, it uses the following format: - ATTRIBUTE - DATA TYPE - DESCRIPTION - asset_type - string - This asset's type. Either `credit_alphanum4` or `credit_alphanum12`. - asset_code - string - This asset's code - asset_issuer - string - The Stellar address of this asset’s issuer. - accounts - object - The number of accounts grouped by each trustline flag state. - num_claimable_balances - number - The current number of claimable_balances for this asset. - num_contracts - number - The current number of Soroban contracts holding this asset. - num_liquidity_pools - number - The current number of liquidity pools holding this asset. - balances - object - The number of units issued for this asset grouped by each trustline flag state. - claimable_balances_amount - string - The number of units for this asset held by all claimable balances. - contracts_amount - string - The number of units for this asset held by all Soroban contracts. - liquidity_pools_amount - string - The number of units for this asset held by all liquidity pools. - flags - object - Flags denote the enabling/disabling of certain asset issuer privileges. - auth_immutable - boolean - If set to `true`, none of the following flags can be changed. - auth_required - boolean - If set to `true`, anyone who wants to hold an asset issued by this account must first be approved by this account. - auth_revocable - boolean - If set to `true`, this account can freeze the balance of a holder of an asset issued by this account. - paging_token - number - A cursor value for use in [pagination](../../structure/pagination/README.mdx). The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/assets?asset_code=USD\u0026asset_issuer=GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX\u0026cursor=\u0026limit=10\u0026order=asc" }, "next": { "href": "https://horizon-testnet.stellar.org/assets?asset_code=USD\u0026asset_issuer=GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX\u0026cursor=USD_GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX_credit_alphanum4\u0026limit=10\u0026order=asc" }, "prev": { "href": "https://horizon-testnet.stellar.org/assets?asset_code=USD\u0026asset_issuer=GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX\u0026cursor=USD_GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX_credit_alphanum4\u0026limit=10\u0026order=desc" } }, "_embedded": { "records": [ { "_links": { "toml": { "href": "https://www.anchorusd.com/.well-known/stellar.toml" } }, "asset_type": "credit_alphanum4", "asset_code": "USD", "asset_issuer": "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", "paging_token": "USD_GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX_credit_alphanum4", "accounts": { "authorized": 9390, "authorized_to_maintain_liabilities": 1240, "unauthorized": 5 }, "num_claimable_balances": 253, "balances": { "authorized": "1347404.4083346", "authorized_to_maintain_liabilities": "177931.9984610", "unauthorized": "717.4677360" }, "claimable_balances_amount": "36303.8674450", "flags": { "auth_required": false, "auth_revocable": false, "auth_immutable": false } } ] } } ``` --- ## Claimable Balances(Claimablebalances) A Claimable Balance represents the transfer of ownership of some amount of an asset. Claimable balances provide a mechanism for setting up a payment which can be claimed in the future. This allows you to make payments to accounts which are currently not able to accept them. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. | | | | --- | --- | | GET | [/claimable_balances](../../list-all-claimable-balances.api.mdx) | | GET | [/claimable_balances/:claimable_balance_id](../../retrieve-a-claimable-balance.api.mdx) | | GET | [/claimable_balances/:claimable_balance_id/transactions](../../cb-retrieve-related-transactions.api.mdx) | | GET | [/claimable_balances/:claimable_balance_id/operations](../../cb-retrieve-related-operations.api.mdx) | --- ## The Claimable Balance Object When Horizon returns information about a claimable balance, it uses the following format: - ATTRIBUTE - DATA TYPE - DESCRIPTION - id - string - A unique identifier for this claimable balance. - paging_token - number - A cursor value for use in [pagination](../../structure/pagination/README.mdx). - asset - string - The asset available to be claimed in the [SEP-11 form](https://github.com/stellar/stellar-protocol/blob/0c675fb3a482183dcf0f5db79c12685acf82a95c/ecosystem/sep-0011.md#values) `asset_code:issuing_address` or `native` (for XLM) - amount - string - The amount of `asset` that can be claimed. - sponsor - string (optional) - The account id of the sponsor who is paying the reserves for this claimable balance. - last_modified_ledger - integer - The sequence number of the last ledger in which this claimable balance was modified. - last_modified_time - string - An ISO 8601 formatted string of last modification time. - claimants - array of objects - The list of entries which could claim the claimable balance. - destination - string - The account ID who can claim the balance. - predicate - object - The condition which must be satisfied so `destination` can claim the balance. - unconditional - boolean (optional) - If true it means this clause of the condition is always satisfied. - and - array of objects (optional) - The array will always contain two elements which also are predicates. This clause of the condition is satisfied if both of the two elements in the array are satisfied. - or - array of objects (optional) - The array will always contain two elements which also are predicates. This clause of the condition is satisfied if at least one of the two elements in the array are satisfied. - not - object (optional) - The value is also a predicate. This clause of the condition is satisfied if the value is _not_ satisfied. - absBefore - string (optional) - A customized ISO 8601 formatted string representing a deadline for when the claimable balance can be claimed. If the balance is claimed before the date then this clause of the condition is satisfied. The format of this date string is a custom extension on top of ISO 8601 format. It allows for years to be outside the 0000-9999 range. The dates are derived from a unix epoch value in range of signed 64 bit integer. This means the date expresses a much larger calendar range of 292277026596 years into future and -292471206707 years back in past. This custom extension format will add a `'+'` prefix on values that go beyond year 9999 into the future and for years that are prior to year 0(B.C per Gregorian calendar) it will add prefix of `'-'`. Here are examples of date string values that are possible: `'2022-02-10T15:30:22Z'` `'+39121901036-03-29T15:30:22Z'` `'-7025-12-23T00:00:00Z'` - absBeforeEpoch - string (optional) - A unix epoch value in seconds representing the same deadline date for when the claimable balance can be claimed. It is the same date/time value that absBefore represents, just expressed in integral unix epoch seconds within the range of a signed 64bit integer. - relBefore - string (optional) - A relative deadline for when the claimable balance can be claimed. The value represents the number of seconds since the close time of the ledger which created the claimable balance. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/claimable_balances/000000000102030000000000000000000000000000000000000000000000000000000000" }, "operations": { "href": "https://horizon-testnet.stellar.org/claimable_balances/000000000102030000000000000000000000000000000000000000000000000000000000/operations{?cursor,limit,order}", "templated": true }, "transactions": { "href": "https://horizon-testnet.stellar.org/claimable_balances/000000000102030000000000000000000000000000000000000000000000000000000000/transactions{?cursor,limit,order}", "templated": true } }, "id": "000000000102030000000000000000000000000000000000000000000000000000000000", "paging_token": "000000000102030000000000000000000000000000000000000000000000000000000000", "asset": "native", "amount": "10.0000000", "claimants": [ { "destination": "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", "predicate": { "and": [ { "or": [ { "relBefore": "12" }, { "absBefore": "2020-08-26T11:15:39Z", "absBeforeEpoch": "1598440539" } ] }, { "not": { "unconditional": true } } ] } } ], "last_modified_ledger": 28411995, "last_modified_time": "2020-02-26T19:29:16Z" } ``` --- ## Effects Effects represent specific changes that occur in the ledger as a result of successful operations, but are not necessarily directly reflected in the ledger or history, as transactions and operations are. | | | | --- | ------------------------------------------ | | GET | [/effects](../../list-all-effects.api.mdx) | --- ## Effect Types There are eight groups of effect types. Each effect type has its own set of attributes. ### Account Effects - TYPE - skip - OPERATION(S) - Account Created - skip - create_account - Account Removed - skip - merge_account - Account Credited - skip - create_account, payment, path_payment, merge_account - Account Debited - skip - create_account, payment, path_payment, merge_account - Account Thresholds Updated - skip - set_options - Account Home Domain Updated - skip - set_options - Account Flags Updated - skip - set_options - Account Inflation Destination Updated - skip - set_options ### Signer Effects - TYPE - skip - OPERATION(S) - Signer Created - skip - set_options - Signer Removed - skip - set_options - Signer Updated - skip - set_options ### Trustline Effects - TYPE - skip - OPERATION(S) - Trustline Created - skip - change_trust - Trustline Removed - skip - change_trust - Trustline Updated - skip - change_trust, allow_trust - Trustline Authorized - skip - allow_trust - Trustline Deauthorized - skip - allow_trust ### Trading Effects - TYPE - skip - STATUS / OPERATION(S) - Offer Created - skip - Unused; not emitted - Offer Removed - skip - Unused; not emitted - Offer Updated - skip - Unused; not emitted - Trade - skip - manage_buy_offer, manage_sell_offer, create_passive_sell_offer, path_payment :::note[Offer Lifecycle] Although offer identifiers were [historically defined](https://github.com/stellar/stellar-horizon/blob/main/internal/db2/history/main.go#L114-L122), [manage_offer](../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#manage-sell-offer)'s effects don't have ingestion code. To view them, decode the transaction's [`result_xdr`](../../structure/xdr.mdx), which contains the `ManageOfferSuccessResult`. Use [List All Offers](../../get-all-offers.api.mdx) to inspect currently open offers and [Retrieve an Offer's Trades](../../get-trades-by-offer-id.api.mdx) for retained trade history associated with an offer. ::: ### Data Effects - TYPE - skip - OPERATION(S) - Data Created - skip - manage_data - Data Removed - skip - manage_data - Data Updated - skip - manage_data ### Claimable Balance Effects - TYPE - skip - OPERATION(S) - Claimable Balance Created - skip - create_claimable_balance - Claimable Balance Claimant Created - skip - create_claimable_balance - Claimable Balance Claimed - skip - claim_claimable_balance ### Sponsorship Effects - TYPE - skip - OPERATION(S) - Account Sponsorship Created - skip - create_account - Account Sponsorship Updated - skip - revoke_sponsorship - Account Sponsorship Removed - skip - revoke_sponsorship - Trustline Sponsorship Created - skip - change_trust - Trustline Sponsorship Updated - skip - revoke_sponsorship - Trustline Sponsorship Removed - skip - revoke_sponsorship - Account Data Sponsorship Created - skip - manage_data - Account Data Sponsorship Updated - skip - revoke_sponsorship - Account Data Sponsorship Removed - skip - revoke_sponsorship - Claimable Balance Sponsorship Created - skip - create_claimable_balance - Claimable Balance Sponsorship Updated - skip - revoke_sponsorship - Claimable Balance Sponsorship Removed - skip - revoke_sponsorship - Account Signer Sponsorship Created - skip - set_options - Account Signer Sponsorship Updated - skip - revoke_sponsorship - Account Signer Sponsorship Removed - skip - revoke_sponsorship ### Liquidity Pool Effects - TYPE - skip - OPERATION(S) - Liquidity Pool Created - skip - change_trust - Liquidity Pool Removed - skip - change_trust - Liquidity Pool Revoked - skip - change_trust, allow_trust - Liquidity Pool Deposited - skip - liquidity_pool_deposit - Liquidity Pool Withdraw - skip - liquidity_pool_withdraw - Liquidity Pool Trade - skip - path_payment ### Miscellaneous Effects - TYPE - skip - OPERATION(S) - Sequence Bumped - skip - bump_sequence --- ## Ledgers Each ledger stores the state of the network at a point in time and contains all the changes - transactions, operations, effects, etc. - to that state. Learn more about [ledgers](../../../../../../learn/glossary.mdx#ledger). | | | | --- | --- | | GET | [/ledgers/:ledger_sequence](../../retrieve-a-ledger.api.mdx) | | GET | [/ledgers/:ledger_sequence/transactions](../../retrieve-a-ledgers-transactions.api.mdx) | | GET | [/ledgers/:ledger_sequence/operations](../../retrieve-a-ledgers-operations.api.mdx) | | GET | [/ledgers/:ledger_sequence/payments](../../retrieve-a-ledgers-payments.api.mdx) | | GET | [/ledgers/:ledger_sequence/effects](../../retrieve-a-ledgers-effects.api.mdx) | | GET | [/ledgers](../../list-all-ledgers.api.mdx) | --- ## The Ledger Object When Horizon returns information about a ledger, it uses the following format: - ATTRIBUTE - DATA TYPE - DESCRIPTION - id - string - A unique identifier for this ledger. - paging_token - number - A cursor value for use in [pagination](../../structure/pagination/README.mdx). - hash - string - A hex-encoded SHA-256 hash of this ledger’s [XDR](../../../../../../learn/fundamentals/data-format/xdr.mdx)-encoded form. - prev_hash - string - The hash of the ledger immediately preceding this ledger. - sequence - number - The sequence number of this ledger, and the parameter used in Horizon calls that require a ledger number. - successful_transaction_count - number - The number of successful transactions in this ledger. - failed_transaction_count - number - The number of failed transactions in this ledger. - operation_count - number - The number of operations applied in this ledger. - tx_set_operation_count - number - The number of total operations in the transaction set (including failed transactions). - closed_at - string - An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted string of when this ledger was closed. - total_coins - string - The total number of lumens in circulation. - fee_pool - string - The sum of all transaction fees. - base_fee_in_stroops - number - The fee the network charges per operation in a transaction. - base_reserve_in_stroops - number - The reserve the network uses when calculating an account’s minimum balance. - max_tx_set_size - number - The maximum number of operations validators have agreed to process in a given ledger. Since Protocol 11, ledger capacity has been measured in operations rather than transactions. For more info on that decision, see [CAP-5](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0005.md#repurposing-ledgerheadermaxtxsetsize). - protocol_version - number - The protocol version that the Stellar network was running when this ledger was committed. - header_xdr - string - A base64 encoded string of the raw `LedgerHeader` xdr struct for this ledger. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/ledgers/26857634" }, "transactions": { "href": "https://horizon-testnet.stellar.org/ledgers/26857634/transactions{?cursor,limit,order}", "templated": true }, "operations": { "href": "https://horizon-testnet.stellar.org/ledgers/26857634/operations{?cursor,limit,order}", "templated": true }, "payments": { "href": "https://horizon-testnet.stellar.org/ledgers/26857634/payments{?cursor,limit,order}", "templated": true }, "effects": { "href": "https://horizon-testnet.stellar.org/ledgers/26857634/effects{?cursor,limit,order}", "templated": true } }, "id": "548393ec23959e1959a62f003029ecf96be89e13df036073bf64918996ec4227", "paging_token": "115352659677937664", "hash": "548393ec23959e1959a62f003029ecf96be89e13df036073bf64918996ec4227", "prev_hash": "446d6eca81dd6db6daf50d93ca9d297bd60b1233b91de3765cccdf503cfffcb0", "sequence": 26857634, "successful_transaction_count": 27, "failed_transaction_count": 1, "operation_count": 133, "tx_set_operation_count": 134, "closed_at": "2019-11-18T19:27:21Z", "total_coins": "105443902087.3472865", "fee_pool": "1807038.9789761", "base_fee_in_stroops": 100, "base_reserve_in_stroops": 5000000, "max_tx_set_size": 1000, "protocol_version": 12, "header_xdr": "AAAADERtbsqB3W222vUNk8qdKXvWCxIzuR3jdlzM31A8//ywoQieYsSc05/BpgEqnLR7fKXz7t0K42V7NOjbGZA/wTEAAAAAXdLwmQAAAAAAAAAAplf68mTg/Z/DDyEZeLCoNbJnMZm4SYsYWjUjuDOSfPeRNFE4n9Hm19yKutjwVurFjk72JKVHI8J+ELwLZgWsywGZ0KIOoh6z7HlbYQAAEG9XKhRBAAABFgAAAAAH9M6YAAAAZABMS0AAAAPop9+CeMs1/7BHgFltiQPH+VT+ACYb5P0lSXh7RpBLtd34kEpeL8qKJxYz4ufmkQ2lEv/HMR/i3bi1Rt0PYj185/0kAZ3ZRbmm2mVRMzmaCOak1rn2vejHXDh+MGlr6D6vI2tc/M6VIumTKUa7SgumWDyW0r5FcJTbu/FXDQ/6C4YAAAAA" } ``` --- ## Liquidity Pools(Liquiditypools) Liquidity Pools provide a simple, non-interactive way to trade large amounts of capital and enable high volumes of trading. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. | | | | --- | --- | | GET | [/liquidity_pools](../../list-liquidity-pools.api.mdx) | | GET | [/liquidity_pools/:liquidity_pool_id](../../retrieve-a-liquidity-pool.api.mdx) | | GET | [/liquidity_pools/:liquidity_pool_id/effects](../../retrieve-related-effects.api.mdx) | | GET | [/liquidity_pools/:liquidity_pool_id/trades](../../retrieve-related-trades.api.mdx) | | GET | [/liquidity_pools/:liquidity_pool_id/transactions](../../lp-retrieve-related-transactions.api.mdx) | | GET | [/liquidity_pools/:liquidity_pool_id/operations](../../lp-retrieve-related-operations.api.mdx) | --- ## Offers(Offers) Offers are statements about how much of an asset an account wants to buy or sell. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. Learn more about [offers](../../../../../../learn/glossary.mdx#decentralized-exchange). | | | | --- | ---------------------------------------------------------------- | | GET | [/offers](../../get-all-offers.api.mdx) | | GET | [/offers/:offer_id](../../get-offer-by-offer-id.api.mdx) | | GET | [/offers/:offer_id/trades](../../get-trades-by-offer-id.api.mdx) | --- ## The Offer Object When Horizon returns information about an offer, it uses the following format: - ATTRIBUTE - DATA TYPE - DESCRIPTION - id - string - A unique identifier for this offer. - paging_token - number - A cursor value for use in [pagination](../../structure/pagination/README.mdx). - seller - string - The account ID of the account making this offer. - selling - asset code - The asset this offer wants to sell. - buying - asset code - The asset this offer wants to buy. - amount - string - The amount of `selling` that the account making this offer is willing to sell. - price_r - object - A precise representation of the buy and sell price of the assets on offer. - n - number - The numerator. - d - number - The denominator. - price - string - How many units of `buying` it takes to get 1 unit of `selling`. A number representing the decimal form of `price_r`. - last_modified_ledger - integer - The sequence number of the last ledger in which this offer was modified. - last_modified_time - string - An ISO 8601 formatted string of last modification time. - sponsor - string (optional) - The account id of the sponsor who is paying the reserves for this offer. The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/offers/165561423" }, "offer_maker": { "href": "https://horizon-testnet.stellar.org/accounts/GCK4WSNF3F6ZNCMK6BU77ZCZ3NMF3JGU2U3ZAPKXYBKYYCJA72FDBY7K" } }, "id": 165561423, "paging_token": "165561423", "seller": "GCK4WSNF3F6ZNCMK6BU77ZCZ3NMF3JGU2U3ZAPKXYBKYYCJA72FDBY7K", "selling": { "asset_type": "credit_alphanum4", "asset_code": "NGNT", "asset_issuer": "GAWODAROMJ33V5YDFY3NPYTHVYQG7MJXVJ2ND3AOGIHYRWINES6ACCPD" }, "buying": { "asset_type": "native" }, "amount": "18421.4486092", "price_r": { "n": 45112058, "d": 941460545 }, "price": "0.0479171", "last_modified_ledger": 28411995, "last_modified_time": "2020-02-26T19:29:16Z" } ``` --- ## Operations Operations are objects that represent a desired change to the ledger: payments, offers to exchange currency, changes made to account options, etc. Operations are submitted to the Stellar network grouped in a Transaction. Each of Stellar’s operations have a unique operation object. | | | | --- | --- | | GET | [/operations/:operation_id](../../retrieve-an-operation.api.mdx) | | GET | [/operations/:operation_id/effects](../../retrieve-an-operations-effects.api.mdx) | | GET | [/operations](../../list-all-operations.api.mdx) | | GET | [/payments](../../list-all-payments.api.mdx) | --- ## The Operation Object Each of Stellar’s operations have unique response shapes. Below are the attributes that are common across individual operation objects. See the [generic Operation errors](../../../errors/result-codes/operations.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - id - number - The operation's ID number. - paging_token - string - A cursor value for use in [pagination](../../../structure/pagination/README.mdx). - type_i - number - A number indicating the operation type. - type - string - The name of the operation type. - transaction_hash - string - A unique identifier for the transaction this operation belongs to. - transaction_successful - boolean - Indicates if this operation was part of a successful transaction. - source_account - string - The account that originates the operation. - created_at - string - The date this operation was created. ```json { "_links": { "effects": { "href": "/operations/402494270214144/effects/{?cursor,limit,order}", "templated": true }, "precedes": { "href": "/operations?cursor=402494270214144&order=asc" }, "self": { "href": "/operations/402494270214144" }, "succeeds": { "href": "/operations?cursor=402494270214144&order=desc" }, "transactions": { "href": "/transactions/402494270214144" } }, "id": 402494270214144, "paging_token": "402494270214144", "type_i": 0, "type": "create_account" } ``` --- ## Account Merge Object Removes the source account from the Stellar and transfers the source account's lumens to another account. See the [`Account Merge` errors](../../../errors/result-codes/operation-specific/account-merge.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - account - string - The Stellar address being removed. - into - string - The Stellar address receiving the deleted account's lumens. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/121887714411839489" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/02077009a551ec94c776f83529293dcfc1c2cd5b38af043ef7f3699bf5a71f0a" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/121887714411839489/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=121887714411839489" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=121887714411839489" } }, "id": "121887714411839489", "paging_token": "121887714411839489", "transaction_successful": true, "source_account": "GCVLWV5B3L3YE6DSCCMHLCK7QIB365NYOLQLW3ZKHI5XINNMRLJ6YHVX", "type": "account_merge", "type_i": 8, "created_at": "2020-02-24T17:03:00Z", "transaction_hash": "02077009a551ec94c776f83529293dcfc1c2cd5b38af043ef7f3699bf5a71f0a", "account": "GCVLWV5B3L3YE6DSCCMHLCK7QIB365NYOLQLW3ZKHI5XINNMRLJ6YHVX", "into": "GATL3ETTZ3XDGFXX2ELPIKCZL7S5D2HY3VK4T7LRPD6DW5JOLAEZSZBA" } ``` --- ## Allow Trust Object Updates the “authorized” flag of an existing trust line. This must be called by the issuer of the asset. See the [`Allow Trust` errors](../../../errors/result-codes/operation-specific/allow-trust.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - asset_type - string - The type of asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - asset_code - string - The Stellar address of the asset. - asset_issuer - string - The code for the asset. - authorize - int - Flag indicating whether the trustline is authorized. 0 if the account is not authorized to transact with the asset in any way. 1 if the account is authorized to transact with the asset. 2 if the account is authorized to maintain orders, but not to perform other transactions. - trustee - string - The issuing account, or source account in this instance. - trustor - string - The trusting account, or the account being authorized or unauthorized. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/120497059836067841" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/ac8dd0ddf1d047081c8e4c2a7ef9cc38a1a8af6c211184e1b16ebf2e32915d7f" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/120497059836067841/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=120497059836067841" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=120497059836067841" } }, "id": "120497059836067841", "paging_token": "120497059836067841", "transaction_successful": true, "source_account": "GCRZQVBBDAWVOCO5R2NI34YR55RO2GQXPTDUE5OZESXGZRRTAEQLKEKN", "type": "allow_trust", "type_i": 7, "created_at": "2020-02-03T14:30:52Z", "transaction_hash": "ac8dd0ddf1d047081c8e4c2a7ef9cc38a1a8af6c211184e1b16ebf2e32915d7f", "asset_type": "credit_alphanum4", "asset_code": "LSV1", "asset_issuer": "GCRZQVBBDAWVOCO5R2NI34YR55RO2GQXPTDUE5OZESXGZRRTAEQLKEKN", "trustee": "GCRZQVBBDAWVOCO5R2NI34YR55RO2GQXPTDUE5OZESXGZRRTAEQLKEKN", "trustor": "GDSYBYRG6NIBJWR7BLY72HYV7VM4A7WWHUJ45FI7H4Q2U2RPR3BB3CFR", "authorize": true } ``` --- ## Begin Sponsoring Future Reserves Initiate a sponsorship. - ATTRIBUTE - DATA TYPE - DESCRIPTION - sponsored_id - string - The id of the account which will be sponsored. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=124922916260433921" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=124922916260433921" } }, "id": "124922916260433921", "paging_token": "124922916260433921", "transaction_successful": true, "source_account": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA", "type": "begin_sponsoring_future_reserves", "type_i": 16, "created_at": "2020-04-09T00:14:11Z", "transaction_hash": "f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1", "sponsored_id": "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML" } ``` --- ## Bump Sequence Object Bumps forward the sequence number of the source account, allowing it to invalidate any transactions with a smaller sequence number. See the [`Bump Sequence` errors](../../../errors/result-codes/operation-specific/bump-sequence.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - bump_to - string - The new desired value for the source account's sequence number. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=124922916260433921" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=124922916260433921" } }, "id": "124922916260433921", "paging_token": "124922916260433921", "transaction_successful": true, "source_account": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA", "type": "bump_sequence", "type_i": 11, "created_at": "2020-04-09T00:14:11Z", "transaction_hash": "f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1", "bump_to": "120192344968520085" } ``` --- ## Manage Buy Offer Object Creates, updates, or deletes a buy offer to trade assets. A buy offer specifies a certain amount of the buying asset that should be sold in exchange for the minimum quantity of the selling asset. See the [`Manage Buy Offer` errors](../../../errors/result-codes/operation-specific/manage-buy-offer.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - amount - string - The amount of `buying_asset` that the account making this offer is willing to buy. - price - string - How many units of `buying_asset` it takes to get 1 unit of `selling_asset`. A number representing the decimal form of `price_r`. - price_r - object - A precise representation of the buy and sell price of the assets on offer. - n - number - The numerator. - d - number - The denominator. - buying_asset_type - string - The type for the buying asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - buying_asset_issuer - string - The Stellar address of the buying asset’s issuer. Appears if the `buying_asset_type` is not `native`. - buying_asset_code - string - The code for the buying asset. Appears if the `buying_asset_type` is not `native`. - selling_asset_type - string - The type for the selling asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - selling_asset_issuer - string - The Stellar address of the selling asset’s issuer. Appears if the `selling_asset_type` is not `native`. - selling_asset_code - string - The code for the selling asset. Appears if the `selling_asset_type` is not `native`. - offer_id - string - A unique identifier for this offer. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/124893981065674756" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/3b41ec1411ed67ed47c96c34067c9fcfadf6e7cc013effa0b10f3df5ed758ffc" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/124893981065674756/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=124893981065674756" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=124893981065674756" } }, "id": "124893981065674756", "paging_token": "124893981065674756", "transaction_successful": true, "source_account": "GDT7WYNV6YBFJH3G6TX5K3ALBZY7A7A7CLIGXK4XZ6H5SROPS4UFGEMC", "type": "manage_buy_offer", "type_i": 12, "created_at": "2020-04-08T14:03:03Z", "transaction_hash": "3b41ec1411ed67ed47c96c34067c9fcfadf6e7cc013effa0b10f3df5ed758ffc", "amount": "20.4521401", "price": "0.0300003", "price_r": { "n": 9000190, "d": 300003333 }, "buying_asset_type": "native", "selling_asset_type": "credit_alphanum4", "selling_asset_code": "EURT", "selling_asset_issuer": "GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S", "offer_id": "0" } ``` --- ## Change Trust Object Creates, updates, or deletes a trust line from the source account to another account's issued asset. See the [`Change Trust` errors](../../../errors/result-codes/operation-specific/change-trust.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - asset_type - string - The type of asset being trusted, one of `native`, `credit_alphanum4`, `credit_alphanum12`, or `liquidity_pool_shares`. - asset_code - string - The Stellar address of the asset being trusted. (Only present for credit assets.) - asset_issuer - string - The code for the asset being trusted. (Only present for credit assets.) - limit - string - Limits the amount of an asset that the source account can hold. - trustee - string - The issuing account. (Only present for credit assets.) - trustor - string - The source account. - liquidity_pool_id - string - The liquidity pool whose trustline is being modified. (Only present for `asset_type == liquidity_pool_shares`.) ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/120192477935251457" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/ec4116595bdfa8c1039c40af425e497c91fcf387c2a2a0cfa1f3bf64733f1f23" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/120192477935251457/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=120192477935251457" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=120192477935251457" } }, "id": "120192477935251457", "paging_token": "120192477935251457", "transaction_successful": true, "source_account": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA", "type": "change_trust", "type_i": 6, "created_at": "2020-01-29T19:46:55Z", "transaction_hash": "ec4116595bdfa8c1039c40af425e497c91fcf387c2a2a0cfa1f3bf64733f1f23", "asset_type": "credit_alphanum4", "asset_code": "NGNT", "asset_issuer": "GAWODAROMJ33V5YDFY3NPYTHVYQG7MJXVJ2ND3AOGIHYRWINES6ACCPD", "limit": "922337203685.4775807", "trustee": "GAWODAROMJ33V5YDFY3NPYTHVYQG7MJXVJ2ND3AOGIHYRWINES6ACCPD", "trustor": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA" } ``` --- ## Claim Claimable Balance Claims a claimable balance. - ATTRIBUTE - DATA TYPE - DESCRIPTION - balance_id - string - The id of the claimable balance. - claimant - string - The id of the account which claimed the balance. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=124922916260433921" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=124922916260433921" } }, "id": "124922916260433921", "paging_token": "124922916260433921", "transaction_successful": true, "source_account": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA", "type": "claim_claimable_balance", "type_i": 15, "created_at": "2020-04-09T00:14:11Z", "transaction_hash": "f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1", "id": "000000000102030000000000000000000000000000000000000000000000000000000000", "claimant": "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML" } ``` --- ## Create Account Object Creates and funds a new account with the specified starting balance. See the [`Create Account` errors](../../../errors/result-codes/operation-specific/create-account.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - starting_balance - string - The amount of XLM to send the newly created account. - funder - string - The account that funds the new account. - account - string - A new account that is funded. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/120192344791343105" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/ef0fe04ac3c7de7228ca2598886059868ad05c224a041e8b2d9ee2a8a9dd6894" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/120192344791343105/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=120192344791343105" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=120192344791343105" } }, "id": "120192344791343105", "paging_token": "120192344791343105", "transaction_successful": true, "source_account": "GBVFTZL5HIPT4PFQVTZVIWR77V7LWYCXU4CLYWWHHOEXB64XPG5LDMTU", "type": "create_account", "type_i": 0, "created_at": "2020-01-29T19:43:59Z", "transaction_hash": "ef0fe04ac3c7de7228ca2598886059868ad05c224a041e8b2d9ee2a8a9dd6894", "starting_balance": "2.0000000", "funder": "GBVFTZL5HIPT4PFQVTZVIWR77V7LWYCXU4CLYWWHHOEXB64XPG5LDMTU", "account": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA" } ``` --- ## Create Claimable Balance Creates a new claimable balance. - ATTRIBUTE - DATA TYPE - DESCRIPTION - asset - string - The asset available to be claimed in the [SEP-11 form](https://github.com/stellar/stellar-protocol/blob/0c675fb3a482183dcf0f5db79c12685acf82a95c/ecosystem/sep-0011.md#values) `asset_code:issuing_address` or `native` (for XLM) - amount - string - The amount available to be claimed. - claimants - array of objects - The list of entries which could claim the claimable balance. - destination - string - The account ID who can claim the balance. - predicate - object - The condition which must be satisfied so `destination` can claim the balance. - unconditional - boolean (optional) - If true it means this clause of the condition is always satisfied. - and - array of objects (optional) - The array will always contain two elements which also are predicates. This clause of the condition is satisfied if both of the two elements in the array are satisfied. - or - array of objects (optional) - The array will always contain two elements which also are predicates. This clause of the condition is satisfied if at least one of the two elements in the array are satisfied. - not - object (optional) - The value is also a predicate. This clause of the condition is satisfied if the value is _not_ satisfied. - absBefore - string (optional) - A customized ISO 8601 formatted string representing a deadline for when the claimable balance can be claimed. If the balance is claimed before the date then this clause of the condition is satisfied. The format of this date string is a custom extension on top of ISO 8601 format. It allows for years to be outside the 0000-9999 range. The dates are derived from a unix epoch value in range of signed 64 bit integer. This means the date expresses a much larger calendar range of 292277026596 years into future and -292471206707 years back in past. This custom extension format will add a `'+'` prefix on values that go beyond year 9999 into the future and for years that are prior to year 0(B.C per Gregorian calendar) it will add prefix of `'-'`. \ Here are examples of date string values that are possible: \ `'2022-02-10T15:30:22Z'` \ `'+39121901036-03-29T15:30:22Z'` \ `'-7025-12-23T00:00:00Z'` - absBeforeEpoch - string (optional) - A unix epoch value in seconds representing the same deadline date for when the claimable balance can be claimed. It is the same date/time value that absBefore represents, just expressed in integral unix epoch seconds within the range of a signed 64bit integer. - relBefore - string (optional) - A relative deadline for when the claimable balance can be claimed. The value represents the number of seconds since the close time of the ledger which created the claimable balance. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=124922916260433921" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=124922916260433921" } }, "id": "124922916260433921", "paging_token": "124922916260433921", "transaction_successful": true, "source_account": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA", "type": "create_claimable_balance", "type_i": 14, "created_at": "2020-04-09T00:14:11Z", "transaction_hash": "f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1", "asset": "NGNT:GAWODAROMJ33V5YDFY3NPYTHVYQG7MJXVJ2ND3AOGIHYRWINES6ACCPD", "amount": "200.0000000", "claimants": [ { "destination": "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", "predicate": { "and": [ { "or": [ { "relBefore": "12" }, { "absBefore": "2020-08-26T11:15:39Z", "absBeforeEpoch": "1598440539" } ] }, { "not": { "unconditional": true } } ] } } ] } ``` --- ## End Sponsoring Future Reserves End a sponsorship. - ATTRIBUTE - DATA TYPE - DESCRIPTION - begin_sponsor - string - The id of the account which initiated the sponsorship. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=124922916260433921" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=124922916260433921" } }, "id": "124922916260433921", "paging_token": "124922916260433921", "transaction_successful": true, "source_account": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA", "type": "end_sponsoring_future_reserves", "type_i": 17, "created_at": "2020-04-09T00:14:11Z", "transaction_hash": "f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1", "begin_sponsor": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA" } ``` --- ## Extend Footprint TTL Object [Extends](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#extend-footprint-ttl) the TTL for a given set of ledger entries. - ATTRIBUTE - DATA TYPE - DESCRIPTION - extend_to - number - The new live until ledger which will be applied to the ledger entries. ```json { "id": "1109896858714113", "paging_token": "1109896858714113", "transaction_successful": true, "source_account": "GBVAL7WM2G3NUTDIT5EYE4BMDRNLRQWI4RDGSVFCPXHPQRSSPEUQ2PNK", "type": "extend_footprint_ttl", "type_i": 25, "created_at": "2024-02-22T10:50:50Z", "transaction_hash": "1768d762bedf3129a00ec095d1aaed4c378dc3f55250758ed5008992b303303b", "extend_to": 100 } ``` --- ## Invoke Host Function Object [Invokes](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#invoke-host-function) a Soroban smart contract function. - ATTRIBUTE - DATA TYPE - DESCRIPTION - function - string - The type of `InvokeHostFunctionOp` which can be one of `HostFunctionTypeHostFunctionTypeInvokeContract`, `HostFunctionTypeHostFunctionTypeCreateContract`, or `HostFunctionTypeHostFunctionTypeUploadContractWasm`. - parameters - array - An array of parameters passed into the Soroban smart contract function (only present when the type of `InvokeHostFunctionOp` is `HostFunctionTypeHostFunctionTypeInvokeContract`). - type - string - The [type](../../../../../../../learn/fundamentals/contract-development/types/built-in-types.mdx) of the function parameter. - value - string - The base64 encoding of the XDR value of the parameter. - address - string - The address of the newly created contract (only present when the type of `InvokeHostFunctionOp` is `HostFunctionTypeHostFunctionTypeCreateContract`). - salt - string - The salt used to create the contract (only present when the type of `InvokeHostFunctionOp` is `HostFunctionTypeHostFunctionTypeCreateContract`). - asset_balance_changes - array - An array of [Stellar Asset Contract](../../../../../../../tokens/stellar-asset-contract) balance update events that occurred as a side effect of the `InvokeHostFunctionOp` (only present when the type of `InvokeHostFunctionOp` is `HostFunctionTypeHostFunctionTypeInvokeContract` _and_ the invocation is on a Stellar Asset Contract). - asset_type - string - The type of asset being sent. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - code - string - The code for the asset being sent. Appears if the `asset_type` is not `native`. - issuer - string - The Stellar address of the issuer of the asset being sent. Appears if the `asset_type` is not `native`. - type - string - The type of the [Stellar Asset Contract event](../../../../../../../tokens/stellar-asset-contract). It can be one of `transfer`, `mint`, `clawback`, or `burn`. - from - string - The Stellar address of the sender (can be a contract or a Stellar account). This field is not present when `type` is `mint`. - to - string - The Stellar address of the recipient (can be a contract or a Stellar account). This field is only present when `type` is `transfer` or `mint`. - amount - string - The amount sent. ```json { "id": "1109896858714113", "paging_token": "1109896858714113", "transaction_successful": true, "source_account": "GBVAL7WM2G3NUTDIT5EYE4BMDRNLRQWI4RDGSVFCPXHPQRSSPEUQ2PNK", "type": "invoke_host_function", "type_i": 24, "created_at": "2024-02-22T10:50:50Z", "transaction_hash": "1768d762bedf3129a00ec095d1aaed4c378dc3f55250758ed5008992b303303b", "function": "HostFunctionTypeHostFunctionTypeInvokeContract", "parameters": [ { "value": "AAAAEgAAAAHJOCa1uNUk//1ibktaJMY8Q0o5+CSWzWTw16NBiWFiGA==", "type": "Address" }, { "value": "AAAADwAAAAtzZXRfcmVjb3JkcwA=", "type": "Sym" }, { "value": "AAAAEAAAAAEAAAACAAAAEAAAAAEAAAACAAAADwAAAAVPdGhlcgAAAAAAAA8AAAADVVNEAAAAABAAAAABAAAAAgAAAA8AAAAFT3RoZXIAAAAAAAAPAAAAA0VVUgA=", "type": "Vec" }, { "value": "AAAAEAAAAAEAAAACAAAAEQAAAAEAAAACAAAADwAAAAVwcmljZQAAAAAAAAoAAAAAAAAAAAAAAAAAEcMIAAAADwAAAAl0aW1lc3RhbXAAAAAAAAAFAAAAAGXXJpwAAAARAAAAAQAAAAIAAAAPAAAABXByaWNlAAAAAAAACgAAAAAAAAAAAAAAAAAQXTQAAAAPAAAACXRpbWVzdGFtcAAAAAAAAAUAAAAAZdcmnA==", "type": "Vec" } ], "address": "", "salt": "", "asset_balance_changes": [] } ``` --- ## Liquidity Pool Deposit Object Deposit asset reserves into a liquidity pool. See the [`Liquidity Pool Deposit` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#liquidity-pool-deposit) for parameters, errors, etc. - ATTRIBUTE - DATA TYPE - DESCRIPTION - liquidity_pool_id - string - The liquidity pool associated with this deposit - reserves_max - array - An array of objects corresponding to the maximum amount of each reserve that could've been deposited - asset - string - The asset in canonical (`Code:Issuer`) form - amount - string - A floating point value encoded as a string - min_price - string - A floating point value encoded as a string indicating the minimum exchange rate for this deposit operation - min_price_r - object - A precise (fractional) representation of the buy and sell price of the assets on offer. - n - number - The numerator. - d - number - The denominator. - max_price - string - A floating point value encoded as a string indicating the maximum exchange rate for this deposit operation - max_price_r - object - A precise (fractional) representation of the buy and sell price of the assets on offer. - n - number - The numerator. - d - number - The denominator. - reserves_deposited - array - An array of objects representing how much of each reserve ended up actually deposited into the pool - asset - string - The asset in canonical (`Code:Issuer`) form - amount - string - A floating point value encoded as a string - shares_received - string - A floating point value encoded as a string representing the number of pool shares received for this deposit ```json { "id": "3697472920621057", "paging_token": "3697472920621057", "transaction_successful": true, "source_account": "GBB4JST32UWKOLGYYSCEYBHBCOFL2TGBHDVOMZP462ET4ZRD4ULA7S2L", "type": "liquidity_pool_deposit", "type_i": 22, "created_at": "2021-11-18T03:47:47Z", "transaction_hash": "43ed5ce19190822ec080b67c3ccbab36a56bc34102b1a21d3ee690ed3bc23378", "liquidity_pool_id": "abcdef", "reserves_max": [ { "asset": "JPY:GBVAOIACNSB7OVUXJYC5UE2D4YK2F7A24T7EE5YOMN4CE6GCHUTOUQXM", "amount": "1000.0000005" }, { "asset": "EURT:GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S", "amount": "3000.0000005" } ], "min_price": "0.2680000", "min_price_r": { "n": 67, "d": 250 }, "max_price": "0.3680000", "max_price_r": { "n": 73, "d": 250 }, "reserves_deposited": [ { "asset": "JPY:GBVAOIACNSB7OVUXJYC5UE2D4YK2F7A24T7EE5YOMN4CE6GCHUTOUQXM", "amount": "983.0000005" }, { "asset": "EURT:GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S", "amount": "2378.0000005" } ], "shares_received": "1000" } ``` --- ## Liquidity Pool Withdraw Object Withdraws asset reserves from a liquidity pool by redeeming pool shares. See the [`Liquidity Pool Withdraw` operation](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#liquidity-pool-withdraw) for parameters, errors, etc. - ATTRIBUTE - DATA TYPE - DESCRIPTION - liquidity_pool_id - string - The liquidity pool associated with this withdrawal - reserves_min - array - An array of objects corresponding to the minimum amount of each reserve that should've been withdrawn - asset - string - The asset in canonical (`Code:Issuer`) form - amount - string - A floating point value encoded as a string - shares - string - The number of shares that were redeemed for this withdrawal operation - reserves_received - array - An array of objects representing how much of each reserve ended up actually withdrawn from the pool - asset - string - The asset in canonical (`Code:Issuer`) form - amount - string - A floating point value encoded as a string ```json { "id": "3697472920621057", "paging_token": "3697472920621057", "transaction_successful": true, "source_account": "GBB4JST32UWKOLGYYSCEYBHBCOFL2TGBHDVOMZP462ET4ZRD4ULA7S2L", "type": "liquidity_pool_deposit", "type_i": 22, "created_at": "2021-11-18T03:47:47Z", "transaction_hash": "43ed5ce19190822ec080b67c3ccbab36a56bc34102b1a21d3ee690ed3bc23378", "liquidity_pool_id": "abcdef", "reserves_max": [ { "asset": "JPY:GBVAOIACNSB7OVUXJYC5UE2D4YK2F7A24T7EE5YOMN4CE6GCHUTOUQXM", "amount": "1000.0000005" }, { "asset": "EURT:GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S", "amount": "3000.0000005" } ], "min_price": "0.2680000", "min_price_r": { "n": 67, "d": 250 }, "max_price": "0.3680000", "max_price_r": { "n": 73, "d": 250 }, "reserves_deposited": [ { "asset": "JPY:GBVAOIACNSB7OVUXJYC5UE2D4YK2F7A24T7EE5YOMN4CE6GCHUTOUQXM", "amount": "983.0000005" }, { "asset": "EURT:GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S", "amount": "2378.0000005" } ], "shares_received": "1000" } ``` --- ## Manage Data Object Set, modify, or delete a data entry (name/value pair) for an account. See the [`Manage Data` errors](../../../errors/result-codes/operation-specific/manage-data.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - name - string - The key for this data entry. It can be up to 64 bytes long. If this is a new `Name`, it will add the given name/value pair to the account. If this `Name` is already present, then the associated value will be modified. - value - string - If present, then this value will be set in the DataEntry. It can be up to 64 bytes long. If not present, then the existing `Name` will be deleted. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/121957408846438401" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/1e1b8f628c338a0306cbcb512bd89473a0c6b25df67ad77cfc1478437a575665" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/121957408846438401/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=121957408846438401" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=121957408846438401" } }, "id": "121957408846438401", "paging_token": "121957408846438401", "transaction_successful": true, "source_account": "GCAXBKU3AKYJPLQ6PEJ6L47KOATCYCBJ2NFRGAK7FUUA2DCEUC265SU2", "type": "manage_data", "type_i": 10, "created_at": "2020-02-25T17:47:32Z", "transaction_hash": "1e1b8f628c338a0306cbcb512bd89473a0c6b25df67ad77cfc1478437a575665", "name": "config.memo_required", "value": "MQ==" } ``` --- ## Create Passive Sell Offer Object Creates an offer that will not consume a counter offer that exactly matches this offer. This is useful for offers meant to be 1:1 exchanges for path payments. Use Manage Sell Offer to manage this offer after using this operation to create it. See the [`Create Passive Sell Offer` errors](../../../errors/result-codes/operation-specific/create-passive-sell-offer.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - amount - string - The amount of `selling_asset` that the account making this offer is willing to sell. - price - string - How many units of `selling_asset` it takes to get 1 unit of `buying_asset`. A number representing the decimal form of `price_r`. - price_r - object - A precise representation of the buy and sell price of the assets on offer. - n - number - The numerator. - d - number - The denominator. - buying_asset_type - string - The type for the buying asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - buying_asset_issuer - string - The Stellar address of the buying asset’s issuer. Appears if the `buying_asset_type` is not `native`. - buying_asset_code - string - The code for the buying asset. Appears if the `buying_asset_type` is not `native`. - selling_asset_type - string - The type for the selling asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - selling_asset_issuer - string - The Stellar address of the selling asset’s issuer. Appears if the `selling_asset_type` is not `native`. - selling_asset_code - string - The code for the selling asset. Appears if the `selling_asset_type` is not `native`. - offer_id - string - A unique identifier for this offer. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/124895183656849409" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/20afb7f9613efe9e851579190c80758ee101550e85740f71274c3eb3f0cb0418" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/124895183656849409/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=124895183656849409" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=124895183656849409" } }, "id": "124895183656849409", "paging_token": "124895183656849409", "transaction_successful": true, "source_account": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA", "type": "create_passive_sell_offer", "type_i": 4, "created_at": "2020-04-08T14:28:15Z", "transaction_hash": "20afb7f9613efe9e851579190c80758ee101550e85740f71274c3eb3f0cb0418", "amount": "1.0000000", "price": "1.0000000", "price_r": { "n": 1, "d": 1 }, "buying_asset_type": "credit_alphanum4", "buying_asset_code": "USD", "buying_asset_issuer": "GBNLJIYH34UWO5YZFA3A3HD3N76R6DOI33N4JONUOHEEYZYCAYTEJ5AK", "selling_asset_type": "credit_alphanum4", "selling_asset_code": "USD", "selling_asset_issuer": "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX" } ``` --- ## Path Payment Strict Receive Object Sends a payment from one account to another in a path through the order books, starting as one asset and ending as another. Path payments that are `Strict Receive` designate the payment amount in the asset received. See the [`Path Payment Strict Receive` errors](../../../errors/result-codes/operation-specific/path-payment-strict-receive.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - asset_type - string - The type of asset being received. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - asset_code - string - The code for the asset being received. Appears if the `asset_type` is not `native`. - asset_issuer - string - The Stellar address of the issuer of the asset being received. Appears if the `asset_type` is not `native`. - from - string - The payment sender’s public key. - to - string - The payment recipient’s public key. - amount - string - Amount received designated in the destination asset. - path - array of objects - The intermediary assets that this path hops through. - asset_code - string - The code for this intermediary asset. - asset_issuer - string - The Stellar address of the intermediary asset’s issuer. - asset_type - string - The type for the intermediary asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - source_amount - string - Amount sent designated in the source asset. - source_max - string - The maximum amount to be sent designated in the source asset. - source_asset_type - string - The type for the source asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - source_asset_code - string - The code for the source asset. - source_asset_issuer - string - The Stellar address of the source asset’s issuer. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/124018825644490753" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/2624935eefedc195562623d982e501ba2a183382959fa0b9d03cf66dced3b332" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/124018825644490753/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=124018825644490753" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=124018825644490753" } }, "id": "124018825644490753", "paging_token": "124018825644490753", "transaction_successful": true, "source_account": "GBZH7S5NC57XNHKHJ75C5DGMI3SP6ZFJLIKW74K6OSMA5E5DFMYBDD2Z", "type": "path_payment_strict_receive", "type_i": 2, "created_at": "2020-03-26T19:33:55Z", "transaction_hash": "2624935eefedc195562623d982e501ba2a183382959fa0b9d03cf66dced3b332", "asset_type": "credit_alphanum4", "asset_code": "BRL", "asset_issuer": "GDVKY2GU2DRXWTBEYJJWSFXIGBZV6AZNBVVSUHEPZI54LIS6BA7DVVSP", "from": "GBZH7S5NC57XNHKHJ75C5DGMI3SP6ZFJLIKW74K6OSMA5E5DFMYBDD2Z", "to": "GBZH7S5NC57XNHKHJ75C5DGMI3SP6ZFJLIKW74K6OSMA5E5DFMYBDD2Z", "amount": "0.1000000", "path": [ { "asset_type": "credit_alphanum4", "asset_code": "USD", "asset_issuer": "GBUYUAI75XXWDZEKLY66CFYKQPET5JR4EENXZBUZ3YXZ7DS56Z4OKOFU" }, { "asset_type": "native" } ], "source_amount": "0.0198773", "source_max": "0.0198774", "source_asset_type": "credit_alphanum4", "source_asset_code": "USD", "source_asset_issuer": "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX" } ``` --- ## Path Payment Strict Send Object Sends a payment from one account to another in a path through the order books, starting as one asset and ending as another. Path payments that are `Strict Send` designate the payment amount in the asset sent. See the [`Path Payment Strict Send` errors](../../../errors/result-codes/operation-specific/path-payment-strict-send.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - asset_type - string - The type of asset being received. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - asset_code - string - The code for the asset being received. Appears if the `asset_type` is not `native`. - asset_issuer - string - The Stellar address of the issuer of the asset being received. Appears if the `asset_type` is not `native`. - from - string - The payment sender’s public key. - to - string - The payment recipient’s public key. - amount - string - Amount received designated in the destination asset. - path - array of objects - The intermediary assets that this path hops through. - asset_code - string - The code for this intermediary asset. - asset_issuer - string - The Stellar address of the intermediary asset’s issuer. - asset_type - string - The type for the intermediary asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - source_amount - string - Amount sent designated in the source asset. - destination_min - string - The minimum amount of destination asset expected to be received. - source_asset_type - string - The type for the source asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - source_asset_code - string - The code for the source asset. Appears if the `asset_type` is not `native`. - source_asset_issuer - string - The Stellar address of the source asset’s issuer. Appears if the `asset_type` is not `native`. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/124624072438579201" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/2b863994825fe85b80bfdff433b348d5ce80b23cd9ee2a56dcd6ee1abd52c9f8" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/124624072438579201/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=124624072438579201" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=124624072438579201" } }, "id": "124624072438579201", "paging_token": "124624072438579201", "transaction_successful": true, "source_account": "GBZH7S5NC57XNHKHJ75C5DGMI3SP6ZFJLIKW74K6OSMA5E5DFMYBDD2Z", "type": "path_payment_strict_send", "type_i": 13, "created_at": "2020-04-04T13:47:50Z", "transaction_hash": "2b863994825fe85b80bfdff433b348d5ce80b23cd9ee2a56dcd6ee1abd52c9f8", "asset_type": "credit_alphanum4", "asset_code": "BRL", "asset_issuer": "GDVKY2GU2DRXWTBEYJJWSFXIGBZV6AZNBVVSUHEPZI54LIS6BA7DVVSP", "from": "GBZH7S5NC57XNHKHJ75C5DGMI3SP6ZFJLIKW74K6OSMA5E5DFMYBDD2Z", "to": "GBZH7S5NC57XNHKHJ75C5DGMI3SP6ZFJLIKW74K6OSMA5E5DFMYBDD2Z", "amount": "26.5544244", "path": [ { "asset_type": "credit_alphanum4", "asset_code": "EURT", "asset_issuer": "GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S" }, { "asset_type": "native" } ], "source_amount": "5.0000000", "destination_min": "26.5544244", "source_asset_type": "credit_alphanum4", "source_asset_code": "USD", "source_asset_issuer": "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX" } ``` --- ## Restore Footprint Object [Restores](../../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#restore-footprint) archived entries and makes them accessible. ```json { "id": "1109896858714113", "paging_token": "1109896858714113", "transaction_successful": true, "source_account": "GBVAL7WM2G3NUTDIT5EYE4BMDRNLRQWI4RDGSVFCPXHPQRSSPEUQ2PNK", "type": "restore_footprint", "type_i": 26, "created_at": "2024-02-22T10:50:50Z", "transaction_hash": "1768d762bedf3129a00ec095d1aaed4c378dc3f55250758ed5008992b303303b" } ``` --- ## Revoke Sponsorship Revoke sponsorship of a ledger entry. - ATTRIBUTE - DATA TYPE - DESCRIPTION - account_id - string (optional) - The id of the account which is no longer sponsored. - claimable_balance_id - string (optional) - The id of the claimable balance which is no longer sponsored. - data_account_id - string (optional) - The id of the account whose data entry is no longer sponsored. - data_name - string (optional) - The name of the data entry which is no longer sponsored. - offer_id - string (optional) - The id of the offer which is no longer sponsored. - trustline_account_id - string (optional) - The id of the account whose trustline is no longer sponsored. - trustline_asset - string (optional) - The asset of the trustline which is no longer sponsored. - signer_account_id - string (optional) - The account id of the signer which is no longer sponsored. - signer_key - string (optional) - The type of the signer which is no longer sponsored. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/124922916260433921/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=124922916260433921" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=124922916260433921" } }, "id": "124922916260433921", "paging_token": "124922916260433921", "transaction_successful": true, "source_account": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA", "type": "revoke_sponsorship", "type_i": 19, "created_at": "2020-04-09T00:14:11Z", "transaction_hash": "f94c338370839a598753221714de0b0193d4fc56ea369db6efe88f18669cc5a1", "account_id": "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA" } ``` --- ## Manage Sell Offer Object Creates, updates, or deletes a sell offer to trade assets. A sell offer specifies a certain amount of the selling asset that should be sold in exchange for the maximum quantity of the buying asset. See the [`Manage Sell Offer` errors](../../../errors/result-codes/operation-specific/manage-sell-offer.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - amount - string - The amount of `selling_asset` that the account making this offer is willing to sell. - price - string - How many units of `selling_asset` it takes to get 1 unit of `buying_asset`. A number representing the decimal form of `price_r`. - price_r - object - A precise representation of the buy and sell price of the assets on offer. - n - number - The numerator. - d - number - The denominator. - buying_asset_type - string - The type for the buying asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - buying_asset_issuer - string - The Stellar address of the buying asset’s issuer. Appears if the `buying_asset_type` is not `native`. - buying_asset_code - string - The code for the buying asset. Appears if the `buying_asset_type` is not `native`. - selling_asset_type - string - The type for the selling asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - selling_asset_issuer - string - The Stellar address of the selling asset’s issuer. Appears if the `selling_asset_type` is not `native`. - selling_asset_code - string - The code for the selling asset. Appears if the `selling_asset_type` is not `native`. - offer_id - string - A unique identifier for this offer. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/124892722640347138" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/ef8ffb54ff5990a686fda3ebfc07b8162f042ff0fcdb4f7ff141531e386f0a18" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/124892722640347138/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=124892722640347138" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=124892722640347138" } }, "id": "124892722640347138", "paging_token": "124892722640347138", "transaction_successful": true, "source_account": "GCM4PT6XDZBWOOENDS6FOU22GJQLJPV2GC7VRVII4TFGZBA3ZXNM55SV", "type": "manage_sell_offer", "type_i": 3, "created_at": "2020-04-08T13:36:39Z", "transaction_hash": "ef8ffb54ff5990a686fda3ebfc07b8162f042ff0fcdb4f7ff141531e386f0a18", "amount": "1336.0326986", "price": "0.0559999", "price_r": { "n": 559999, "d": 10000000 }, "buying_asset_type": "credit_alphanum4", "buying_asset_code": "USD", "buying_asset_issuer": "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", "selling_asset_type": "native", "offer_id": "0" } ``` --- ## Set Options Object Sets an account's flags, inflation destination, signers, and home domain. See the [`Set Options` errors](../../../errors/result-codes/operation-specific/set-options.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - signer_key - string - The public key of the new signer. - signer_weight - number - The weight of the new signer. Can range from `1` to `255`. - master_key_weight - number - The weight of the master key. Can range from `1` to `255`. - low_threshold - number - The sum weight for the low threshold. - med_threshold - number - The sum weight for the medium threshold. - high_threshold - number - The sum weight for the high threshold. - home_domain - string - The home domain used for stellar.toml file discovery. - set_flags - array - The array of numeric values of flags that has been set in this operation. Options include `1` for `AUTH_REQUIRED_FLAG`, `2` for `AUTH_REVOCABLE_FLAG`, and `4` for `AUTH_IMMUTABLE_FLAG`. - set_flags_s - array - The array of string values of flags that has been set in this operation. Options include `AUTH_REQUIRED_FLAG`, `AUTH_REVOCABLE_FLAG`, and `AUTH_IMMUTABLE_FLAG`. - clear_flags - array - The array of numeric values of flags that has been cleared in this operation. Options include `1` for `AUTH_REQUIRED_FLAG`, `2` for `AUTH_REVOCABLE_FLAG`, and `4` for `AUTH_IMMUTABLE_FLAG`. - clear_flags_s - array - The array of string values of flags that has been cleared in this operation. Options include `AUTH_REQUIRED_FLAG`, `AUTH_REVOCABLE_FLAG`, and `AUTH_IMMUTABLE_FLAG`. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/102125410241826819" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/e020277cf755a1c29234d34f123f546a2c4805d7b4ca9303e253667b0ff4d846" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/102125410241826819/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=102125410241826819" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=102125410241826819" } }, "id": "102125410241826819", "paging_token": "102125410241826819", "transaction_successful": true, "source_account": "GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ", "type": "set_options", "type_i": 5, "created_at": "2019-05-08T21:20:34Z", "transaction_hash": "e020277cf755a1c29234d34f123f546a2c4805d7b4ca9303e253667b0ff4d846", "home_domain": "www.stellar.org" } ``` --- ## Payments Payments are objects that represent balance transfer from one address to another. Payments are submitted to the Stellar network grouped in a Transaction. | | | | --- | -------------------------------------------- | | GET | [/payments](../../list-all-payments.api.mdx) | --- ## The Payment Object Sends an amount in a specific asset to a destination account. See the [`Payment` errors](../../errors/result-codes/operation-specific/payment.mdx). - ATTRIBUTE - DATA TYPE - DESCRIPTION - asset_type - string - The type of asset being sent. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - asset_code - string - The code for the asset being sent. Appears if the `asset_type` is not `native`. - asset_issuer - string - The Stellar address of the issuer of the asset being sent. Appears if the `asset_type` is not `native`. - from - string - The payment sender’s public key. - to - string - The payment recipient’s public key. - amount - string - Amount sent. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/122511124621283329" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/452a180790caf4dbe658d996316cd727ce5573f5f0a77790da540cc49214fe80" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/122511124621283329/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=122511124621283329" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=122511124621283329" } }, "id": "122511124621283329", "paging_token": "122511124621283329", "transaction_successful": true, "source_account": "GCAXBKU3AKYJPLQ6PEJ6L47KOATCYCBJ2NFRGAK7FUUA2DCEUC265SU2", "type": "payment", "type_i": 1, "created_at": "2020-03-04T22:46:47Z", "transaction_hash": "452a180790caf4dbe658d996316cd727ce5573f5f0a77790da540cc49214fe80", "asset_type": "credit_alphanum4", "asset_code": "NGNT", "asset_issuer": "GAWODAROMJ33V5YDFY3NPYTHVYQG7MJXVJ2ND3AOGIHYRWINES6ACCPD", "from": "GCAXBKU3AKYJPLQ6PEJ6L47KOATCYCBJ2NFRGAK7FUUA2DCEUC265SU2", "to": "GC2QCKFI3DOBEYVBONPVNA2PMLU225IKKI6XPENMWR2CTWSFBAOU7T34", "amount": "5.0000000" } ``` --- ## Trades When an offer is fully or partially fulfilled, a trade happens. Trades can also be caused by successful path payments, because path payments involve fulfilling offers. A trade occurs between two parties—`base` and `counter`. Which is which is either arbitrary or determined by the calling query. Learn more about [trades](../../../../../../learn/glossary.mdx#decentralized-exchange). | | | | --- | --------------------------------------- | | GET | [/trades](../../get-all-trades.api.mdx) | --- ## The Trade Object When Horizon returns information about a trade, it uses the following format: - ATTRIBUTE - DATA TYPE - DESCRIPTION - id - string - A unique identifier for this trade. - paging_token - number - A cursor value for use in [pagination](../../structure/pagination/README.mdx). - ledger_close_time - string - An ISO 8601 formatted string of when the ledger with this trade was closed. - base_account - string (optional) - The account ID of the base party for this trade. - base_offer_id - string (optional) - The base offer ID. If this offer was immediately and fully consumed, this will be a synethic ID. - base_liquidity_pool_id - string (optional) - The base liquidity pool ID. If this trade was executed against a liquidity pool. - base_amount - string - The amount of the base asset that was moved from `base_account` to `counter_account`. - base_asset_type - string - The type for the base asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - base_asset_code - string - The code for the base asset. - base_asset_issuer - string - The Stellar address of the base asset’s issuer. - counter_account - string (optional) - The account ID of the base party for this trade. - counter_offer_id - string (optional) - The counter offer ID. If this offer was immediately and fully consumed, this will be a synethic ID. - counter_liquidity_pool_id - string (optional) - The counter liquidity pool ID. If this trade was executed against a liquidity pool. - counter_amount - string - The amount of the counter asset that was moved from `counter_account` to `base_account`. - counter_asset_type - string - The type for the counter asset. Either `native`, `credit_alphanum4`, or `credit_alphanum12`. - counter_asset_code - string - The code for the counter asset. - counter_asset_issuer - string - The Stellar address of the counter asset’s issuer. - price - object - An object of a number numerator and number denominator that represents the original offer price. To derive the price, divide `n` by `d`. `n` and `d` are represented as strings because they might be 64-bit integers. - n - string - The numerator. - d - string - The denominator. - base_is_seller - boolean - Indicates with party is the seller. ```json { "_links": { "self": { "href": "" }, "base": { "href": "https://horizon-testnet.stellar.org/accounts/GA23DVJUJVXUQ45SKQMZR7KH2ZOOBFWGWSEXHCT7VVKP2TYIMCQTQGNQ" }, "counter": { "href": "https://horizon-testnet.stellar.org/accounts/GAFGG7CRFRCJLBHGI5L4IZD4QYR4GDX5NKB46CE4KZMNBILBTP3L4M75" }, "operation": { "href": "https://horizon-testnet.stellar.org/operations/100089067462524929" } }, "id": "100089067462524929-0", "paging_token": "100089067462524929-0", "ledger_close_time": "2019-04-07T11:30:03Z", "offer_id": "79502917", "base_offer_id": "4711775085889912833", "base_account": "GA23DVJUJVXUQ45SKQMZR7KH2ZOOBFWGWSEXHCT7VVKP2TYIMCQTQGNQ", "base_amount": "99.9999996", "base_asset_type": "native", "counter_offer_id": "79502917", "counter_account": "GAFGG7CRFRCJLBHGI5L4IZD4QYR4GDX5NKB46CE4KZMNBILBTP3L4M75", "counter_amount": "11.4722884", "counter_asset_type": "credit_alphanum4", "counter_asset_code": "EURT", "counter_asset_issuer": "GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S", "base_is_seller": false, "price": { "n": "10000000", "d": "87166567" } } ``` --- ## Transactions(Transactions) :::info Transaction meta `result_meta_xdr` will be removed from the SDF hosted Horizon API (`horizon.stellar.org`) in Q3 2024. Instead of using Horizon to access transaction metadata, developers could access it with the [`getTransactions`](../../../../rpc/api-reference/methods/getTransactions.mdx) endpoint in the Stellar RPC. ::: Transactions are commands that modify the ledger state and consist of one or more operations. Learn more about [transactions](../../../../../../learn/glossary.mdx#transaction). | | | | --- | --- | | GET | [/transactions/:transaction_id](../../retrieve-a-transaction.api.mdx) | | GET | [/transactions/:transaction_id/operations](../../retrieve-a-transactions-operations.api.mdx) | | GET | [/transactions/:transaction_id/effects](../../retrieve-a-transactions-effects.api.mdx) | | GET | [/transactions](../../list-all-transactions.api.mdx) | | POST | [/transactions](../../submit-a-transaction.api.mdx) | | POST | [/transactions_async](../../submit-async-transaction.api.mdx) | --- ## The Transaction Object :::info Transaction meta `result_meta_xdr` will be removed from the SDF hosted Horizon API (`horizon.stellar.org`) in Q3 2024. Instead of using Horizon to access transaction metadata, developers could access it with the [`getTransactions`](../../../../rpc/api-reference/methods/getTransactions.mdx) endpoint in the Stellar RPC. ::: When Horizon returns information about a transaction, it uses the following format: - ATTRIBUTE - DATA TYPE - DESCRIPTION - id - string - A unique identifier for this transaction. - paging_token - number - A cursor value for use in [pagination](../../structure/pagination/README.mdx). - successful - boolean - Indicates if this transaction was successful or not. - hash - string - A hex-encoded SHA-256 hash of this transaction’s [XDR](../../../../../../learn/fundamentals/data-format/xdr.mdx)-encoded form. - ledger - number - The sequence number of the ledger that this transaction was included in. - created_at - ISO8601 string - The date this transaction was created. - source_account - string - The account that originates the transaction. - source_account_sequence - string - The source account's sequence number that this transaction consumed. - fee_charged - number - The fee (in [stroops](../../../../../../learn/fundamentals/lumens.mdx)) paid by the source account to apply this transaction to the ledger. - max_fee - number - The maximum fee (in [stroops](../../../../../../learn/fundamentals/lumens.mdx)) that the source account was willing to pay. - operation_count - number - The number of operations contained within this transaction. - envelope_xdr - string - A base64 encoded string of the raw `TransactionEnvelope` XDR struct for this transaction. - result_xdr - string - A base64 encoded string of the raw `TransactionResult` XDR struct for this transaction. - result_meta_xdr - string - **_[To be deprecated in Q3]_** A base64 encoded string of the raw `TransactionMeta` XDR struct for this transaction - fee_meta_xdr - string - A base64 encoded string of the raw `LedgerEntryChanges` XDR struct produced by taking fees for this transaction. - memo - string - The optional memo attached to a transaction. - memo_type - string - The type of memo. Potential values include `MEMO_TEXT`, `MEMO_ID`, `MEMO_HASH`, `MEMO_RETURN`. - signatures - string - An array of signatures used to sign this transaction. - preconditions - object - A set of transaction preconditions affecting its validity. - time_bounds - object - The time range for which this transaction is valid, with bounds as unsigned 64-bit UNIX timestamps - min_time - string - the lower bound - max_time - string - the upper bound - ledger_bounds - object - The ledger range for which this transaction is valid, as unsigned 32-bit integers. - min_ledger - number - the lower bound - max_ledger - number - the upper bound - min_account_sequence - string - Containing a positive, signed 64-bit integer representing the lowest source account sequence number for which the transaction is valid. - min_account_sequence_age - number - The minimum duration of time (in seconds as an unsigned 64-bit integer) that must have passed since the source account's sequence number changed for the transaction to be valid. - min_account_sequence_ledger_gap - number - An unsigned 32-bit integer representing the minimum number of ledgers that must have closed since the source account's sequence number changed for the transaction to be valid. - extra_signers - array of strings - The list of up to two additional signers that must have corresponding signatures for this transaction to be valid. ```json { "memo": "298424", "_links": { "self": { "href": "https://horizon-testnet.stellar.org/transactions/132c440e984ab97d895f3477015080aafd6c4375f6a70a87327f7f95e13c4e31" }, "account": { "href": "https://horizon-testnet.stellar.org/accounts/GCO2IP3MJNUOKS4PUDI4C7LGGMQDJGXG3COYX3WSB4HHNAHKYV5YL3VC" }, "ledger": { "href": "https://horizon-testnet.stellar.org/ledgers/27956256" }, "operations": { "href": "https://horizon-testnet.stellar.org/transactions/132c440e984ab97d895f3477015080aafd6c4375f6a70a87327f7f95e13c4e31/operations{?cursor,limit,order}", "templated": true }, "effects": { "href": "https://horizon-testnet.stellar.org/transactions/132c440e984ab97d895f3477015080aafd6c4375f6a70a87327f7f95e13c4e31/effects{?cursor,limit,order}", "templated": true }, "precedes": { "href": "https://horizon-testnet.stellar.org/transactions?order=asc\u0026cursor=120071205238677504" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/transactions?order=desc\u0026cursor=120071205238677504" } }, "id": "132c440e984ab97d895f3477015080aafd6c4375f6a70a87327f7f95e13c4e31", "paging_token": "120071205238677504", "successful": true, "hash": "132c440e984ab97d895f3477015080aafd6c4375f6a70a87327f7f95e13c4e31", "ledger": 27956256, "created_at": "2020-01-27T22:13:17Z", "source_account": "GCO2IP3MJNUOKS4PUDI4C7LGGMQDJGXG3COYX3WSB4HHNAHKYV5YL3VC", "source_account_sequence": "64034663849209932", "fee_charged": 100, "max_fee": 100, "operation_count": 1, "envelope_xdr": "AAAAAJ2kP2xLaOVLj6DRwX1mMyA0mubYnYvu0g8OdoDqxXuFAAAAZADjfzAACzBMAAAAAQAAAAAAAAAAAAAAAF4vYIYAAAABAAAABjI5ODQyNAAAAAAAAQAAAAAAAAABAAAAAKdeYELovtcnTxqPEVsdbxHLMoMRalZsK7lo/+3ARzUZAAAAAAAAAADUFJPYAAAAAAAAAAHqxXuFAAAAQBpLpQyh+mwDd5nDSxTaAh5wopBBUaSD1eOK9MdiO+4kWKVTqSr/Ko3kYE/+J42Opsewf81TwINONPbY2CtPggE=", "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", "result_meta_xdr": "AAAAAQAAAAIAAAADAaqUIAAAAAAAAAAAnaQ/bEto5UuPoNHBfWYzIDSa5tidi+7SDw52gOrFe4UAAkRg8uGCXADjfzAACzBLAAAAAAAAAAEAAAAAnaQ/bEto5UuPoNHBfWYzIDSa5tidi+7SDw52gOrFe4UAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAaqUIAAAAAAAAAAAnaQ/bEto5UuPoNHBfWYzIDSa5tidi+7SDw52gOrFe4UAAkRg8uGCXADjfzAACzBMAAAAAAAAAAEAAAAAnaQ/bEto5UuPoNHBfWYzIDSa5tidi+7SDw52gOrFe4UAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMBqoicAAAAAAAAAACnXmBC6L7XJ08ajxFbHW8RyzKDEWpWbCu5aP/twEc1GQAAAAAAmwWMAaVkEgAAAC0AAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEBqpQgAAAAAAAAAACnXmBC6L7XJ08ajxFbHW8RyzKDEWpWbCu5aP/twEc1GQAAAADUr5lkAaVkEgAAAC0AAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMBqpQgAAAAAAAAAACdpD9sS2jlS4+g0cF9ZjMgNJrm2J2L7tIPDnaA6sV7hQACRGDy4YJcAON/MAALMEwAAAAAAAAAAQAAAACdpD9sS2jlS4+g0cF9ZjMgNJrm2J2L7tIPDnaA6sV7hQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEBqpQgAAAAAAAAAACdpD9sS2jlS4+g0cF9ZjMgNJrm2J2L7tIPDnaA6sV7hQACRGAezO6EAON/MAALMEwAAAAAAAAAAQAAAACdpD9sS2jlS4+g0cF9ZjMgNJrm2J2L7tIPDnaA6sV7hQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", "fee_meta_xdr": "AAAAAgAAAAMBqpQYAAAAAAAAAACdpD9sS2jlS4+g0cF9ZjMgNJrm2J2L7tIPDnaA6sV7hQACRGDy4YLAAON/MAALMEsAAAAAAAAAAQAAAACdpD9sS2jlS4+g0cF9ZjMgNJrm2J2L7tIPDnaA6sV7hQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEBqpQgAAAAAAAAAACdpD9sS2jlS4+g0cF9ZjMgNJrm2J2L7tIPDnaA6sV7hQACRGDy4YJcAON/MAALMEsAAAAAAAAAAQAAAACdpD9sS2jlS4+g0cF9ZjMgNJrm2J2L7tIPDnaA6sV7hQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", "memo_type": "text", "signatures": [ "GkulDKH6bAN3mcNLFNoCHnCikEFRpIPV44r0x2I77iRYpVOpKv8qjeRgT/4njY6mx7B/zVPAg0409tjYK0+CAQ==" ] } ``` --- ## Retrieve a Claimable Balance The single claimable balance endpoint provides information on a claimable balance. Request --- ## Retrieve a Ledger The single ledger endpoint provides information on a specific [ledger](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/ledgers). Request --- ## Retrieve a Ledgers's Effects This endpoint returns the effects of a specific ledger. Request --- ## Retrieve a Ledger's Operations This endpoint returns successful operations in a specific ledger. Request --- ## Retrieve a Ledger's Payments This endpoint returns all payment-related operations in a specific ledger. Operation types that can be returned by this endpoint include: create_account, payment, path_payment, and account_merge. Request --- ## Retrieve a Ledger's Transactions This endpoint represents successful transactions in a given ledger. Request --- ## Retrieve a Liquidity Pool The single liquidity pool endpoint provides information on a liquidity pool. Request --- ## Retrieve a Transaction The single transaction endpoint provides information on a specific transaction. Request --- ## Retrieve a Transaction's Effects This endpoint returns the effects of a specific transaction. Request --- ## Retrieve a Transaction's Operations This endpoint returns Successful operations for a specific transaction. Request --- ## Retrieve a Transaction's Payments This endpoint returns the payments of a specific transaction. Request --- ## Retrieve an Account The single account endpoint provides information on a specific account. The balances section in the response will also list all the trustlines this account has established, including trustlines that haven’t been authorized yet. Request --- ## Retrieve an Operation The single operation endpoint provides information about a specific operation. Request --- ## Retrieve an Operation's Effects This endpoint returns the effects of a specific operation. Request --- ## Retrieve an Order Book The order book endpoint provides an order book's bids and asks and can be used in [streaming](https://developers.stellar.org/docs/data/apis/horizon/api-reference/structure/streaming) mode. When filtering for a specific order book, you must use use all six of these arguments: `base_asset_type`, `base_asset_issuer`, `base_asset_code`, `counter_asset_type`, `counter_asset_issuer`, and `counter_asset_code`. If the base or counter asset is XLM, you only need to indicate the asset type as `native` and do not need to designate the code or the issuer. Request --- ## Retrieve Fee Stats The fee stats endpoint provides information about per-operation fee stats over the last 5 ledgers. --- ## Retrieve Related Effects This endpoint represents effects referencing a given liquidity pool and can be used in streaming mode. Streaming mode allows you to listen for new effects referencing this liquidity pool as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known effect unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream effects created since your request time. Request --- ## Retrieve Related Trades This endpoint represents successful trades fulfilled by the given liquidity pool and can be used in streaming mode. Streaming mode allows you to listen for new trades referencing this liquidity pool as they are added to the Stellar ledger. If called in streaming mode, Horizon will start at the earliest known trade unless a cursor is set, in which case it will start from that cursor. By setting the cursor value to now, you can stream trade created since your request time. Request --- ## Horizon API Reference How Horizon is structured. --- ## Consistency For endpoints which serve data which can change from ledger to ledger (for example an account balance), Horizon includes a `Latest-Ledger` HTTP header in its response. The value of the `Latest-Ledger` HTTP header is the sequence number of the latest ledger known to Horizon at the time the request was processed. Horizon will guarantee that all the data included in the response is consistent with that ledger. This mechanism prevents race conditions where a request is processed at the boundary of two ledgers and ensures that the response is consistent with the ledger included in the `Latest-Ledger` HTTP header. --- ## Pagination To make it possible to explore the millions of records for resources like transactions and operations, Horizon paginates the data it returns for collection-based endpoints. Each individual transaction, operation, ledger, etc. is returned as a record, and a group of records is called a collection. Records are returned as an array under the `_embedded` attribute. To move between pages of a collection of records, use the links in the `next` and `prev` attributes nested under the top-level `_links` attribute. - ATTRIBUTE - DATA TYPE - DESCRIPTION - \_links - array - Provides links for navigating to other pages. - \_links.self - array - An `href` key with a link to the response itself as the value. - \_links.next - array - An `href` key with a link to the next page for this endpoint as the value. - \_links.prev - array - An `href` key with a link to the next page for this endpoint as the value. - \_embedded - array - An `href` key with a link to the next page for this endpoint as the value. - \_embedded.records - array - Returns an array of records. ```js var StellarSdk = require("@stellar/stellar-sdk"); var server = new StellarSdk.Horizon.Server( "https://horizon-testnet.stellar.org", ); server .transactions() .call() .then(function (resp) { // page 1 console.log(resp); return resp.next(); }) .then(function (resp) { // page 2 console.log(resp); }) .catch(function (err) { console.error(err); }); ``` ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/transactions/2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906/operations?cursor=&limit=5&order=asc" }, "next": { "href": "https://horizon-testnet.stellar.org/transactions/2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906/operations?cursor=113928152169844741&limit=5&order=asc" }, "prev": { "href": "https://horizon-testnet.stellar.org/transactions/2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906/operations?cursor=113928152169844737&limit=5&order=desc" } }, "_embedded": { "records": [ { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/113928152169844737" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/113928152169844737/effects" } }, "id": "113928152169844737", "paging_token": "113928152169844737", "transaction_successful": true, "source_account": "GDO2BIMNH7T6MOJVPKEJHWAGMYGOQU5QMK5BKT5XMVWPZKHAGA4JNAQZ", "type": "manage_buy_offer", "type_i": 12, "created_at": "2019-10-28T19:44:09Z", "transaction_hash": "2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906", "amount": "0.0000000", "price": "0.0001000", "price_r": { "n": 1, "d": 10000 }, "buying_asset_type": "native", "selling_asset_type": "credit_alphanum4", "selling_asset_code": "SLT", "selling_asset_issuer": "GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP", "offer_id": 126229445 }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/113928152169844738" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/113928152169844738/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=113928152169844738" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=113928152169844738" } }, "id": "113928152169844738", "paging_token": "113928152169844738", "transaction_successful": true, "source_account": "GDO2BIMNH7T6MOJVPKEJHWAGMYGOQU5QMK5BKT5XMVWPZKHAGA4JNAQZ", "type": "manage_buy_offer", "type_i": 12, "created_at": "2019-10-28T19:44:09Z", "transaction_hash": "2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906", "amount": "0.0000000", "price": "0.0001000", "price_r": { "n": 1, "d": 10000 }, "buying_asset_type": "native", "selling_asset_type": "credit_alphanum4", "selling_asset_code": "SLT", "selling_asset_issuer": "GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP", "offer_id": 126229446 }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/113928152169844739" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/113928152169844739/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=113928152169844739" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=113928152169844739" } }, "id": "113928152169844739", "paging_token": "113928152169844739", "transaction_successful": true, "source_account": "GDO2BIMNH7T6MOJVPKEJHWAGMYGOQU5QMK5BKT5XMVWPZKHAGA4JNAQZ", "type": "manage_buy_offer", "type_i": 12, "created_at": "2019-10-28T19:44:09Z", "transaction_hash": "2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906", "amount": "0.0000000", "price": "0.0001000", "price_r": { "n": 1, "d": 10000 }, "buying_asset_type": "native", "selling_asset_type": "credit_alphanum4", "selling_asset_code": "SLT", "selling_asset_issuer": "GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP", "offer_id": 126229447 }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/113928152169844740" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/113928152169844740/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=113928152169844740" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=113928152169844740" } }, "id": "113928152169844740", "paging_token": "113928152169844740", "transaction_successful": true, "source_account": "GDO2BIMNH7T6MOJVPKEJHWAGMYGOQU5QMK5BKT5XMVWPZKHAGA4JNAQZ", "type": "manage_buy_offer", "type_i": 12, "created_at": "2019-10-28T19:44:09Z", "transaction_hash": "2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906", "amount": "0.0000000", "price": "0.0001000", "price_r": { "n": 1, "d": 10000 }, "buying_asset_type": "native", "selling_asset_type": "credit_alphanum4", "selling_asset_code": "SLT", "selling_asset_issuer": "GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP", "offer_id": 126229448 }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/113928152169844741" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/113928152169844741/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=113928152169844741" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=113928152169844741" } }, "id": "113928152169844741", "paging_token": "113928152169844741", "transaction_successful": true, "source_account": "GDO2BIMNH7T6MOJVPKEJHWAGMYGOQU5QMK5BKT5XMVWPZKHAGA4JNAQZ", "type": "manage_buy_offer", "type_i": 12, "created_at": "2019-10-28T19:44:09Z", "transaction_hash": "2a09c3d79027721f2b8a78e2a936cbeda484bfe98a6e34856c1fee743b7e8906", "amount": "0.0000000", "price": "0.0001000", "price_r": { "n": 1, "d": 10000 }, "buying_asset_type": "native", "selling_asset_type": "credit_alphanum4", "selling_asset_code": "SLT", "selling_asset_issuer": "GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP", "offer_id": 126229449 } ] } } ``` --- ## Page Arguments | | | | --- | ------------------------------------------------------------------ | | GET | `/{endpoint}?cursor={paging_token}&order={asc,desc}&limit={1-200}` | - ARGUMENT - REQUIRED? - DESCRIPTION - cursor - optional - A number that points to a specific location in a collection of responses and is pulled from the `paging_token` value of a record. - order - optional - A designation of the order in which records should appear. Options include `asc`(ascending) or `desc` (descending). If this argument isn’t set, it defaults to `asc`. - limit - optional - The maximum number of records returned. The limit can range from 1 to 200 - an upper limit that is hardcoded in Horizon for performance reasons. If this argument isn’t designated, it defaults to 10. ```curl curl "https://horizon-testnet.stellar.org/ledgers/26478723/operations?cursor=113725249324879872&limit=5&order=asc" ``` ```js var StellarSdk = require("@stellar/stellar-sdk"); var server = new StellarSdk.Horizon.Server( "https://horizon-testnet.stellar.org", ); server .operations() .forLedger("26478723") .cursor("113725249324879872") .limit(5) .order("asc") .call() .then(function (resp) { console.log(resp); }) .catch(function (err) { console.error(err); }); ``` ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/ledgers/26478723/operations?cursor=113725249324879872\u0026limit=5\u0026order=asc" }, "next": { "href": "https://horizon-testnet.stellar.org/ledgers/26478723/operations?cursor=113725249324916737\u0026limit=5\u0026order=asc" }, "prev": { "href": "https://horizon-testnet.stellar.org/ledgers/26478723/operations?cursor=113725249324879873\u0026limit=5\u0026order=desc" } }, "_embedded": { "records": [ { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/113725249324879873" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/6adab48bbc0a38b9d40938b63a8ae0f5b334948c2d5acfb755dea616f98720d1" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/113725249324879873/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=113725249324879873" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=113725249324879873" } }, "id": "113725249324879873", "paging_token": "113725249324879873", "transaction_successful": true, "source_account": "GCRGHKY6RBFVQLF2JCHB7TK7A5BIABITFKVIEOXK4BPEIDE446OEFYXZ", "type": "manage_offer", "type_i": 3, "created_at": "2019-10-25T20:45:51Z", "transaction_hash": "6adab48bbc0a38b9d40938b63a8ae0f5b334948c2d5acfb755dea616f98720d1", "amount": "5048.0792092", "price": "0.0000079", "price_r": { "n": 79, "d": 10000000 }, "buying_asset_type": "credit_alphanum4", "buying_asset_code": "BTC", "buying_asset_issuer": "GATEMHCCKCY67ZUCKTROYN24ZYT5GK4EQZ65JJLDHKHRUZI3EUEKMTCH", "selling_asset_type": "native", "offer_id": 125121197 }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/113725249324888065" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/c4de60af4815d94b6f3aa9947403f96dd0e8c3ba7d84eccce9c2c798470381ed" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/113725249324888065/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=113725249324888065" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=113725249324888065" } }, "id": "113725249324888065", "paging_token": "113725249324888065", "transaction_successful": true, "source_account": "GCFKS7EBBZQTBBM7HHOASGZBE3L2TADWG6OJBVJXYIS44LQFXFL766UB", "type": "manage_offer", "type_i": 3, "created_at": "2019-10-25T20:45:51Z", "transaction_hash": "c4de60af4815d94b6f3aa9947403f96dd0e8c3ba7d84eccce9c2c798470381ed", "amount": "103.0721239", "price": "0.0655000", "price_r": { "n": 131, "d": 2000 }, "buying_asset_type": "credit_alphanum4", "buying_asset_code": "USD", "buying_asset_issuer": "GDSRCV5VTM3U7Y3L6DFRP3PEGBNQMGOWSRTGSBWX6Z3H6C7JHRI4XFJP", "selling_asset_type": "native", "offer_id": 0 }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/113725249324892161" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/6d0fe444dd346e05742776305f5e90dd102bd83dfa00a54cfd79bd94753beba0" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/113725249324892161/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=113725249324892161" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=113725249324892161" } }, "id": "113725249324892161", "paging_token": "113725249324892161", "transaction_successful": true, "source_account": "GB44WLYD6HQXZHLRCPARIAMBASN35TDHXU5Q3I3DRHQMLLB3CBMIZLY5", "type": "manage_buy_offer", "type_i": 12, "created_at": "2019-10-25T20:45:51Z", "transaction_hash": "6d0fe444dd346e05742776305f5e90dd102bd83dfa00a54cfd79bd94753beba0", "amount": "2942.0268642", "price": "6.7000000", "price_r": { "n": 67, "d": 10 }, "buying_asset_type": "credit_alphanum4", "buying_asset_code": "SLT", "buying_asset_issuer": "GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP", "selling_asset_type": "native", "offer_id": 0 }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/113725249324908545" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/36907ac7f802a079fa1d7e7fddeb31decbed1218b47924243daaa508af8b1bbe" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/113725249324908545/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=113725249324908545" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=113725249324908545" } }, "id": "113725249324908545", "paging_token": "113725249324908545", "transaction_successful": true, "source_account": "GBVI7F7QBE3ZEHP7HUAXAEKENITQKCXR5CML5JMOCHJN6EOQXLTSJYJI", "type": "payment", "type_i": 1, "created_at": "2019-10-25T20:45:51Z", "transaction_hash": "36907ac7f802a079fa1d7e7fddeb31decbed1218b47924243daaa508af8b1bbe", "asset_type": "credit_alphanum4", "asset_code": "TFC", "asset_issuer": "GDS3XDJAA4VY6MJYASIGSIMPHZ7AQNZ54RKLWT7MWCOU5YKYEVCNLVS3", "from": "GBVI7F7QBE3ZEHP7HUAXAEKENITQKCXR5CML5JMOCHJN6EOQXLTSJYJI", "to": "GAMKXMT23OMMOFJMZIHT5T3C65JI5YGAOHJMDYBBS4JJBB3X2CVLT3GO", "amount": "0.0380000" }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/113725249324916737" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/785058d3363b448c95943a40230333b8d523607c9617089bb17c9202a2e7384a" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/113725249324916737/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=113725249324916737" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=113725249324916737" } }, "id": "113725249324916737", "paging_token": "113725249324916737", "transaction_successful": true, "source_account": "GDD7ABRF7BCK76W33RXDQG5Q3WXVSQYVLGEMXSOWRGZ6Z3G3M2EM2TCP", "type": "manage_offer", "type_i": 3, "created_at": "2019-10-25T20:45:51Z", "transaction_hash": "785058d3363b448c95943a40230333b8d523607c9617089bb17c9202a2e7384a", "amount": "0.0000000", "price": "1.0000000", "price_r": { "n": 1, "d": 1 }, "buying_asset_type": "native", "selling_asset_type": "credit_alphanum4", "selling_asset_code": "BTC", "selling_asset_issuer": "GBVOL67TMUQBGL4TZYNMY3ZQ5WGQYFPFD5VJRWXR72VA33VFNL225PL5", "offer_id": 125126043 } ] } } ``` --- ## Rate Limiting Horizon rate limits on a per-IP-address basis. It can be configured via the option `PER_HOUR_RATE_LIMIT` and defaults to 3600 requests per hour. It is recommended that operators of Horizon tune this value based on their individual infrastructural capabilities and usage needs. This limit can be disabled entirely by setting the parameter to `0`. When a client exceeds this limit, Horizon will return a `429 Too Many Requests` error. While streaming, each update of the stream counts as a request and against a client’s allotted rate limit. --- ## Response Format Horizon delivers responses as JSON objects formatted according to [HAL](https://en.wikipedia.org/wiki/Hypertext_Application_Language). The HAL format makes Horizon more explorable, paginates responses, and connects parent and child resources. Consuming this format is simple using one of the many [open source libraries available](https://github.com/mikekelly/hal_specification/wiki/Libraries) for most major programming languages. HAL is just JSON with two reserved attribute names: - `_links` - `_embedded` If a response is a single record, the `_links` section will provide links to any parent or child records, and there will be no `_embedded` property. If a response is a collection, the `_links` section will provide [pagination](./pagination/README.mdx) links, and the response’s list of records will be nested underneath the `_embedded` property. - ATTRIBUTES - DATA TYPE - DESCRIPTION - \_links - array - Provides links for navigating to other pages or to parents and children. - \_embedded - array - Present when querying an endpoint that responds with a collection of records. ```json { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/accounts/GCRGHKY6RBFVQLF2JCHB7TK7A5BIABITFKVIEOXK4BPEIDE446OEFYXZ/payments?cursor=&limit=5&order=asc" }, "next": { "href": "https://horizon-testnet.stellar.org/accounts/GCRGHKY6RBFVQLF2JCHB7TK7A5BIABITFKVIEOXK4BPEIDE446OEFYXZ/payments?cursor=111764193027313665&limit=5&order=asc" }, "prev": { "href": "https://horizon-testnet.stellar.org/accounts/GCRGHKY6RBFVQLF2JCHB7TK7A5BIABITFKVIEOXK4BPEIDE446OEFYXZ/payments?cursor=111720727958269953&limit=5&order=desc" } }, "_embedded": { "records": [ { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/111720727958269953" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/4034838d5b47e4f8c23776faca4d9403637b1f037e436759b57fc892ae5cd96c" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/111720727958269953/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=111720727958269953" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=111720727958269953" } }, "id": "111720727958269953", "paging_token": "111720727958269953", "transaction_successful": true, "source_account": "GAR4S3ASZ4HTJ6GQ2DEDLVL4YE6D64UPIOQI4I67L5VPYBGEZDGOI462", "type": "create_account", "type_i": 0, "created_at": "2019-09-26T12:34:24Z", "transaction_hash": "4034838d5b47e4f8c23776faca4d9403637b1f037e436759b57fc892ae5cd96c", "starting_balance": "20.0000000", "funder": "GAR4S3ASZ4HTJ6GQ2DEDLVL4YE6D64UPIOQI4I67L5VPYBGEZDGOI462", "account": "GCRGHKY6RBFVQLF2JCHB7TK7A5BIABITFKVIEOXK4BPEIDE446OEFYXZ" }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/111721376498331649" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/cbf92ce9e2b75b0182597acb1e7c0b58695ec6f69e84a8625c5ab1dda8df31bc" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/111721376498331649/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=111721376498331649" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=111721376498331649" } }, "id": "111721376498331649", "paging_token": "111721376498331649", "transaction_successful": true, "source_account": "GAR4S3ASZ4HTJ6GQ2DEDLVL4YE6D64UPIOQI4I67L5VPYBGEZDGOI462", "type": "payment", "type_i": 1, "created_at": "2019-09-26T12:47:50Z", "transaction_hash": "cbf92ce9e2b75b0182597acb1e7c0b58695ec6f69e84a8625c5ab1dda8df31bc", "asset_type": "native", "from": "GAR4S3ASZ4HTJ6GQ2DEDLVL4YE6D64UPIOQI4I67L5VPYBGEZDGOI462", "to": "GCRGHKY6RBFVQLF2JCHB7TK7A5BIABITFKVIEOXK4BPEIDE446OEFYXZ", "amount": "5000.0000000" }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/111722218311925761" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/9fcf20d23862f40cd8a59f0ad0247a7d56a1b38cb79e4953b5aad8df5f2608a7" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/111722218311925761/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=111722218311925761" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=111722218311925761" } }, "id": "111722218311925761", "paging_token": "111722218311925761", "transaction_successful": true, "source_account": "GBSIPZRLSM2KMLUZYEGKU2WMA6HPEE3NGB47YY4MLK43ISLLCJKFA2F2", "type": "payment", "type_i": 1, "created_at": "2019-09-26T13:05:12Z", "transaction_hash": "9fcf20d23862f40cd8a59f0ad0247a7d56a1b38cb79e4953b5aad8df5f2608a7", "asset_type": "native", "from": "GBSIPZRLSM2KMLUZYEGKU2WMA6HPEE3NGB47YY4MLK43ISLLCJKFA2F2", "to": "GCRGHKY6RBFVQLF2JCHB7TK7A5BIABITFKVIEOXK4BPEIDE446OEFYXZ", "amount": "10.0000000" }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/111747107647434753" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/44af49d32061cdb352d131559560559e7be815b16c45412b7682600c71224623" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/111747107647434753/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=111747107647434753" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=111747107647434753" } }, "id": "111747107647434753", "paging_token": "111747107647434753", "transaction_successful": true, "source_account": "GCRGHKY6RBFVQLF2JCHB7TK7A5BIABITFKVIEOXK4BPEIDE446OEFYXZ", "type": "payment", "type_i": 1, "created_at": "2019-09-26T21:43:41Z", "transaction_hash": "44af49d32061cdb352d131559560559e7be815b16c45412b7682600c71224623", "asset_type": "credit_alphanum4", "asset_code": "BTC", "asset_issuer": "GATEMHCCKCY67ZUCKTROYN24ZYT5GK4EQZ65JJLDHKHRUZI3EUEKMTCH", "from": "GCRGHKY6RBFVQLF2JCHB7TK7A5BIABITFKVIEOXK4BPEIDE446OEFYXZ", "to": "GBSIPZRLSM2KMLUZYEGKU2WMA6HPEE3NGB47YY4MLK43ISLLCJKFA2F2", "amount": "0.0000709" }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/111764193027313665" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/7bd57f8cc75ce2d9740568eade9700cd7b19491a2c938232e5b3f0768f3e588a" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/111764193027313665/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc\u0026cursor=111764193027313665" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc\u0026cursor=111764193027313665" } }, "id": "111764193027313665", "paging_token": "111764193027313665", "transaction_successful": true, "source_account": "GCRGHKY6RBFVQLF2JCHB7TK7A5BIABITFKVIEOXK4BPEIDE446OEFYXZ", "type": "payment", "type_i": 1, "created_at": "2019-09-27T03:37:57Z", "transaction_hash": "7bd57f8cc75ce2d9740568eade9700cd7b19491a2c938232e5b3f0768f3e588a", "asset_type": "credit_alphanum4", "asset_code": "BTC", "asset_issuer": "GATEMHCCKCY67ZUCKTROYN24ZYT5GK4EQZ65JJLDHKHRUZI3EUEKMTCH", "from": "GCRGHKY6RBFVQLF2JCHB7TK7A5BIABITFKVIEOXK4BPEIDE446OEFYXZ", "to": "GBSIPZRLSM2KMLUZYEGKU2WMA6HPEE3NGB47YY4MLK43ISLLCJKFA2F2", "amount": "0.0107156" } ] } } ``` --- ## Streaming Horizon provides a streaming mechanism for receiving events in near real time. Instead of repeatedly sending requests to Horizon for batch updates, a connection is established between a client and Horizon with updates to an endpoint response streaming as new ledgers close and updates occur. This reduces requests that return no data and allows near instantaneous updates client-side. All attributes for the endpoints that allow streaming are the same as regular responses. A caller can initiate streaming by setting ‘Accept: text/event-stream’ in the HTTP header when making the request. | | | ----------------------------------------------------- | | [Ledgers](../resources/ledgers/README.mdx) | | [Transactions](../resources/transactions/README.mdx) | | [Operations](../resources/operations/README.mdx) | | [Payments](../resources/payments/README.mdx) | | [Effects](../resources/effects/README.mdx) | | [Accounts](../resources/accounts/README.mdx) | | [Trades](../resources/trades/README.mdx) | | [Order Books](../aggregations/order-books/README.mdx) | --- ## XDR In the Stellar network, transactions are encoded using a standardized protocol called [External Data Representation](https://en.wikipedia.org/wiki/External_Data_Representation) (XDR). In Horizon, you will only encounter XDR when [posting](../submit-a-transaction.api.mdx) and [getting](../retrieve-a-transaction.api.mdx) transactions and in the [ledger](../resources/ledgers/README.mdx) header. When you post a transaction, a client will encode the transaction as XDR before submitting it to Horizon. When you request a transaction, Horizon returns some data about the transaction in human-readable JSON. The full canonical data about the transaction is encoded in machine-readable XDR, available in XDR attributes at the end of the response. You can decode this XDR on the Stellar Lab’s [XDR page](https://lab.stellar.org/xdr/view?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;). - ATTRIBUTE - TYPE - DESCRIPTION - envelope_xdr - string - The XDR encoded transaction as stellar-core sees it. - result_xdr - string - The effects of a transaction encoded in XDR. - result_meta_xdr - string - The details about the effects of a transaction encoded in XDR. - fee_meta_xdr - string - The fees associated with the transaction encoded in XDR. ```json { // Response truncated to highlight XDR-related attributes "envelope_xdr": "AAAAAPewD+/6X8o0bx3bp49Wf+mUhG3o+TUrcjcst717DWJVAAAAyAFvzscADTkNAAAAAAAAAAAAAAACAAAAAAAAAAYAAAACWE1BVEsAAAAAAAAAAAAAAAPvNOuztX4IjvV8pztsEc1/ZnTz0G3p5Cx4vcf04+xUAAONfqTGgAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAD2NyeXB0b21hcmluZS5ldQAAAAAAAAAAAAAAAAF7DWJVAAAAQK3vfUCZ8mbjW3ssMd0n1tJTF9Fv6EbuJ6cWKkYXBqG5itqanPbFzIQoZEHbPS8nr2vo4dROvKI0uQzNcfExKwM=", "result_xdr": "AAAAAAAAAMgAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAFAAAAAAAAAAA=", "result_meta_xdr": "AAAAAQAAAAIAAAADAZU3mQAAAAAAAAAA97AP7/pfyjRvHdunj1Z/6ZSEbej5NStyNyy3vXsNYlUAAAAABlxskAFvzscADTkMAAAAAgAAAAAAAAAAAAAAD2NyeXB0b21hcmluZS5ldQABAAAAAAAAAAAAAAAAAAAAAAAAAQGVN5kAAAAAAAAAAPewD+/6X8o0bx3bp49Wf+mUhG3o+TUrcjcst717DWJVAAAAAAZcbJABb87HAA05DQAAAAIAAAAAAAAAAAAAAA9jcnlwdG9tYXJpbmUuZXUAAQAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAAwGVN5gAAAABAAAAAPewD+/6X8o0bx3bp49Wf+mUhG3o+TUrcjcst717DWJVAAAAAlhNQVRLAAAAAAAAAAAAAAAD7zTrs7V+CI71fKc7bBHNf2Z089Bt6eQseL3H9OPsVAAATfBgJfPoAAONfqTGgAAAAAABAAAAAAAAAAAAAAABAZU3mQAAAAEAAAAA97AP7/pfyjRvHdunj1Z/6ZSEbej5NStyNyy3vXsNYlUAAAACWE1BVEsAAAAAAAAAAAAAAAPvNOuztX4IjvV8pztsEc1/ZnTz0G3p5Cx4vcf04+xUAABN8GAl8+gAA41+pMaAAAAAAAEAAAAAAAAAAAAAAAIAAAADAZU3mQAAAAAAAAAA97AP7/pfyjRvHdunj1Z/6ZSEbej5NStyNyy3vXsNYlUAAAAABlxskAFvzscADTkNAAAAAgAAAAAAAAAAAAAAD2NyeXB0b21hcmluZS5ldQABAAAAAAAAAAAAAAAAAAAAAAAAAQGVN5kAAAAAAAAAAPewD+/6X8o0bx3bp49Wf+mUhG3o+TUrcjcst717DWJVAAAAAAZcbJABb87HAA05DQAAAAIAAAAAAAAAAAAAAA9jcnlwdG9tYXJpbmUuZXUAAQAAAAAAAAAAAAAAAAAAAA==", "fee_meta_xdr": "AAAAAgAAAAMBlTeXAAAAAAAAAAD3sA/v+l/KNG8d26ePVn/plIRt6Pk1K3I3LLe9ew1iVQAAAAAGXG1YAW/OxwANOQwAAAACAAAAAAAAAAAAAAAPY3J5cHRvbWFyaW5lLmV1AAEAAAAAAAAAAAAAAAAAAAAAAAABAZU3mQAAAAAAAAAA97AP7/pfyjRvHdunj1Z/6ZSEbej5NStyNyy3vXsNYlUAAAAABlxskAFvzscADTkMAAAAAgAAAAAAAAAAAAAAD2NyeXB0b21hcmluZS5ldQABAAAAAAAAAAAAAAAAAAAA" // Response truncated to highlight XDR-related attributes } ``` --- ## Submit a Transaction This endpoint actually submits a transaction to the Stellar network. It only takes a single, required parameter: the signed transaction. Refer to the Transactions page for details on how to craft a proper one. If you submit a transaction that has already been included in a ledger, this endpoint will return the same response as would’ve been returned for the original transaction submission. This allows for safe resubmission of transactions in error scenarios, as highlighted in the error-handling guide. Request --- ## Submit a Transaction Asynchronously This endpoint submits transactions to the Stellar network asynchronously. It is designed to allow users to submit transactions without blocking them while waiting for a response from Horizon. At the same time, it also provides clear response status codes from stellar-core to help understand the status of the submitted transaction. You can then use Horizon's [GET transaction endpoint](https://developers.stellar.org/docs/data/apis/horizon/api-reference/retrieve-a-transaction) to wait for the transaction to be included in a ledger and ingested by Horizon. Request --- ## Providers :::info On August 1, 2024, the publicly accessible SDF-hosted Horizon had its historical data truncated to one year. That update optimized the performance of the publicly accessible Horizon and ensured a streamlined experience for all users. Consider third-party ecosystem providers of Horizon, which may provide a longer history retention window as well as other features. ::: Multiple infrastructure providers have made Horizon services available, and offer plans ranging from free to dedicated instances. These providers can be used for development, testing, and production. These providers allow access to the Futurenet, Testnet and Mainnet network. | Provider | Futurenet | Testnet | Mainnet | Full History | | --- | --- | --- | --- | --- | | [Blockdaemon\*](https://www.blockdaemon.com/apply/soroban) | ❌ | ✅ | ✅ | ✅ | | [Validation Cloud\*](https://app.validationcloud.io) | ❌ | ✅ | ✅ | ✅ | | [QuickNode](https://www.quicknode.com/docs/stellar) | ❌ | ✅ | ✅ | ❌ | | [Ankr](https://www.ankr.com/rpc/advanced-api) | ❌ | ✅ | ✅ | ❌ | | [Obsrvr](https://www.withObsrvr.com) | ❌ | ✅ | ✅ | ❌ | | [Nodies](https://nodies.org) | ❌ | ✅ | ✅ | ❌ | \*Blockdaemon and Validation Cloud provide full historical data for Horizon. ### Publicly Accessible APIs | Provider | Network | URL | | --- | --- | --- | | [LOBSTR](https://lobstr.co) | Mainnet | Horizon: `https://horizon.stellar.lobstr.co` | --- ## Migrate from Horizon to RPC Applications using [Horizon's REST-like API] will need to be updated to use the [RPC JSON-RPC API] when migrating from [Horizon] to [RPC]. This guide provides an overview of the key differences between the two APIs and how to migrate your application. ## Request / Response Format Horizon's REST-like API uses HTTP methods and status codes to communicate with clients. Responses are JSON in the HAL format. See [Horizon's Response Format]. RPC's JSON-RPC API uses JSON-RPC 2.0 to communicate with clients. Requests to the API are JSON objects that contain one or more method invocations. Responses are also JSON objects that contain a result for each invocation in the request. See [JSON-RPC]. Both formats utilise JSON for the overall structure which are relatively simple and do not require any special client code, although there are client [SDKs] available. Some values contained within are XDR encoded and can be decoded using Stellar [SDKs]. ## Endpoint Mapping Applications that use the following Horizon endpoints can typically migrate directly to the RPC using the referenced methods. Endpoints without mappings do not have a direct replacement in the RPC API. To build similar functionality in an application, please consider partnering with an indexer or using the information listed below to build your own indexed representation of horizon endpoints. Consider using other [Data] products for analytics use cases. | Horizon Endpoint | Corresponding RPC Method(s) | Indexer Equivalent | Analytics Resources | | --- | --- | --- | --- | | [`GET /`] | [`getLatestLedger`] [`getVersionInfo`] [`getHealth`] [`getNetwork`] | Not applicable | [Analyst Guide](../analytics/hubble/analyst-guide/README.mdx) | | [`GET /ledgers`] | [`getLedgers`] | Create a [`getLedgers`] with full history, using [Galexie](../indexers/build-your-own/galexie/README.mdx), to build a historical ledger view. | [Ledgers](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#ledgers) | | [`GET /ledgers/{seq}`] | [`getLedgers`] (with filter for sequence) | Use a [`getLedgers`] view with full history, filtered by ledger sequence | [Ledgers](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#ledgers) | | [`GET /ledgers/{seq}/transactions`] | [`getTransactions`] (with filter for ledger sequence) | Use [`getTransactions`] with ledger sequence filtering to retrieve transactions for a specific ledger | [Transactions](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#transactions) | | [`GET /ledgers/{seq}/operations`] | [`getTransactions`] | Use [`getTransactions`] for the given ledger, then parse the transaction's XDR for individual operations. | [Operations](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#operations) | | [`GET /ledgers/{seq}/payments`] | [`getEvents`] [`getTransactions`] ⚠️ | Use [`getEvents`] (specifically CAP-67 events when available) and parse [`getTransactions`] meta XDR to identify payment-like operations. | [Payments](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#payments) | | [`GET /ledgers/{seq}/effects`] | [`getEvents`] [`getTransactions`] ⚠️ | Use [`getEvents`] (when expanded to cover all effects with CAP-67) and parse [`getTransactions`] meta XDR for relevant data. | [Effects](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#effects) | | [`POST /transactions`] | [`sendTransaction`] | Not applicable | Not applicable | | [`POST /transactions_async`] | [`sendTransaction`] | Not applicable | Not applicable | | [`GET /transactions`] | [`getTransactions`] | Use [`getTransactions`] to build a historical transaction list | [Transactions](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#transactions) | | [`GET /transactions/{hash}`] | [`getTransaction`] | Use getTransaction to retrieve a specific transaction by its hash | [Transactions](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#transactions) | | [`GET /transactions/{hash}/operations`] | [`getTransaction`] | Filter [`getTransaction`] by hash and then parse its XDR for operations | [Operations](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#operations) | | [`GET /transactions/{hash}/payments`] | No direct RPC equivalent; use [`getEvents`] or parse [`getTransactions`] | Filter [`getTransaction`] by hash and analyze its events and operation types to identify payments. | [Payments](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#payments) | | [`GET /transactions/{hash}/effects`] | No direct RPC equivalent; use [`getEvents`] or parse [`getTransactions`] | Filter [`getTransaction`] by hash and analyze its events and metadata for relevant data. | [Effects](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#effects) | | [`GET /operations`] | [`getTransactions`] | Ingest all historical ledgers/transactions and build and build a view of operations for filtering. | [Operations](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#operations) | | [`GET /operations/{id}`] | No direct RPC equivalent | Store Horizon's operation ID and map it back to the ledger sequence and transaction index to retrieve the relevant transaction via [`getTransactions`]. | [Operations](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#operations) | | [`GET /operations/{id}/effects`] | No direct RPC equivalent | Retrieve the operation by ID (as above) and then parse its associated transaction and events for effects. | [Effects](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#effects) | | [`GET /fee_stats`] | [`getFeeStats`] [`simulateTransaction`] | Indexed data not recommended. | [Fee Stats](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#fee-stats) | | [`GET /accounts`] | No direct RPC equivalent | Ingest all ledger history via [`getLedgers`] to build and maintain a complete list of accounts. | [Accounts](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#accounts) | | [`GET /accounts/{address}`] | [`getLedgerEntries`] | Use [`getLedgerEntries`] for a specific account address. Note: RPC will not provide trust line information associated with the account directly, as Horizon does. You will need to derive this from ledger entries. | [Accounts](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#accounts) | | [`GET /claimable_balances`] | No direct RPC equivalent | Ingest all ledger history via [`getLedgers`] to build and maintain a complete list of claimable balances. | [Claimable Balances](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#claimable-balances) | | [`GET /claimable_balances/{id}`] | [`getLedgerEntries`] | Use [`getLedgerEntries`] to retrieve a specific claimable balance by ID. | [Claimable Balances](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#claimable-balances) | | [`GET /claimable_balances/{id}/transactions`] | No direct RPC equivalent | Trace transactions that interact with the specific claimable balance ID from their historical ledger data. | [Claimable Balances](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#claimable-balances) | | [`GET /claimable_balances/{id}/operations`] | No direct RPC equivalent | trace operations related to the specific claimable balance ID from your historical ledger data. | [Claimable Balances](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#claimable-balances) | | [`GET /liquidity_pools`] | No direct RPC equivalent | ingest all ledger history via [`getLedgers`] to build and maintain a complete list of liquidity pools. | [Liquiditity Pools](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#liquidity-pools) | | [`GET /liquidity_pools/{id}`] | [`getLedgerEntries`] | Use [`getLedgerEntries`] to retrieve a specific liquidity pool by ID. | [Liquiditity Pools](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#liquidity-pools) | | [`GET /liquidity_pools/{id}/transactions`] | No direct RPC equivalent | Trace transactions that interact with the specific liquidity pool ID from their historical ledger data. | [Liquiditity Pools](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#liquidity-pools) | | [`GET /liquidity_pools/{id}/operations`] | No direct RPC equivalent | Trace operations related to the specific liquidity pool ID from their historical ledger data. | [Liquiditity Pools](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#liquidity-pools) | | [`GET /liquidity_pools/{id}/effects`] | No direct RPC equivalent | Trace effects related to the specific liquidity pool ID from their historical ledger data. | [Liquiditity Pools](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#liquidity-pools) | | [`GET /liquidity_pools/{id}/trades`] | No direct RPC equivalent | Infer trades related to the specific liquidity pool ID from historical ledger data (e.g., from [`getEvents`] and [`getTransactions`] metadata). | [Liquiditity Pools](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#liquidity-pools) | | [`GET /offers`] | No direct RPC equivalent | Ingest all ledger history via [`getLedgers`] to build and maintain a complete list of offers. | [Offers](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#offers) | | [`GET /offers/{id}`] | [`getLedgerEntries`] | Use [`getLedgerEntries`] to retrieve a specific offer by ID. | [Offers](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#offers) | | [`GET /offers/{id}/trades`] | No direct RPC equivalent | Infer trades related to the specific offer ID from historical ledger data (e.g., from [`getEvents`] and [`getTransactions`] metadata). | [Offers](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#offers) | | [`GET /payments`] | [`getEvents`] [`getTransactions`] ⚠️ | Use [`getEvents`] (CAP-67 events) and process [`getTransactions`] metadata for payment-like operations across all history. | [Payments](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#payments) | | [`GET /effects`] | [`getEvents`] [`getTransactions`] ⚠️ | Use [`getEvents`] (when expanded) and process [`getTransactions`] metadata to derive effects across all history. | [Effects](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#effects) | | [`GET /trades`] | [`getEvents`] [`getTransactions`] ⚠️ | Infer trades from [`getEvents`] and [`getTransactions`] metadata across all history, as there is no direct "trade event" in RPC. | [Trades](../analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx#trades) | :::tip The [`getTransactions`] method can be used to retrieve events batched by transaction. The events are contained in the meta XDR of the transaction (field `resultMetaXdr`). ::: :::warning The [`getEvents`] method is not a direct replacement for Horizon's endpoints. The method returns a stream of events that in the current protocol only include events from contracts. In the near future as a result of [CAP-67] this method will be expanded to include events from non-contract operations. In the interim the [`getTransactions`] method can be used to retrieve the meta XDR of transactions containing non-contract operations to determine what movements of value have occurred. The meta XDR also contains events from contracts. ::: [CAP-67]: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md [Horizon]: ./horizon/README.mdx [RPC]: ./rpc/README.mdx [Horizon's REST-like API]: ./horizon/api-reference/README.mdx [Horizon's Response Format]: ./horizon/api-reference/structure/response-format.mdx [RPC JSON-RPC API]: ./rpc/api-reference/README.mdx [JSON-RPC]: ./rpc/api-reference/structure/json-rpc.mdx [Data]: ../analytics/README.mdx [SDKs]: ../../tools/sdks/README.mdx [`GET /`]: ./horizon/api-reference/README.mdx [`GET /ledgers`]: ./horizon/api-reference/list-all-ledgers.api.mdx [`GET /ledgers/{seq}`]: ./horizon/api-reference/retrieve-a-ledger.api.mdx [`GET /ledgers/{seq}/transactions`]: ./horizon/api-reference/retrieve-a-ledgers-transactions.api.mdx [`GET /ledgers/{seq}/operations`]: ./horizon/api-reference/retrieve-a-ledgers-operations.api.mdx [`GET /ledgers/{seq}/payments`]: ./horizon/api-reference/retrieve-a-ledgers-payments.api.mdx [`GET /ledgers/{seq}/effects`]: ./horizon/api-reference/retrieve-a-ledgers-effects.api.mdx [`POST /transactions`]: ./horizon/api-reference/submit-a-transaction.api.mdx [`POST /transactions_async`]: ./horizon/api-reference/submit-async-transaction.api.mdx [`GET /transactions`]: ./horizon/api-reference/list-all-transactions.api.mdx [`GET /transactions/{hash}`]: ./horizon/api-reference/retrieve-a-transaction.api.mdx [`GET /transactions/{hash}/operations`]: ./horizon/api-reference/retrieve-a-transactions-operations.api.mdx [`GET /transactions/{hash}/payments`]: ./horizon/api-reference/retrieve-a-transactions-payments.api.mdx [`GET /transactions/{hash}/effects`]: ./horizon/api-reference/retrieve-a-transactions-effects.api.mdx [`GET /operations`]: ./horizon/api-reference/list-all-operations.api.mdx [`GET /operations/{id}`]: ./horizon/api-reference/retrieve-an-operation.api.mdx [`GET /operations/{id}/effects`]: ./horizon/api-reference/retrieve-an-operations-effects.api.mdx [`GET /fee_stats`]: ./horizon/api-reference/README.mdx [`GET /accounts`]: ./horizon/api-reference/list-all-accounts.api.mdx [`GET /accounts/{address}`]: ./horizon/api-reference/retrieve-an-account.api.mdx [`GET /claimable_balances`]: ./horizon/api-reference/list-all-claimable-balances.api.mdx [`GET /claimable_balances/{id}`]: ./horizon/api-reference/retrieve-a-claimable-balance.api.mdx [`GET /claimable_balances/{id}/transactions`]: ./horizon/api-reference/cb-retrieve-related-transactions.api.mdx [`GET /claimable_balances/{id}/operations`]: ./horizon/api-reference/cb-retrieve-related-operations.api.mdx [`GET /liquidity_pools`]: ./horizon/api-reference/list-liquidity-pools.api.mdx [`GET /liquidity_pools/{id}`]: ./horizon/api-reference/retrieve-a-liquidity-pool.api.mdx [`GET /liquidity_pools/{id}/transactions`]: ./horizon/api-reference/lp-retrieve-related-transactions.api.mdx [`GET /liquidity_pools/{id}/operations`]: ./horizon/api-reference/lp-retrieve-related-operations.api.mdx [`GET /liquidity_pools/{id}/effects`]: ./horizon/api-reference/retrieve-related-effects.api.mdx [`GET /liquidity_pools/{id}/trades`]: ./horizon/api-reference/retrieve-related-trades.api.mdx [`GET /offers`]: ./horizon/api-reference/get-all-offers.api.mdx [`GET /offers/{id}`]: ./horizon/api-reference/get-offer-by-offer-id.api.mdx [`GET /offers/{id}/trades`]: ./horizon/api-reference/get-trades-by-offer-id.api.mdx [`GET /payments`]: ./horizon/api-reference/list-all-payments.api.mdx [`GET /effects`]: ./horizon/api-reference/list-all-effects.api.mdx [`GET /trades`]: ./horizon/api-reference/get-all-trades.api.mdx [`getLatestLedger`]: ./rpc/api-reference/methods/getLatestLedger.mdx [`getVersionInfo`]: ./rpc/api-reference/methods/getVersionInfo.mdx [`getHealth`]: ./rpc/api-reference/methods/getHealth.mdx [`getNetwork`]: ./rpc/api-reference/methods/getNetwork.mdx [`getLedgers`]: ./rpc/api-reference/methods/getLedgers.mdx [`getTransactions`]: ./rpc/api-reference/methods/getTransactions.mdx [`getTransaction`]: ./rpc/api-reference/methods/getTransaction.mdx [`getEvents`]: ./rpc/api-reference/methods/getEvents.mdx [`sendTransaction`]: ./rpc/api-reference/methods/sendTransaction.mdx [`getFeeStats`]: ./rpc/api-reference/methods/getFeeStats.mdx [`simulateTransaction`]: ./rpc/api-reference/methods/simulateTransaction.mdx [`getLedgerEntries`]: ./rpc/api-reference/methods/getLedgerEntries.mdx --- ## Use the Stellar RPC to Access Blockchain Data, Query Transactions & More # RPC Introduction :::info Stellar-RPC was renamed from Soroban-RPC in Nov 2024. Additional context on this decision can be found on our [developer blog]. ::: Stellar RPC is a lightweight tool that provides real-time access to Stellar network data. Much like RPC nodes in other blockchain ecosystems, it allows developers to query the network efficiently. Whether you’re building a non-custodial wallet, issuing assets, or monitoring network activity, Stellar RPC is designed to provide trusted, stable infrastructure that anyone can run. For any new builders coming to Stellar, Stellar RPC should be your starting point—it’s built to align with the growing needs of the ecosystem. RPC can be accessed via cURL or one of the [Stellar SDKs](../../../tools/sdks/README.mdx). ## Why Run RPC? Running RPC within your own infrastructure provides a number of benefits. You can: - Have full operational control without dependency on any third party provider for network data and transaction submission. The only way to harness the true power of a decentralized blockchain! - Avoid the added overhead of directly interacting with [Stellar Core], whose primary focus is performance and therefore provides a very limited API - Avoid the added overhead of storing way more data than your application actually needs, as would be the case when running [Horizon] What Stellar RPC is not: - An indexer for historical data. RPC retains only a bounded, recent window of history (about 7 days by default). - A primary backend service for your application. Use RPC as your gateway to the blockchain, but ingest and index only the data you care about. - A drop-in replacement for Horizon. Horizon provides several indexing features not commonly supported by RPC nodes. We believe these business opportunities should be passed back to third party applications (indexers, analytics providers, etc) and away from the SDF. ## In These Docs - [Admin Guide](./admin-guide/README.mdx): how to set up and operate your own RPC instance. - [RPC Methods](./api-reference/methods/README.mdx): descriptions of RPC methods, including their expected inputs and outputs. - [Structure](./api-reference/structure/README.mdx): how the RPC API is structured. - [Ecosystem Providers](./providers.mdx): third party providers that provide RPC instances as a service. [quickstart]: https://github.com/stellar/quickstart [developer blog]: https://stellar.org/blog/foundation-news/stellar-rpc-has-arrived [Stellar Core]: ../../validators [Horizon]: ./horizon --- ## Admin Guide(Admin-guide) All you need to know about setting up, running, and using Stellar RPC. --- ## Configuring(Admin-guide) For production, we recommend running Stellar RPC with a [TOML](https://toml.io/en) configuration file rather than CLI flags. This is similar to creating a configuration file for Stellar-Core as we did previously. For example, using [our docker image](https://hub.docker.com/r/stellar/stellar-rpc): ```bash docker run -p 8001:8001 -p 8000:8000 \ -v :/config stellar/stellar-rpc \ --config-path ``` Use this [Stellar RPC subcommand](https://github.com/stellar/stellar-rpc/blob/main/cmd/stellar-rpc/README.md#configuring-and-running-rpc-server) to generate a starter configuration file: ```bash docker run stellar/stellar-rpc:latest gen-config-file > stellar-rpc-config.toml ``` The resulting configuration should look like this: ```toml # Admin endpoint to listen and serve on. WARNING: this should not be accessible # from the Internet and does not use TLS. "" (default) disables the admin server # ADMIN_ENDPOINT = "" # path to additional configuration for the Stellar Core configuration file used # by captive core. It must, at least, include enough details to define a quorum # set # CAPTIVE_CORE_CONFIG_PATH = "" # Storage location for Captive Core bucket data CAPTIVE_CORE_STORAGE_PATH = "/" # establishes how many ledgers exist between checkpoints, do NOT change this # unless you really know what you are doing CHECKPOINT_FREQUENCY = 64 # configures classic fee stats retention window expressed in number of ledgers CLASSIC_FEE_STATS_RETENTION_WINDOW = 10 # SQLite DB path DB_PATH = "stellar_rpc.sqlite" # Default cap on the amount of events included in a single getEvents response DEFAULT_EVENTS_LIMIT = 100 # Default cap on the amount of ledgers included in a single getLedgers response DEFAULT_LEDGERS_LIMIT = 50 # Default cap on the amount of transactions included in a single getTransactions # response DEFAULT_TRANSACTIONS_LIMIT = 50 # Endpoint to listen and serve on ENDPOINT = "localhost:8000" # The friendbot URL to be returned by getNetwork endpoint # FRIENDBOT_URL = "" # comma-separated list of stellar history archives to connect with HISTORY_ARCHIVE_URLS = [] # configures history retention window for transactions and events, expressed in # number of ledgers, the default value is 120960 which corresponds to about 7 # days of history HISTORY_RETENTION_WINDOW = 120960 # Ingestion Timeout when bootstrapping data (checkpoint and in-memory # initialization) and preparing ledger reads INGESTION_TIMEOUT = "50m0s" # format used for output logs (json or text) # LOG_FORMAT = "text" # minimum log severity (debug, info, warn, error) to log LOG_LEVEL = "info" # Maximum amount of events allowed in a single getEvents response MAX_EVENTS_LIMIT = 10000 # The maximum duration of time allowed for processing a getEvents request. When # that time elapses, the rpc server would return -32001 and abort the request's # execution MAX_GET_EVENTS_EXECUTION_DURATION = "10s" # The maximum duration of time allowed for processing a getFeeStats request. # When that time elapses, the rpc server would return -32001 and abort the # request's execution MAX_GET_FEE_STATS_EXECUTION_DURATION = "5s" # The maximum duration of time allowed for processing a getHealth request. When # that time elapses, the rpc server would return -32001 and abort the request's # execution MAX_GET_HEALTH_EXECUTION_DURATION = "5s" # The maximum duration of time allowed for processing a getLatestLedger request. # When that time elapses, the rpc server would return -32001 and abort the # request's execution MAX_GET_LATEST_LEDGER_EXECUTION_DURATION = "5s" # The maximum duration of time allowed for processing a getLedgers request. When # that time elapses, the rpc server would return -32001 and abort the request's # execution MAX_GET_LEDGERS_EXECUTION_DURATION = "5s" # The maximum duration of time allowed for processing a getLedgerEntries # request. When that time elapses, the rpc server would return -32001 and abort # the request's execution MAX_GET_LEDGER_ENTRIES_EXECUTION_DURATION = "5s" # The maximum duration of time allowed for processing a getNetwork request. When # that time elapses, the rpc server would return -32001 and abort the request's # execution MAX_GET_NETWORK_EXECUTION_DURATION = "5s" # The maximum duration of time allowed for processing a getTransactions request. # When that time elapses, the rpc server would return -32001 and abort the # request's execution MAX_GET_TRANSACTIONS_EXECUTION_DURATION = "5s" # The maximum duration of time allowed for processing a getTransaction request. # When that time elapses, the rpc server would return -32001 and abort the # request's execution MAX_GET_TRANSACTION_EXECUTION_DURATION = "5s" # The maximum duration of time allowed for processing a getVersionInfo request. # When that time elapses, the rpc server would return -32001 and abort the # request's execution MAX_GET_VERSION_INFO_EXECUTION_DURATION = "5s" # maximum ledger latency (i.e. time elapsed since the last known ledger closing # time) considered to be healthy (used for the /health endpoint) MAX_HEALTHY_LEDGER_LATENCY = "30s" # Maximum amount of ledgers allowed in a single getLedgers response MAX_LEDGERS_LIMIT = 200 # The max request execution duration is the predefined maximum duration of time # allowed for processing a request. When that time elapses, the server would # return 504 and abort the request's execution MAX_REQUEST_EXECUTION_DURATION = "25s" # The maximum duration of time allowed for processing a sendTransaction request. # When that time elapses, the rpc server would return -32001 and abort the # request's execution MAX_SEND_TRANSACTION_EXECUTION_DURATION = "15s" # The maximum duration of time allowed for processing a simulateTransaction # request. When that time elapses, the rpc server would return -32001 and abort # the request's execution MAX_SIMULATE_TRANSACTION_EXECUTION_DURATION = "15s" # Maximum amount of transactions allowed in a single getTransactions response MAX_TRANSACTIONS_LIMIT = 200 # Network passphrase of the Stellar network transactions should be signed for. # Commonly used values are "Test SDF Future Network ; October 2022", "Test SDF # Network ; September 2015" and "Public Global Stellar Network ; September 2015" # NETWORK_PASSPHRASE = "" # Enable debug information in preflighting (provides more detailed errors). It # should not be enabled in production deployments. PREFLIGHT_ENABLE_DEBUG = true # Number of workers (read goroutines) used to compute preflights for the # simulateTransaction endpoint. Defaults to the number of CPUs. PREFLIGHT_WORKER_COUNT = 8 # Maximum number of outstanding preflight requests for the simulateTransaction # endpoint. Defaults to the number of CPUs. PREFLIGHT_WORKER_QUEUE_SIZE = 8 # Maximum number of outstanding GetEvents requests REQUEST_BACKLOG_GET_EVENTS_QUEUE_LIMIT = 1000 # Maximum number of outstanding GetFeeStats requests REQUEST_BACKLOG_GET_FEE_STATS_QUEUE_LIMIT = 100 # Maximum number of outstanding GetHealth requests REQUEST_BACKLOG_GET_HEALTH_QUEUE_LIMIT = 1000 # Maximum number of outstanding GetLatestsLedger requests REQUEST_BACKLOG_GET_LATEST_LEDGER_QUEUE_LIMIT = 1000 # Maximum number of outstanding getLedgers requests REQUEST_BACKLOG_GET_LEDGERS_QUEUE_LIMIT = 1000 # Maximum number of outstanding GetLedgerEntries requests REQUEST_BACKLOG_GET_LEDGER_ENTRIES_QUEUE_LIMIT = 1000 # Maximum number of outstanding GetNetwork requests REQUEST_BACKLOG_GET_NETWORK_QUEUE_LIMIT = 1000 # Maximum number of outstanding GetTransactions requests REQUEST_BACKLOG_GET_TRANSACTIONS_QUEUE_LIMIT = 1000 # Maximum number of outstanding GetTransaction requests REQUEST_BACKLOG_GET_TRANSACTION_QUEUE_LIMIT = 1000 # Maximum number of outstanding GetVersionInfo requests REQUEST_BACKLOG_GET_VERSION_INFO_QUEUE_LIMIT = 1000 # Maximum number of outstanding requests REQUEST_BACKLOG_GLOBAL_QUEUE_LIMIT = 5000 # Maximum number of outstanding SendTransaction requests REQUEST_BACKLOG_SEND_TRANSACTION_QUEUE_LIMIT = 500 # Maximum number of outstanding SimulateTransaction requests REQUEST_BACKLOG_SIMULATE_TRANSACTION_QUEUE_LIMIT = 100 # The request execution warning threshold is the predetermined maximum duration # of time that a request can take to be processed before a warning would be # generated REQUEST_EXECUTION_WARNING_THRESHOLD = "5s" # configures soroban inclusion fee stats retention window expressed in number of # ledgers SOROBAN_FEE_STATS_RETENTION_WINDOW = 50 # HTTP port for Captive Core to listen on (0 disables the HTTP server) STELLAR_CAPTIVE_CORE_HTTP_PORT = 11626 # path to stellar core binary STELLAR_CORE_BINARY_PATH = "/usr/bin/stellar-core" # Timeout used when submitting requests to stellar-core STELLAR_CORE_TIMEOUT = "2s" # URL used to query Stellar Core (local captive core by default) # STELLAR_CORE_URL = "" # Enable strict toml configuration file parsing. This will prevent unknown # fields in the config toml from being parsed. # STRICT = false ``` Note that the above generated configuration contains the default values and you need to substitute them with proper values to run the image. For instance, when using a container, it is recommended to create a volume for the Captive Core and RPC persistent storage and point `CAPTIVE_CORE_STORAGE_PATH` and `DB_PATH` to it accordingly. Then, you should create a configuration file for [Stellar Core](https://github.com/stellar/stellar-core). You can find sample configuration files for [Testnet](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/ledgerbackend/configs/captive-core-testnet.cfg) and [Pubnet](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/ledgerbackend/configs/captive-core-pubnet.cfg) When using docker, you should create and mount a volume [mount a volume](https://docs.docker.com/storage/volumes) on your container where the above configuration is stored. If you are running locally on a unix-style machine, you could create a folder in your home directory called `~/rpc-config` ``` cd ~ mkdir rpc-config ``` Then you would add the following config files to that local directory: - soroban-rpc-config.toml - stellar-captive-core.cfg Then you would mount that volume using by adding the following parameter: `-v /Users//test-rpc-config:/opt/stellar` to your `docker run` command. Your running container would mount that volume at the path `/opt/stellar` ## Next Step After installation is complete, you are now ready to proceed to [Running RPC](./running.mdx)! --- ## Data Lake Integration Expand your RPC node's capabilities by connecting it to a data lake for complete historical ledger access. #### Integration Overview RPC version 23.0 introduces data lake integration for the `getLedgers` endpoint, enabling access to the historical ledgers outside your node's local retention period (typically 7 days). All other RPC endpoints will still operate based on the `HISTORY_RETENTION_WINDOW` configured on your node. The process involves setting up a ledger data lake and then configuring your RPC node to use it. :::info The metadata of a ledger returned by `getLedgers` may vary depending on its source. When a ledger is retrieved from the RPC's local datastore, its metadata is subject to your RPC configuration. However, ledgers fetched from the data lake are typically stored with all metadata included. ::: ### 1. Accessing a Data Lake You have two options for utilizing a data lake: - **Public Data Lake:** The simplest way is to use a publicly available data lake. For example, Stellar ledger data lake is available through the AWS Open Data program at `s3://aws-public-blockchain/v1.1/stellar/ledgers/pubnet`. - **Self-Hosted Data Lake:** This method lets you have more control over data integrity, availability and access, but requires you to create and manage your own data lake. The **Galexie** tool can help you deploy a data lake on either AWS S3 or Google Cloud Storage (GCS). For detailed instructions, refer to the [Galexie Admin Guide](../../../../data/indexers/build-your-own/galexie/README.mdx). ### 2. Configuring RPC for Data Lake Integration #### Pre-requisite Before you begin, configure your RPC node with cloud provider credentials and ensure it has read permissions for the data lake bucket. #### Configuration Steps Update your RPC node's configuration file with the following settings: 1. **Specify Storage Path:** Define the storage backend (`GCS` or `S3`) and provide the full path to the bucket (e.g., `my-bucket/path/to/data`). 2. **Enable the Feature Flag:** Set `SERVE_LEDGERS_FROM_DATASTORE` to `true`. 3. **Configure Ledger Backend:** Configure how data is read from the datastore through [BufferedStorageBackend](../../../../data/indexers/build-your-own/ingest-sdk/developer_guide/ledgerbackends/bufferedstoragebackend.mdx). #### Configuration Examples Below are examples for configuring GCS and S3 backends. A. GCS Configuration Example ```toml # Enable fetching historical ledgers from the datastore when not available locally SERVE_LEDGERS_FROM_DATASTORE = true # External datastore configuration for GCS [datastore_config] type = "GCS" [datastore_config.params] destination_bucket_path = "your-bucket/path/to/data" [datastore_config.schema] ledgers_per_file = 1 files_per_partition = 64000 [buffered_storage_backend_config] buffer_size = 100 num_workers = 10 retry_limit = 3 retry_wait = "5s" ``` B. S3 Configuration Example ```toml # Enable fetching historical ledgers from the datastore when not available locally SERVE_LEDGERS_FROM_DATASTORE = true # External datastore configuration for S3 [datastore_config] type = "S3" [datastore_config.params] destination_bucket_path = "your-bucket/path/to/data`" region = "your_s3_region" # e.g., "us-east-1" [datastore_config.schema] ledgers_per_file = 1 files_per_partition = 64000 [buffered_storage_backend_config] buffer_size = 100 num_workers = 10 retry_limit = 3 retry_wait = "5s" ``` ### 3. Verifying the Setup After configuring your RPC node, you can verify that the integration is working by making a `GetLedgers` request for a ledger sequence number that's older than your node’s standard retention window.The RPC should successfully return the ledger data from the data lake. Example Request: ``` curl -X POST https:/// \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "getLedgers", "params": { "startLedger": 100, "pagination": { "limit": 1 } } }' ``` --- ## Development For local development, we recommend [downloading](https://hub.docker.com/r/stellar/quickstart) and running a local instance via [Docker Quickstart](https://github.com/stellar/quickstart) and running a local network or communicating with a live development [Testnet]. :::caution We don't recommend running the Quickstart image in production. See the [deploy your own RPC instance](./installing.mdx) section. ::: ### Standalone To run a local standalone network with the Stellar Quickstart Docker image, run the following command: ```bash docker run --rm -it \ -p 8000:8000 \ --name stellar \ stellar/quickstart:testing \ --local \ --enable-stellar-rpc ``` Once the image is started, you can check RPC's status by querying the health check endpoint: ```bash curl --location 'http://localhost:8000/rpc' \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc":"2.0", "id":2, "method":"getHealth" }' ``` You can interact with this local node using the Stellar CLI. First, add it as a configured network: ```bash stellar network add local \ --rpc-url "http://localhost:8000/rpc" \ --network-passphrase "Standalone Network ; February 2017" ``` Then generate a unique identity (public/private keypair) and to fund it using: ```bash stellar keys generate alice ``` :::tip[Test-only Identities] It's a good practice to never use the same keys for testing and development that you use for the public Stellar network. Generate new keys for testing and development and avoid using them for other purposes. ::: Now that you have a configured network and a funded identity, you can use these within other Stellar CLI commands. For example, deploying a contract: ```bash stellar contract deploy \ --wasm target/wasm32v1-none/release/[project_name].wasm \ --source-account alice \ --network local ``` Or invoking a contract: ```bash stellar contract invoke \ --id C... \ --source-account alice \ --network local \ -- \ hello \ --to friend ``` When you're done with your local node, you can close it with ctrlc (not cmd). This will fully remove the container (that's what the `--rm` option to the `docker` command does), which means you will need to re-deploy your contract and re-fund your identity the next time you start it. If you work with local nodes often, you may want to create scripts to make these initialization steps easier. For example, see the [example dapp's `initialize.sh`](https://github.com/stellar/soroban-example-dapp/blob/abdac3afdb6c410cc426831ece93371c1a27347d/initialize.sh). [Testnet]: ../../../../networks/README.mdx --- ## Installing(Admin-guide) We offer three alternatives to deploy your own RPC instance: 1. Run the [stellar-rpc docker image](https://hub.docker.com/r/stellar/stellar-rpc) 2. Install a Debian package. 3. Install from source. ### Docker Image This is the preferred way to deploy your own RPC instance. :::caution Although we have a [Quickstart Image](https://github.com/stellar/quickstart), it's for local development and testing only. It is not suitable for production-grade deployments. ::: Pull the image at the version you'd like to run from [the tags](https://hub.docker.com/r/stellar/stellar-rpc/tags): ```bash docker pull stellar/stellar-rpc ``` ### Debian package ``` apt-get update && apt-get install -y --no-install-recommends ca-certificates curl jq wget gnupg apt-utils gpg && \ curl -sSL https://apt.stellar.org/SDF.asc | gpg --dearmor >/etc/apt/trusted.gpg.d/SDF.gpg && \ echo "deb https://apt.stellar.org focal stable" >/etc/apt/sources.list.d/SDF.list && \ echo "deb https://apt.stellar.org focal testing" >/etc/apt/sources.list.d/SDF-testing.list && \ echo "deb https://apt.stellar.org focal unstable" >/etc/apt/sources.list.d/SDF-unstable.list && \ apt-get update && \ apt-get install -y stellar-core stellar-rpc && \ apt-get clean ``` ### Build From Source Instructions for building Stellar RPC from source can be found [here](https://github.com/stellar/stellar-rpc/blob/main/cmd/stellar-rpc/README.md). ## Next Step After installation is complete, you are now ready to proceed to [Configuring RPC](./configuring.mdx)! --- ## Monitoring(Admin-guide) If you run Stellar RPC with the `--admin-endpoint` configured and [expose the port](https://docs.docker.com/engine/reference/commandline/run/#publish), you'll have access to the [Prometheus](https://prometheus.io) metrics via the `/metrics` endpoint. For example, if the admin endpoint is `0.0.0.0:8001` and you're running the Stellar RPC Docker image: ```bash curl localhost:8001/metrics ``` You will see many of the default Go and Process metrics (prefixed by `go_` and `process_` respectively) such as memory usage, CPU usage, number of threads, etc. We also expose metrics related to Stellar RPC (prefixed by `soroban_rpc`). There are many, but some notable ones are: - `soroban_rpc_transactions_count` - count of transactions ingested with a sliding window of 10m - `soroban_rpc_events_count` - count of events ingested with a sliding window of 10m - `soroban_rpc_ingest_local_latest_ledger` - latest ledger ingested - `soroban_rpc_db_round_trip_time_seconds` - time required to run `SELECT 1` query in the DATABASE Stellar RPC also provides logging to console for: - Startup activity - Ingesting, applying, and closing ledgers - Handling JSON RPC requests - Any errors The logs have the format: ``` time= level= msg= pid= subservice= ``` ## Grafana dashboard We provide a [Grafana dashboard](https://grafana.com/grafana/dashboards/19229-soroban-rpc) with different metrics to monitor your RPC instance. ## Verify the RPC Instance After installation, it will be worthwhile to verify the installation of Stellar RPC is healthy. There are two methods: 1. Access the health status endpoint of the JSON RPC service using an HTTP client 2. Use our pre-built [System Test Docker image](https://hub.docker.com/r/stellar/system-test/tags) as a tool to run a set of live tests against Stellar RPC ### Health Status Endpoint If you send a JSON RPC HTTP request to your running instance of Stellar RPC: ```bash curl --location 'http://localhost:8000' \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc":"2.0", "id":2, "method":"getHealth" }' ``` You should get back an HTTP 200 status response if the instance is healthy: ```json { "jsonrpc": "2.0", "id": 2, "result": { "status": "healthy" } } ``` ### System Test Docker Image This test will compile, deploy, and invoke example contracts to the network to which your RPC instance is connected to. Here is an example for verifying your instance if it is connected to Testnet: ```bash # checkout https://github.com/stellar/system-test repo first, and build the image locally with release component versions system-test$ make \ CORE_IMAGE=stellar/stellar-core:22 \ CORE_IMAGE_BIN_PATH=/usr/bin/stellar-core \ SOROBAN_RPC_GIT_REF=https://github.com/stellar/soroban-rpc.git#v22.1.2 \ SOROBAN_CLI_GIT_REF=https://github.com/stellar/soroban-cli.git#v22.2.0 \ RS_XDR_GIT_REF=v22.1.0 \ JS_STELLAR_SDK_NPM_VERSION=13.1.0 \ build # run the local system test image $ docker run --rm -t --name e2e_test \ stellar/system-test:dev \ --VerboseOutput true \ --TargetNetworkRPCURL \ --TargetNetworkPassphrase "Test SDF Network ; September 2015" \ --TargetNetworkTestAccountSecret \ --TargetNetworkTestAccountPublic \ ``` Make sure you configure the system test correctly: - Set `--TargetNetworkRPCURL` to your RPC HTTP URL - Set `--TargetNetworkPassphrase` to the same network your RPC instance is connected to: - `"Test SDF Network ; September 2015"` for Testnet - `"Test SDF Future Network ; October 2022"` for Futurenet - Set `--SorobanExamplesGitHash` to the corresponding release tag on the [Soroban Examples repo](https://github.com/stellar/soroban-examples/tags) - Create and fund an account to be used for test purposes on the same network your RPC instance is connected to - This account needs a small XLM balance to submit Soroban transactions - Tests can be run repeatedly with the same account - Set `--TargetNetworkTestAccountPublic` to the `StrKey` encoded public key of the account - Set `--TargetNetworkTestAccountSecret` to the `StrKey` encoded secret for the account --- ## Prerequisites(Admin-guide) The RPC service can be installed on bare metal or a virtual machine. It is natively supported on both Linux and Windows operating systems. ### Minimum Hardware Requirements | Node Type | CPU | RAM | Disk | AWS SKU | Google Cloud SKU | | --- | --- | --- | --- | --- | --- | | Stellar RPC | 4-8 vCPUs | 16GB | 350 GB persistent volume >= 3K IOPS | [c5.2xlarge] | [n4-highcpu-8] | _\* Disk: Assuming the default 7-day retention window for data storage. Otherwise, +40GB per retention day_ _\* RAM/CPU: Assuming RPC will serve a modest request volume (e.g. 100 requests per second across all endpoints). For deployments expecting heavier request volume to Stellar RPC (e.g. >500 requests per second), we recommend horizontal scaling and load balancing across at least 32GB of RAM and an appropriately scaled CPU._ We highly recommend using a local SSD disk for storage. RPC is compatible with network-based filesystems like AWS EBS but it will negatively impact its performance. [c5.2xlarge]: https://aws.amazon.com/ec2/instance-types/c5/ [n4-highcpu-8]: https://cloud.google.com/compute/docs/general-purpose-machines#n4-highcpu --- ## Running(Admin-guide) You can run the `stellar/stellar-rpc` container with the following command: ```bash docker run -p 8001:8001 -p 8000:8000 \ -v $HOME/rpc-config:/opt/stellar stellar/stellar-rpc \ --captive-core-config-path=/opt/stellar/stellar-captive-core.cfg \ --config-path=/opt/stellar/soroban-rpc-config.toml ``` ## Next Step Refer to [Monitoring](./monitoring.mdx) for more details on RPC runtime logging and metrics available. --- ## API Reference(Api-reference) View all RPC API information. --- ## Methods All you need to know about available RPC methods, parameters and responses, and making RPC requests. Don't know which endpoint you need to get what you want? A lot of the returned fields are deeply-nested [XDR structures](../../../../../learn/fundamentals/data-format/xdr.mdx#more-about-xdr), so it can be hard to figure out what kind of information is available in each of these. Here's a bit of a dive into what the "workhorse" endpoints provide, in decreasing order of granularity: - [`getLedgers`](./getLedgers.mdx) operates at the block level, providing you with the full, complete details of what occurred during application of that ledger (known as "ledger metadata", defined in the protocol by the [`LedgerCloseMeta`](https://github.com/stellar/stellar-xdr/blob/v22.0/Stellar-ledger.x#L539) union, specifically the `V1` iteration). Each of the subsequent endpoints is just a microscope into a subset of the data available provided by this endpoint. Metadata includes things like: - Details for recreating the blockchain's state (see [Ledger Headers](../../../../../learn/fundamentals/stellar-data-structures/ledgers.mdx#ledger-headers) for more). - The consensus information that led to the block closing (see [Stellar Consensus Protocol](../../../../../learn/fundamentals/stellar-consensus-protocol.mdx)). - The set of transactions, their respective operations, and the results of applying those transactions in this block (see [Transactions](../../../../../learn/fundamentals/transactions/operations-and-transactions.mdx)). - [`getTransaction(s)`](./getTransactions.mdx) operates across a span of ledgers or on a single transaction hash depending on the variant. The structured data here includes details such as: - The exact transaction structure ("envelope") that was submitted. - Results for each of the operations within the transaction. - All side-effects to ledger state that occurred as a result of this transaction. - [`getEvents`](./getEvents.mdx) lets you search for events that occurred over a ledger range. Events are emitted by the system and by smart contracts to communicate meaningful state changes to off chain indexers (see [Events](../../../../../learn/fundamentals/stellar-data-structures/events.mdx) for more). Each event is made up of topics (which you can filter on) and data which are `ScVal`s, Stellar's generic "value" type (see [Contract Development](../../../../../learn/fundamentals/contract-development/types/built-in-types.mdx) for more). - [`getLedgerEntries`](./getLedgerEntries.mdx), in contrast to the above endpoints, provides information about **live** on-chain state rather than historical actions. The [getLedgerEntries](./getLedgerEntries.mdx) page itself goes into detail on the different kinds of state stored on chain and how to fetch them. If you still aren't sure what you're looking for, remember that you can pass `xdrFormat: "json"` as a parameter to each of these endpoints to get a fully unpacked, human-readable JSON version of the XDR structures returned. You can look through these until you find what you need and go back to the Base64+unpack variation in your app. Equipped with an understanding of how to traverse XDR, the structure of the protocol, and your [favorite SDK](../../../../../tools/sdks/README.mdx), you should be able to find anything you want about the Stellar network with these endpoints. --- ## GetEvents meth.name === "getEvents")[0]} /> ### Using the Lab Let's test the example request for **Native XLM Transfer Events** directly on [the Stellar Laboratory](https://laboratory.stellar.org). The new Lab supports **sharable URLs** that prefill input fields based on query parameters. This makes it easy to share and revisit specific configurations. 👉 [View Native XLM Transfer Events example on the Lab](https://lab.stellar.org/endpoints/rpc/get-events?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$startLedger=572089&filters=%7B%22type%22:%22contract%22,%22contract_ids%22:%5B%22CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC%22%5D,%22topics%22:%5B%22%5B%5C%22AAAADwAAAAh0cmFuc2Zlcg==%5C%22,%5C%22*%5C%22,%5C%22*%5C%22,%5C%22*%5C%22%5D%22%5D%7D;;) ![Lab: Get Events](/assets/api/rpc/getevents.jpg) --- ## GetFeeStats meth.name === "getFeeStats")[0]} /> ### Using the Lab You can view the current **mainnet fee stats** using [`mainnet.sorobanrpc.com`](https://sorobanrpc.com), powered by Overcat's RPC service, directly in the [Stellar Laboratory](https://laboratory.stellar.org). 👉 [View Mainnet Fee Stats on the Lab](https://lab.stellar.org/endpoints/rpc/get-fee-stats?$=network$id=mainnet&label=Mainnet&horizonUrl=https:////horizon.stellar.org&rpcUrl=https:////mainnet.sorobanrpc.com&passphrase=Public%20Global%20Stellar%20Network%20/;%20September%202015;;) ![Lab: Get Fee Stats](/assets/api/rpc/getfeestats.png) --- ## GetHealth meth.name === "getHealth")[0]} /> ### Using the Lab You can check the **node health status** on both **Testnet** and **Mainnet** using the `getHealth` RPC method directly in the [Stellar Laboratory](https://laboratory.stellar.org). 👉 [View Testnet Node Health on the Lab](https://lab.stellar.org/endpoints/rpc/get-health?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;) ![Lab: getHealth](/assets/api/rpc/gethealth.png) --- ## GetLatestLedger meth.name === "getLatestLedger")[0]} /> ### Using the Lab You can check the **latest known ledger** on both **Testnet** and **Mainnet** using the `getLatestLedger` RPC method directly in the [Stellar Laboratory](https://laboratory.stellar.org). 👉 [View Testnet Latest Ledger on the Lab](https://lab.stellar.org/endpoints/rpc/get-latest-ledger?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;) ![Lab: getLatestLedger](/assets/api/rpc/getlatestledger.png) --- ## Building ledger keys meth.name === "getLedgerEntries")[0]} /> # Building ledger keys The Stellar ledger is, on some level, essentially a key-value store. The keys are instances of [`LedgerKey`](https://github.com/stellar/stellar-xdr/blob/v25.0/Stellar-ledger-entries.x#L588) and the values are instances of [`LedgerEntry`](https://github.com/stellar/stellar-xdr/blob/v25.0/Stellar-ledger-entries.x#L548). An interesting product of the store's internal design is that the key is a _subset_ of the entry: we'll see more of this later. The `getLedgerEntries` method returns the "values" (or "entries") for a given set of "keys". Ledger keys come in a lot of forms, and we'll go over the commonly used ones on this page alongside tutorials on how to build and use them. ## Types of `LedgerKey`s The source of truth should always be the XDR defined in the protocol. `LedgerKey`s are a union type defined in [Stellar-ledger-entries.x](https://github.com/stellar/stellar-xdr/blob/v25.0/Stellar-ledger-entries.x#L588). There are 10 different forms a ledger key can take: 1. **Account:** holistically defines a Stellar account, including its balance, signers, etc. (see [Accounts](../../../../../learn/fundamentals/stellar-data-structures/accounts.mdx)) 2. **Trustline:** defines a balance line to a non-native asset issued on the network (see [`changeTrustOp`](../../../../../learn/fundamentals/transactions/list-of-operations.mdx#change-trust)) 3. **Offer:** defines an offer made on the Stellar DEX (see [Liquidity on Stellar](../../../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx)) 4. **Account Data:** defines key-value data entries attached to an account (see [`manageDataOp`](../../../../../learn/fundamentals/transactions/list-of-operations.mdx#manage-data)) 5. **Claimable Balance:** defines a balance that may or may not actively be claimable (see [Claimable Balances](../../../../../build/guides/transactions/claimable-balances.mdx)) 6. **Liquidity Pool:** defines the configuration of a native constant liquidity pool between two assets (see [Liquidity on Stellar](../../../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx)) 7. **Contract Data:** defines a piece of data being stored in a contract under a key 8. **Contract Code:** defines the Wasm bytecode of a contract 9. **Config Setting:** defines the currently active network configuration 10. **TTL:** defines the time-to-live of an associated contract data or code entry We're going to focus on a subset of these for maximum value, but once you understand how to build and parse some keys and entries, you can extrapolate to all of them. ### Accounts To fetch an account, all you need is its public key: ```typescript const publicKey = "GALAXYVOIDAOPZTDLHILAJQKCVVFMD4IKLXLSZV5YHO7VY74IWZILUTO"; const accountLedgerKey = xdr.LedgerKey.ledgerKeyAccount( new xdr.LedgerKeyAccount({ accountId: Keypair.fromPublicKey(publicKey).xdrAccountId(), }), ); console.log(accountLedgerKey.toXDR("base64")); ``` ```python from stellar_sdk import Keypair, xdr public_key = "GALAXYVOIDAOPZTDLHILAJQKCVVFMD4IKLXLSZV5YHO7VY74IWZILUTO" account_ledger_key = xdr.LedgerKey( type=xdr.LedgerEntryType.ACCOUNT, account=xdr.LedgerKeyAccount( account_id=Keypair.from_public_key(public_key).xdr_account_id() ), ) print(account_ledger_key.to_xdr()) ``` This will give you the full account details. ```typescript const accountEntryData = ( await s.getLedgerEntries(accountLedgerKey) ).entries[0].account(); ``` ```python account_entry_data = xdr.LedgerEntryData.from_xdr( server.get_ledger_entries([account_ledger_key]).entries[0].xdr ).account ``` If you just want to take a look at the structure, you can pass the raw base64 value we logged above to the [Laboratory](https://lab.stellar.org/endpoints/rpc/get-ledger-entries?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$xdrFormat=json;;) (or via `curl` if you pass `"xdrFormat": "json"` as an additional parameter to `getLedgerEntries`) and see all of the possible fields. You can also dig into them in code, of course: ```typescript console.log( `Account ${publicKey} has ${accountEntryData .balance() .toString()} stroops of XLM and is on sequence number ${accountEntryData .seqNum() .toString()}`, ); ``` ```python print( f"Account {public_key} has {account_entry_data.balance.int64} stroops of XLM and is on sequence number {account_entry_data.seq_num.sequence_number.int64}" ) ``` ### Trustlines A trustline is a balance entry for any non-native asset (such as [Circle's USDC](https://stellar.expert/explorer/public/asset/USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN)). To fetch one, you need the trustline owner (a public key like for [Accounts](#accounts)) and the asset in question: ```typescript const trustlineLedgerKey = xdr.LedgerKey.ledgerKeyTrustLine( new xdr.LedgerKeyTrustLine({ accountId: Keypair.fromPublicKey(publicKey).xdrAccountId(), asset: new Asset( "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", ).toTrustLineXDRObject(), }), ); ``` ```python trustline_ledger_key = xdr.LedgerKey( type=xdr.LedgerEntryType.TRUSTLINE, trust_line=xdr.LedgerKeyTrustLine( account_id=Keypair.from_public_key(public_key).xdr_account_id(), asset=Asset( "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" ).to_trust_line_asset_xdr_object(), ), ) trustline_entry_data = xdr.LedgerEntryData.from_xdr( server.get_ledger_entries([trustline_ledger_key]).entries[0].xdr ).trust_line ``` Much like an [account](#accounts), the resulting entry has a balance, but it also has a limit and flags to control how much of that asset can be held. The asset, however, can be either an issued asset or a liquidity pool: ```typescript let asset: string; let rawAsset = trustlineEntryData.asset(); switch (rawAsset.switch().value) { case AssetType.assetTypeCreditAlphanum4().value: asset = Asset.fromOperation( xdr.Asset.assetTypeCreditAlphanum4(rawAsset.alphaNum4()), ).toString(); break; case AssetType.assetTypeCreditAlphanum12().value: asset = Asset.fromOperation( xdr.Asset.assetTypeCreditAlphanum12(rawAsset.alphaNum12()), ).toString(); break; case AssetType.assetTypePoolShare().value: asset = rawAsset.liquidityPoolId().toXDR("hex"); break; } console.log( `Account ${publicKey} has ${trustlineEntryData .balance() .toString()} stroops of ${asset} with a limit of ${trustlineEntryData .limit() .toString()}`, ); ``` ```python raw_asset = trustline_entry_data.asset asset: str = "" if ( raw_asset.type == xdr.AssetType.ASSET_TYPE_CREDIT_ALPHANUM4 or raw_asset.type == xdr.AssetType.ASSET_TYPE_CREDIT_ALPHANUM12 ): asset_obj = Asset.from_xdr_object(raw_asset) asset = f"{asset_obj.code}:{asset_obj.issuer}" elif raw_asset.type == xdr.AssetType.ASSET_TYPE_POOL_SHARE: asset_obj = LiquidityPoolId.from_xdr_object(raw_asset) asset = f"{asset_obj.liquidity_pool_id}" else: raise ValueError("Invalid asset type") print( f"Account {public_key} has {trustline_entry_data.balance.int64} stroops of {asset} with a limit of {trustline_entry_data.limit.int64}" ) ``` ### Contract Data Suppose we've deployed the [`increment` example contract] and want to find out what value is stored in the `COUNTER` ledger key. To build the key: ```typescript const getLedgerKeySymbol = ( contractId: string, symbolText: string, ): xdr.LedgerKey => { return xdr.LedgerKey.contractData( new xdr.LedgerKeyContractData({ contract: new Address(contractId).toScAddress(), key: xdr.ScVal.scvSymbol(symbolText), // The increment contract stores its state in persistent storage, // but other contracts may use temporary storage // (xdr.ContractDataDurability.temporary()). durability: xdr.ContractDataDurability.persistent(), }), ); }; const ledgerKey = getLedgerKeySymbol( "CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI", "COUNTER", ); ``` ```python from stellar_sdk import xdr, scval, Address def get_ledger_key_symbol(contract_id: str, symbol_text: str) -> str: ledger_key = xdr.LedgerKey( type=xdr.LedgerEntryType.CONTRACT_DATA, contract_data=xdr.LedgerKeyContractData( contract=Address(contract_id).to_xdr_sc_address(), key=scval.to_symbol(symbol_text), durability=xdr.ContractDataDurability.PERSISTENT ), ) return ledger_key.to_xdr() print( get_ledger_key_symbol( "CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI", "COUNTER" ) ) ``` ### Contract Wasm Code To understand this, we need a handle on how smart contract deployment works: - When you deploy a contract, first the code is "installed" (i.e. uploaded onto the blockchain), creating a `LedgerEntry` with the Wasm byte-code that can be uniquely identified by its hash (that is, the hash of the uploaded code itself). - Then, when a contract _instance_ is "instantiated," we create a `LedgerEntry` with a reference to that code's hash. This means many contracts can point to the same Wasm code. Thus, fetching the contract code is a two-step process: 1. First, we look up the contract itself, to see which code hash it is referencing. 2. Then, we can look up the raw Wasm byte-code using that hash. #### 1. Find the ledger key for the contract instance ```typescript function getLedgerKeyContractCode(contractId): xdr.LedgerKey { return new Contract(contractId).getFootprint(); } console.log( getLedgerKeyContractCode( "CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI", ), ); ``` ```python from stellar_sdk import xdr, Address def get_ledger_key_contract_code(contract_id: str) -> xdr.LedgerKey: return xdr.LedgerKey( type=xdr.LedgerEntryType.CONTRACT_DATA, contract_data=xdr.LedgerKeyContractData( contract=Address(contract_id).to_xdr_sc_address(), key=xdr.SCVal(xdr.SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE), durability=xdr.ContractDataDurability.PERSISTENT ) ) print(get_ledger_key_contract_code( "CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI" )) ``` Once we have the ledger entry (via `getLedgerEntries`, see [below](#actually-fetching-the-ledger-entry-data)), we can extract the Wasm hash: #### 2. Request the `ContractCode` using the retrieved `LedgerKey` Now take the `xdr` field from the previous response's `result` object, and create a `LedgerKey` from the hash contained inside. ```typescript function getLedgerKeyWasmId( contractData: xdr.ContractDataEntry, ): xdr.LedgerKey { const wasmHash = contractData.val().instance().executable().wasmHash(); return xdr.LedgerKey.contractCode( new xdr.LedgerKeyContractCode({ hash: wasmHash, }), ); } ``` ```python from stellar_sdk import xdr def get_ledger_key_wasm_id( # received from getLedgerEntries and decoded contract_data: xdr.ContractDataEntry ) -> xdr.LedgerKey: # First, we dig the wasm_id hash out of the xdr we received from RPC wasm_hash = contract_data.val.instance.executable.wasm_hash # Now, we can create the `LedgerKey` as we've done in previous examples ledger_key = xdr.LedgerKey( type=xdr.LedgerEntryType.CONTRACT_CODE, contract_code=xdr.LedgerKeyContractCode( hash=wasm_hash ), ) return ledger_key ``` Now, finally we have a `LedgerKey` that correspond to the Wasm byte-code that has been deployed under the `contractId` we started out with so very long ago. This `LedgerKey` can be used in a final request to `getLedgerEntries`. In that response we will get a `LedgerEntryData` corresponding to a `ContractCodeEntry` which will contain the actual, deployed, real-life contract byte-code: ```typescript const theHashData: xdr.ContractDataEntry = await getLedgerEntries( getLedgerKeyContractCode("C..."), ).entries[0].contractData(); const theCode: Buffer = await getLedgerEntries(getLedgerKeyWasmId(theHashData)) .entries[0].contractCode() .code(); ``` ```python the_hash_data = xdr.LedgerEntryData.from_xdr( server.get_ledger_entries([get_ledger_key_contract_code("C...")]).entries[0].xdr ).contract_data the_code = xdr.LedgerEntryData.from_xdr( server.get_ledger_entries([get_ledger_key_wasm_id(the_hash_data)]).entries[0].xdr ).contract_code.code ``` ## Actually fetching the ledger entry data Once we've learned to _build_ and _parse_ these (which we've done above at length), the process for actually fetching them is always identical. If you know the type of key you fetched, you apply the accessor method accordingly once you've received them from the `getLedgerEntries` method: ```typescript const s = new Server("https://soroban-testnet.stellar.org"); // assume key1 is an account, key2 is a trustline, and key3 is contract data const response = await s.getLedgerEntries(key1, key2, key3); const account = response.entries[0].account(); const trustline = response.entries[1].trustline(); const contractData = response.entries[2].contractData(); ``` ```python server = SorobanServer("https://soroban-testnet.stellar.org") # assume key1 is an account, key2 is a trustline, and key3 is contract data response = server.get_ledger_entries([key1, key2, key3]) account = xdr.LedgerEntryData.from_xdr(response.entries[0].xdr).account trustline = xdr.LedgerEntryData.from_xdr(response.entries[1].xdr).trust_line contract_data = xdr.LedgerEntryData.from_xdr(response.entries[2].xdr).contract_data ``` Now, finally we have a `LedgerKey` that correspond to the Wasm byte-code that has been deployed under the `ContractId` we started out with so very long ago. This `LedgerKey` can be used in a final request to the Stellar-RPC endpoint. ```json { "jsonrpc": "2.0", "id": 12345, "method": "getLedgerEntries", "params": { "keys": [ "AAAAB+QzbW3JDhlUbDVW/C+1/5SIQDstqORuhpCyl73O1vH6", "AAAABgAAAAGfjJVEBc55drW3U87N1Py0Rw0/nlqUA6tQ6r28khEl4gAAABQAAAAB" "AAAABgAAAAAAAAABn4yVRAXOeXa1t1POzdT8tEcNP55alAOrUOq9vJIRJeIAAAAUAAAAAQAAABMAAAAA5DNtbckOGVRsNVb8L7X/lIhAOy2o5G6GkLKXvc7W8foAAAAA" ] } } ``` Then you can inspect them accordingly. Each of the above entries follows the XDR for that `LedgerEntryData` structure precisely. For example, the `AccountEntry` is in [`Stellar-ledger-entries.x#L190`](https://github.com/stellar/stellar-xdr/blob/v25.0/Stellar-ledger-entries.x#L190) and you can use `.seqNum()` to access its current sequence number, as we've shown. In JavaScript, you can see the appropriate methods in the [type definition](https://github.com/stellar/js-stellar-base/blob/6930a70d7fbde675514b5933baff605d97453ba7/types/curr.d.ts#L3034). ## Viewing and understanding XDR If you don't want to parse the XDR out programmatically, you can also leverage both the [Stellar CLI](../../../../../tools/cli/stellar-cli.mdx) and the [Stellar Lab](https://lab.stellar.org/xdr/view) to get a human-readable view of ledger keys and entries. For example, ```bash echo 'AAAAAAAAAAAL76GC5jcgEGfLG9+nptaB9m+R44oweeN3EcqhstdzhQ==' | stellar xdr decode --type LedgerKey --output json-formatted { "account": { "account_id": "GAF67IMC4Y3SAEDHZMN57J5G22A7M34R4OFDA6PDO4I4VINS25ZYLBZZ" } } ``` [`increment` example contract]: ../../../../../build/smart-contracts/getting-started/storing-data ["View XDR" page of the Stellar Lab]: https://lab.stellar.org/xdr/view?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;; ## Using the Lab The `getLedgerEntries` method allows you to **read live ledger data directly** from the network. This includes entries such as **accounts**, **trustlines**, **offers**, **data**, **claimable balances**, **liquidity pools**, and more. It's especially useful for inspecting a contract's **current state**, **deployed code**, or any other ledger entry tied to your application. This method is often the **primary way to retrieve contract-related data** that may not surface through events or `simulateTransaction`. To retrieve a contract’s WASM byte-code, use the `ContractCode` ledger entry key. 👉 [View getLedgerEntries on the Lab](https://lab.stellar.org/endpoints/rpc/get-ledger-entries?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;) Using the [Stellar XDR to JSON library](https://github.com/stellar/js-stellar-xdr-json), the `getLedgerEntries` method can dynamically generate input fields based on XDR-encoded data. For example, consider the following XDR string: `AAAABgAAAAHMA/50/Q+w3Ni8UXWm/trxFBfAfl6De5kFttaMT0/ACwAAABAAAAABAAAAAgAAAA8AAAAHQ291bnRlcgAAAAASAAAAAAAAAAAg4dbAxsGAGICfBG3iT2cKGYQ6hK4sJWzZ6or1C5v6GAAAAAE=` Try it in [the Lab](https://lab.stellar.org/endpoints/rpc/get-ledger-entries?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$ledgerKeyEntries=%5B%22AAAABgAAAAHMA//50//Q+w3Ni8UXWm//trxFBfAfl6De5kFttaMT0//ACwAAABAAAAABAAAAAgAAAA8AAAAHQ291bnRlcgAAAAASAAAAAAAAAAAg4dbAxsGAGICfBG3iT2cKGYQ6hK4sJWzZ6or1C5v6GAAAAAE=%22%5D;;) ![Lab: getledgerentries](/assets/api/rpc/getledgerentries-01.gif) Let's submit `getLedgerEntries` for the following XDR string: `AAAABgAAAAGUvl2TPOjIsxuZgSyt3Lf0d6R2iNYu4rKDuULTaMKUSgAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAABdOuyYDwLteYrby3aOykd5c12LYrui/nhbXOgtejCSYAAAAAE=` Try it in [the Lab](https://lab.stellar.org/endpoints/rpc/get-ledger-entries?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$ledgerKeyEntries=%5B%22AAAABgAAAAGUvl2TPOjIsxuZgSyt3Lf0d6R2iNYu4rKDuULTaMKUSgAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAABdOuyYDwLteYrby3aOykd5c12LYrui//nhbXOgtejCSYAAAAAE=%22%5D&xdrFormat=json;;) ![Lab: getledgerentries-02](/assets/api/rpc/getledgerentries-02.gif) --- ## GetLedgers meth.name === "getLedgers")[0]} /> ### Using the Lab You can retrieve a list of **past ledgers** using the `getLedgers` method in the [Stellar Laboratory](https://laboratory.stellar.org). This endpoint returns detailed ledger data starting from a specified point, with support for pagination—as long as the requested range falls within the history retention of the RPC provider. The method also supports an optional `XDR format` parameter, which allows developers to choose between **unpacked JSON** or **base64-encoded XDR strings** for the response format. 👉 [View getLedgers on the Lab](https://lab.stellar.org/endpoints/rpc/get-ledgers?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$startLedger=573041&xdrFormat=json;;) ![Lab: getLedgers](/assets/api/rpc/getledgers.gif) --- ## GetNetwork meth.name === "getNetwork")[0]} /> ### Using the Lab You can check **general information about the currently configured network** on both **Testnet** and **Mainnet** using the `getNetwork` RPC method directly in the [Stellar Laboratory](https://laboratory.stellar.org). 👉 [View Testnet getNetwork on the Lab](https://lab.stellar.org/endpoints/rpc/get-network?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;) ![Lab: getnetwork](/assets/api/rpc/getnetwork.png) --- ## pip install --upgrade stellar-sdk meth.name === "getTransaction")[0]} /> ### SDK Guide The example above is querying details of a transaction using RPC methods directly. If you are using the Stellar SDK to build applications, you can use the native functions to get the same information. ```python # pip install --upgrade stellar-sdk from stellar_sdk import SorobanServer, soroban_rpc def get_transaction(hash: str) -> soroban_rpc.GetTransactionResponse: server = SorobanServer(server_url='https://soroban-testnet.stellar.org', client=None) tx = server.get_transaction(hash) return tx tx = get_transaction("6bc97bddc21811c626839baf4ab574f4f9f7ddbebb44d286ae504396d4e752da") print("result", tx.status) ``` ```js // yarn add @stellar/stellar-sdk const server = new Server("https://soroban-testnet.stellar.org"); // Fetch transaction details async function getTransactionDetails(hash) { try { server.getTransaction(hash).then((tx) => { console.log({ result: tx }); }); } catch (error) { console.error("Error fetching transaction:", error); } } getTransactionDetails( "6bc97bddc21811c626839baf4ab574f4f9f7ddbebb44d286ae504396d4e752da", ); ``` ```java // implementation 'network.lightsail:stellar-sdk:3.1.0' public class GetTransactionExample { public static void main(String[] args) { SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org"); try { GetTransactionResponse tx = server.getTransaction("6bc97bddc21811c626839baf4ab574f4f9f7ddbebb44d286ae504396d4e752da"); System.out.println("result: " + tx); } catch (Exception e) { System.err.println("An error has occurred:"); e.printStackTrace(); } } } ``` ### Using the Lab You can use the `getTransaction` method in the [Stellar Laboratory](https://laboratory.stellar.org) to **retrieve details about a specific transaction**. This is especially useful for checking the status of a transaction to determine whether it has been successfully recorded on the blockchain. `stellar-rpc` retains a bounded, ledger-denominated history of recent transactions and events, configured by a single `history-retention-window` setting. The stock default is **120960 ledgers** (about **7 days**, depending on ledger cadence), and operators of private instances can adjust it. You can inspect the active window and the range of ledgers available on an instance with the [`getHealth`](./getHealth.mdx) method (`ledgerRetentionWindow`, `oldestLedger`, `latestLedger`). For transaction debugging beyond the retained window, consider indexing the data yourself, using a third-party indexer, or querying **Hubble**—Stellar’s public BigQuery dataset. 👉 [View getTransaction on the Lab](https://lab.stellar.org/endpoints/rpc/get-transaction?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;) (The following transaction hash is a random one I picked from the latest at the time of writing this doc) ![Lab: getTransaction](/assets/api/rpc/gettransaction.gif) --- ## GetTransactions meth.name === "getTransactions")[0]} /> ### Using the Lab The `getTransactions` method returns a **paginated list of transactions** starting from a user-defined point. You can continue paginating through results as long as the requested range falls within the **history retention window** of the corresponding RPC provider. Use this method in the [Stellar Laboratory](https://laboratory.stellar.org) to explore recent activity within a given time frame or ledger range. 👉 [View Transactions on the Lab](https://lab.stellar.org/endpoints/rpc/get-transactions?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$startLedger=573331;;) ![Lab: gettransactions](/assets/api/rpc/gettransactions.png) --- ## GetVersionInfo meth.name === "getVersionInfo")[0]} /> ### Using the Lab The `getVersionInfo` method provides **version details** about the RPC service and its underlying **Captive Core**—a pared-down, embedded version of Stellar Core optimized specifically for Soroban RPC operations. This is useful for debugging, support, or ensuring compatibility between different components in your development or production environments. 👉 [View Version Info on the Lab](https://lab.stellar.org/endpoints/rpc/get-version-info?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;) ![Lab: getVersionInfo](/assets/api/rpc/getversioninfo.png) --- ## pip install --upgrade stellar-sdk(Methods) meth.name === "sendTransaction")[0]} /> ### SDK Guide The example above is sending a transaction using RPC methods directly. If you are using the Stellar SDK to build applications, you can use the native functions to get the same information. ```python # pip install --upgrade stellar-sdk from stellar_sdk import SorobanServer, soroban_rpc, Keypair, Network, TransactionBuilder, scval def send_transaction() -> soroban_rpc.SendTransactionResponse: server = SorobanServer(server_url='https://soroban-testnet.stellar.org', client=None) root_keypair = Keypair.from_secret( "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ) root_account = server.load_account(root_keypair.public_key) # native token contract (XLM) contract_id = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" transaction = ( TransactionBuilder( source_account=root_account, network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, base_fee=100, ) # Transfer 1 native token to GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H # https://developers.stellar.org/docs/tokens/token-interface .append_invoke_contract_function_op(contract_id, "transfer", [ scval.to_address(root_keypair.public_key), # from scval.to_address("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H"), # to scval.to_int128(1 * 10 ** 7) # amount, 1 XLM, decimal places are 7 ]) .set_timeout(30) .build() ) transaction = server.prepare_transaction(transaction) transaction.sign(root_keypair) return server.send_transaction(transaction) response = send_transaction() print("status", response.status) print("hash:", response.hash) print("status:", response.status) print("errorResultXdr:", response.error_result_xdr) ``` ```js // yarn add @stellar/stellar-sdk const server = new Server("https://soroban-testnet.stellar.org"); async function sendTransaction() { try { // native token contract (XLM) const contractId = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; const sourceSecretKey = "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; const contract = new StellarSdk.Contract(contractId); const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSecretKey); const accountId = sourceKeypair.publicKey(); const account = await server.getAccount(accountId); const fee = StellarSdk.BASE_FEE; // Transfer 1 native token to GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H // https://developers.stellar.org/docs/tokens/token-interface const transaction = new StellarSdk.TransactionBuilder(account, { fee }) .setNetworkPassphrase(StellarSdk.Networks.TESTNET) .setTimeout(30) .addOperation( contract.call( "transfer", StellarSdk.nativeToScVal(accountId, { type: "address" }), // from StellarSdk.nativeToScVal( "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", { type: "address" }, ), // to StellarSdk.nativeToScVal("10000000", { type: "i128" }), // amount, 1 XLM, decimal places are 7 ), ) .build(); server .prepareTransaction(transaction) .then((result) => { result.sign(sourceKeypair); return server.sendTransaction(result); }) .then((result) => { console.log("hash:", result.hash); console.log("status:", result.status); console.log("errorResultXdr:", result.errorResultXdr); }); } catch (error) { console.error("Error fetching transaction:", error); } } sendTransaction(); ``` ```java // https://github.com/lightsail-network/java-stellar-sdk?tab=readme-ov-file#installation public class SendTransactionExample { public static void main(String[] args) { SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org"); try { KeyPair sourceKeyPair = KeyPair.fromSecretSeed("SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); TransactionBuilderAccount sourceAccount = server.getAccount(sourceKeyPair.getAccountId()); // native token contract (XLM) String contractId = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; // Transfer 1 native token to GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H // https://developers.stellar.org/docs/tokens/token-interface org.stellar.sdk.operations.InvokeHostFunctionOperation operation = InvokeHostFunctionOperation.invokeContractFunctionOperationBuilder( contractId, "transfer", Arrays.asList( Scv.toAddress(sourceKeyPair.getAccountId()), // from Scv.toAddress( "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H"), // to Scv.toInt128( BigInteger.valueOf(10000000)) // amount, 1 XLM, decimal places are 7 )) .build(); Transaction transaction = new TransactionBuilder(sourceAccount, Network.TESTNET) .setBaseFee(100) .addOperation(operation) .setTimeout(30) .build(); transaction = server.prepareTransaction(transaction); // Sign the transaction transaction.sign(sourceKeyPair); // Send the transaction using the SorobanServer SendTransactionResponse response = server.sendTransaction(transaction); System.out.println(response.getStatus()); System.out.println(response.getHash()); System.out.println(response.getLatestLedger()); System.out.println(response.getLatestLedgerCloseTime()); } catch (Exception e) { System.err.println("An error has occurred:"); e.printStackTrace(); } } } ``` ### Using the Lab The `sendTransaction` method is used to **submit a real transaction to the Stellar network**, making it the only way to execute **on-chain changes** through RPC. Unlike Horizon, this method **does not wait for confirmation**. Instead, it **validates and enqueues** the transaction. To track its final outcome, clients should follow up with a call to [`getTransaction`](../methods/getTransaction.mdx). This method supports **all Stellar transactions**, including but not limited to smart contract invocations. 👉 [Send (Submit) a Transaction on the Lab](https://lab.stellar.org/endpoints/rpc/send-transaction?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;) ![Lab: sendTransaction](/assets/api/rpc/sendtransaction.gif) --- ## SimulateTransaction meth.name === "simulateTransaction")[0] } /> :::note `simulateTransaction` can also invoke read-only functions for free. ::: ### Using the Lab The `simulateTransaction` method allows you to **simulate a smart contract invocation** without actually submitting it to the network. It’s a powerful tool for testing and debugging transactions safely. This endpoint returns the **calculated transaction data**, **required authorizations**, and the **minimal resource fee**, helping you understand how the network would process the transaction before you send it. 👉 [Simulate a Transaction on the Lab](https://lab.stellar.org/endpoints/rpc/simulate-transaction?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;) ![Lab: simulateTransaction](/assets/api/rpc/simulatetransaction.gif) --- ## RPC API Reference How the RPC API is structured. --- ## Data Formats ### XDR Format In the Stellar network, transactions are encoded using a standardized protocol called [External Data Representation](../../../../../learn/fundamentals/data-format/xdr.mdx) (XDR). In RPC, you will encounter XDR when [simulating](../methods/simulateTransaction) and [sending](../methods/sendTransaction) transactions, as well as when retrieving [transactions](../methods/getTransactions), [ledgers](../methods/getLedgers), and [ledger entries](../methods/getLedgerEntries). By default, RPC will return all XDR attributes as the machine-readable base64-encoded string. XDR-encoded response fields are usually suffixed with `Xdr`. You can decode this XDR on the Stellar Lab's [XDR page](https://lab.stellar.org/xdr/view?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;). ### JSON Format All RPC endpoints which return encoded XDR fields accept the `xdrFormat` attribute. This allows a client to change the response format to JSON, ultimately making it more human-readable. Note that you should not rely on any schema for the JSON, as it will change when the underlying XDR changes. In the event that a `json` output format is requested, response fields suffixed with `Xdr` will be omitted and replaced with their `Json` suffixed counterparts. ```json { // xdrFormat = 'base64' "resultMetaXdr": "AAAAAwAAAAAAAAACAAAAAwAWuFYAAAAAAAAAAEBQYAimx5waQHaAptKgy2a/IAHMSe96ETt5wiMOSpKXAAAAF0JZ4rAAACHUAAekYwAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAABa4VAAAAABnyjD0AAAAAAAAAAEAFrhWAAAAAAAAAABAUGAIpsecGkB2gKbSoMtmvyABzEnvehE7ecIjDkqSlwAAABdCWeKwAAAh1AAHpGQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAWuFYAAAAAZ8ow/gAAAAAAAAAAAAAAAAAAAAA=", // xdrFormat = 'json' "resultMetaJson": { "tx": { "tx": { "source_account": "GCW2NGRNWISFIXSGCBXPU23GDBAMHIOQOWO52LZWEWNELRC4XDQMDWNT", "fee": 100, "seq_num": 58248347706520, "cond": { "time": { "min_time": 0, "max_time": 1741309515 } }, "memo": { "text": "6764203ea0bcd7058f922d32" }, "operations": [ { "source_account": null, "body": { "payment": { "destination": "GBZA3UPYFIFXXPH4QPQSFL73TZND7MEN744BLU4DPBR4T2XS526PRRN2", "asset": "native", "amount": 635808330 } } } ], "ext": "v0" }, "signatures": [ { "hint": "5cb8e0c1", "signature": "cfcfc8192be1448b6c1e06265bd89b7375fdd818927f5167071d1e4f383522e6968771c91272fc248b4984f3f8c3ea7089396ab63c5dc557a45130c307f1840a" } ] } } } ``` --- ## JSON-RPC Stellar-RPC will accept HTTP POST requests using the [JSON-RPC 2.0] specification. Errors are returned via the [jsonrpc error object] wherever possible (and makes sense). For production and other publicly-accessible instances, the JSON-RPC endpoint should be served over SSL on port 443, where possible, for security and ease of use. Though, stellar-rpc does not terminate ssl by itself, so will need a load-balancer or other service to terminate SSL for it. To interact with stellar-rpc from inside a JavaScript application, use the [JavaScript SDK] package, which gives a convenient interface for the RPC methods inside of its `rpc` module. When XDR is passed as a parameter or returned, it is always a string encoded using standard base64. :::info Please note that parameter structure must contain named parameters as a by-name object, and not as positional arguments as a by-position array. Positional arguments as a by-position array will be deprecated in future RPC releases. ::: ## Error codes | Error code | Meaning | | :--------- | :------------------------------------------- | | -32600 | The JSON sent is not a valid Request object | | -32601 | The method does not exist / is not available | | -32602 | Invalid method parameter(s) | | -32603 | Internal JSON-RPC error | ## Example Request ```json { "jsonrpc": "2.0", "id": "1", "method": "getTransaction", "params": { "hash": "5ee7e055afb3b0b13fd6ac6d8adb11f799df8593448e23ad93454fefc387bbfa" } } ``` ## Example Response ```js { "jsonrpc": "2.0", "id": "1", "result": { "status": "SUCCESS", // ... } } ``` ## Open-RPC Specification Stellar-RPC provides an [OpenRPC] specification document that can be used to mock, build, and/or validate both server and client software implementations. This document is used to generate all of our [methods] documentation pages. You can view the full [specification document here]. Additionally, you can experiment with this specificaiton document in the [OpenRPC Playground]. [JSON-RPC 2.0]: https://www.jsonrpc.org/specification [jsonrpc error object]: https://www.jsonrpc.org/specification#error_object [JavaScript SDK]: https://github.com/stellar/js-stellar-sdk [OpenRPC]: https://open-rpc.org/ [methods]: ../methods [specification document here]: https://raw.githubusercontent.com/stellar/stellar-docs/main/static/stellar-rpc.openrpc.json [OpenRPC Playground]: https://playground.open-rpc.org/?schemaUrl=https://raw.githubusercontent.com/stellar/stellar-docs/main/static/stellar-rpc.openrpc.json --- ## Pagination(Structure) For methods which support it, the pagination arguments are passed as a final object argument with two values: - `cursor`: string - (optional) An opaque string which acts as a paging token. Each response will include a `cursor` field which can be included in a subsequent request to obtain the next page of results. - `limit`: number - (optional) The maximum number of records returned. Each paginated method has a method-specific range and default. | Method | Limit range | Default limit | | --- | --- | --- | | [`getEvents`](../methods/getEvents.mdx) | 1 to 10000 | 100 | | [`getLedgers`](../methods/getLedgers.mdx) | 1 to 200 | 50 | | [`getTransactions`](../methods/getTransactions.mdx) | 1 to 200 | 50 | These upper limits are hardcoded in Stellar RPC for performance reasons. For example, calling a method with pagination parameter set: ```json { "jsonrpc": "2.0", "id": "1", "method": "exampleMethod", "params": { "some": "argument", "pagination": { "cursor": "1234-1", "limit": 100 } } } ``` --- ## Providers(Rpc) Multiple infrastructure providers have made Stellar RPC services available, and offer plans ranging from free to dedicated instances. These providers can be used for development, testing, and production. These providers allow access to the Futurenet, Testnet and Mainnet network. | Provider | Futurenet | Testnet | Mainnet | Dedicated Nodes | RPC Archive | | --- | --- | --- | --- | --- | --- | | [Blockdaemon](https://www.blockdaemon.com/apply/soroban) | ❌ | ✅ | ✅ | ✅ | ❌ | | [Validation Cloud](https://app.validationcloud.io) | ❌ | ✅ | ✅ | ❌ | ❌ | | [QuickNode](https://www.quicknode.com/docs/stellar) | ❌ | ✅ | ✅ | ✅ | ❌ | | [NowNodes](https://nownodes.io/nodes/stellar-xlm) | ✅ | ✅ | ✅ | ✅ | ❌ | | [Gateway\*](https://gateway.fm/public-rpc) | ❌ | ✅ | ✅ | ✅ | ✅ | | [Ankr](https://www.ankr.com/rpc/stellar) | ❌ | ✅ | ✅ | ❌ | ✅ | | [Infstones](https://infstones.com) | ❌ | ❌ | ✅ | ✅ | ❌ | | [Obsrvr\*](https://www.withObsrvr.com) | ❌ | ✅ | ✅ | ❌ | ✅ | | [Nodies](https://nodies.org) | ❌ | ✅ | ✅ | ❌ | ❌ | | [OnFinality\*](https://onfinality.io/networks/stellar) | ❌ | ❌ | ✅ | ✅ | ✅ | | [Lightsail Network - Quasar\*](https://quasar.lightsail.network) | ❌ | ❌ | ✅ | ❌ | ✅ | | [Uniblock](https://www.uniblock.dev) | ❌ | ✅ | ✅ | ❌ | ❌ | | [Exaion](https://crypto.exaion.com) | ❌ | ❌ | ✅ | ✅ | ✅ | | [Alchemy](https://dashboard.alchemy.com) | ❌ | ✅ | ✅ | ✅ | ❌ | | [GetBlock](https://getblock.io/nodes/xlm/) | ❌ | ❌ | ✅ | ✅ | ✅ | \*_RPC Archive is a new option for those looking to retrieve full ledger history. Currently only the [getLedgers](./api-reference/methods/getLedgers.mdx) RPC method supports this feature. You can choose one of the providers above, or create your own getLedgers archive by following [these steps](./admin-guide/data-lake-integration.mdx)._ _The "Dedicated Nodes" column represents providers who host full nodes as a service._ ### Publicly Accessible APIs | Provider | Network | URL | | --- | --- | --- | | [Liquify](https://www.liquify.io) | Futurenet | RPC: `https://stellar.liquify.com/api=41EEWAH79Y5OCGI7/futurenet` | | | Testnet | RPC: `https://stellar.liquify.com/api=41EEWAH79Y5OCGI7/testnet` | | | Mainnet | RPC: `https://stellar-mainnet.liquify.com/api=41EEWAH79Y5OCGI7/mainnet` | | [Gateway](https://gateway.fm) | Testnet | RPC: `https://soroban-rpc.testnet.stellar.gateway.fm` | | | Mainnet | RPC: `https://soroban-rpc.mainnet.stellar.gateway.fm` | | [sorobanrpc.com](https://sorobanrpc.com) | Mainnet | RPC: `https://mainnet.sorobanrpc.com` | | [Nodies](https://nodies.org) | Testnet | RPC: `https://stellar-soroban-testnet-public.nodies.app` | | | Mainnet | RPC: `https://stellar-soroban-public.nodies.app` | | [SDF](https://stellar.org) | Futurenet | RPC: `https://rpc-futurenet.stellar.org` | | | Testnet | RPC: `https://soroban-testnet.stellar.org` | | [OnFinality](https://onfinality.io/networks/stellar) | Mainnet | RPC: `https://stellar.api.onfinality.io/public` | | [Lightsail Network - Quasar](https://quasar.lightsail.network) | Mainnet | RPC `https://rpc.lightsail.network/` | | | Mainnet | Full Archive RPC: `https://archive-rpc.lightsail.network/` | | [Ankr](https://www.ankr.com/rpc/stellar) | Mainnet | Full Archive RPC `https://rpc.ankr.com/stellar_soroban` | --- ## Indexers Overview ## What is an indexer, and why would you need one? When you first start building on a smart contract platform like Stellar, at first you can get everything you need from RPC calls. You write a contract, or launch a Stellar Classic Asset and [wrap it in a smart contract](../../tokens/stellar-asset-contract.mdx), and you make a bundle of RPC calls from your frontend app directly to your smart contracts. At some point, though, you are likely to run into limitations with this approach. 1. You might need to make _too many_ RPC calls. Imagine: your app lists 20 of a user's NFTs per page. First RPC call: get a list of NFT IDs for this user. Then: 20 simultaneous RPC calls to get data for each NFT. And possibly: for each of those, make additional HTTP requests to S3 buckets or IPFS nodes to fetch image data, etc. This can make your app slow, or even get you rate-limited by an RPC provider. 2. You might want to show _historic_ data, like every time an NFT changed ownership. This sort of data lives _in the history of the blockchain_, but is not accessible via RPC calls to the most up-to-date version of a specific contract. Those would only return the current owner. Additionally, even some information that is currently available from [Horizon] will also require indexing services in the future, as Horizon will be deprecated. ## What exactly is indexing? The word "indexing" has come to encompass a large array of use-cases. What all of them have in common is that they process and structure blockchain data. Let's think of it in the order of what you, an app-builder or small team, will need: ### 1. Off-the-shelf APIs for common data {/* #portfolio-apis */} This is the kind of stuff you might get from Horizon today, but can include more than that. For example, many indexing services in this category also provide the "every time an NFT changed ownership" kind of data mentioned above. Lots of apps need this kind of data, and it tends to have a standard shape. This makes it profitable for companies to build SaaS products, offering API access for a price (often with a free tier, appropriate for your early-stage startup or hackathon project). In the biz, these are called _Portfolio APIs._ Well-known companies offering them: - [Alchemy]: a widely-used data provider (popular on Ethereum), now live on Stellar. Its [Stellar Data API](https://www.alchemy.com/docs/reference/stellar-data-api-overview) serves indexed **transfer history**, **account balances**, and **NFT holdings** across Stellar assets and contract tokens — letting you query this data without running your own indexer (JSON request bodies, opaque `pageKey` pagination). Alchemy also provides [Stellar RPC](https://www.alchemy.com/docs/stellar/stellar-api-overview#stellar-apis) and appears in the [RPC providers](../apis/rpc/providers.mdx) list. - [Allium]: currently building Stellar support, launching Q1 2026 - [OBSRVR]: a home-grown Stellar-native offering, providing both RPC services and [Obsrvr Gateway](https://www.withobsrvr.com/products/gateway), promising "powerful APIs for real-time data fetching, transaction processing, and easy integration into existing systems." - [Horizon]: as mentioned above, Horizon will soon be deprecated, but for now it is the only way to get some of this data. ### 2. Streaming & transforming data to a custom app database {/* #custom-transformations */} Not all apps have data that fits a standard shape. Most, probably, need custom data transformation. Consider again the NFT example. Wouldn't it be great to return all needed NFT data in _one_ request, rather than N+1? That's what this style of indexing provides. And even better: you can (often) use these indexing solutions to aggregate data from multiple sources. If your NFT app stores additional information on IPFS, you could consolidate all of that off-chain data to your database as well. This makes your app much faster, saves you network & RPC requests, and creates new architecture possibilities. Well-known options: - [The Graph]: one of the earliest and most popular-at-the-time options on Ethereum. They now offer three main products: 1. [Subgraphs](https://thegraph.com/docs/en/subgraphs/developing/subgraphs): their main & most famous offering, providing a [decentralized approach](https://thegraph.com/docs/en/subgraphs/developing/publishing/publishing-a-subgraph) to custom data transformation & hosting. The Graph's use of GraphQL APIs made GraphQL a popular choice for this entire category of indexing. 2. **Token API**, for Indexing [Use Case #1](#portfolio-apis) described above 3. **Substreams**, for [Use Case #3](#analytics) described below The Graph offers Stellar support for Substreams, with no current plans to expand Subgraph or Token API support to Stellar. - [Goldsky]: one of the currently-most-loved options on Ethereum for this use-case. Goldsky provides [two main products](https://docs.goldsky.com/subgraph-vs-mirror): 1. **Subgraphs**: similar to those offered by The Graph, but data lives on Goldsky's own infrastructure rather than a decentralized network. Goldsky only offers this for EVM-based chains, with no plans to offer Subgraph support for Stellar. 2. **Mirror**, also called **Pipelines**: a highly efficient tool to Extract, Transform, & Load (ETL) data into your own database. Goldsky Mirrors already support Stellar; see [their documentation](https://docs.goldsky.com/chains/stellar). - [Mercury]: a home-grown Stellar-native team providing streamlined Soroban (Smart Contract) support via their [Retroshades](https://docs.mercurydata.app/retroshades/introduction-to-retroshades) product. Note that this streamlined Soroban support comes at the cost of _only_ supporting Soroban! Mercury also provide [Mercury "Classic"](https://docs.mercurydata.app/mercury-classic/introduction), giving access to contract events & Stellar transactions via a GraphQL interface, which might fit Indexer Use Case #1 more. - [SubQuery]: Decentralized Indexer SDK, Decentralized RPCs, & AI Apps. Supports 300+ chains. Like The Graph, uses a decentralized model. - [OnFinality]: a big player in the Polkadot ecosystem, now expanding to other blockchains. OnFinality provides data _hosting_ services for your SubQuery logic. SubQuery is the _software_, OnFinality is the _infra_, hosting 1. the pre-transformed raw Stellar data, 2. your SubGraph SDK-authored Extract, Transform, and Load (ETL) processor and 3. your final transformed data. - [Allium]: in addition to their Portfolio APIs offering, Allium is also under contract with SDF to build out tools for both Indexing Use Case #2 (this one) and Indexing Use Case #3 (see below), with target launch date of Q1 2026. - [Space and Time]: one challenge with most indexing approaches is the reintroduction of trusted 3rd parties into what is otherwise a verifiable, trustless software stack. Space and Time aims to fix this with "Proof of Indexing" and "Proof of SQL", using Zero-Knowledge proofs to offer tamper-proof computation for enterprises and dapps. Space and Time's Stellar support [launched](https://www.spaceandtime.io/blog/space-and-time-enables-new-sophisticated-financial-apps-for-the-stellar-ecosystem) in Q4 2025. - [OBSRVR Flow]: "Structured ledger data and contract events straight to your app or warehouse—no ETL needed." Currently in private beta. ### 3. Blockchain-Flavored Big-Data Analytics {/* #analytics */} For business intelligence, compliance, tracing suspicious operations, DeFi metric tracking, transaction flow analysis, etc. When your enterprise reaches a certain scale, it's worth paying Data Engineers to set up custom ETL pipelines and manage databases/datalakes, and then paying Data Analysts to answer questions about how people are interacting with your systems. The companies building tools for Indexer Use Case #2 above (The Graph, Goldsky, Allium, etc) tend to also have tools for Use Case #3. While often referred to as _indexing_, you can also think of this category as _analytics_. See the [analytics documentation](../analytics/README.mdx) for solutions custom-tailored to this use-case. ## Build Your Own If none of the indexing providers mentioned above currently meet your needs, you can also build your own. Start with this [tutorial on how to build your own custom network ingestion pipeline](../../build/apps/ingest-sdk/overview.mdx). Along the way, you'll use the following tools & services: ### [Galexie](./build-your-own/galexie/README.mdx) Galexie is a tool for acquiring Stellar ledger metadata from the network and exporting to external storage, a data lake. Galexie is the foundation of the Composable Data Pipeline (CDP) and serves as the first step in extracting raw Stellar ledger metadata and making it accessible. Learn more about CDP’s benefits and applications in [this blog post](https://stellar.org/blog/developers/composable-data-platform). **Why Use It:** - You want to maintain a data lake of pre-computed ledger metadata for historical and currently closed network ledgers. ### [Ingest SDK](./build-your-own/ingest-sdk/README.mdx) A set of Golang packages which can be used within an application as a programmatic domain model to interact with Stellar network. **Why Use It:** - You want rapid development of applications in Golang which can acquire and parse ledger meta data and ledger entries from Stellar network. - You want an intuitive, compile-time, type-safe application developer experience. - You want to programmatically access History Archives to retrieve ledger entries. ### [Processors](./build-your-own/processors/README.mdx) A suite of Go packages that help you parse Stellar blockchain data. [Horizon]: ../apis/horizon/README.mdx [Allium]: https://www.allium.so/ [Alchemy]: https://www.alchemy.com/ [Goldsky]: https://goldsky.com/ [OBSRVR]: https://www.withobsrvr.com/ [The Graph]: https://thegraph.com/docs/en/about/ [Mercury]: https://docs.mercurydata.app/ [SubQuery]: https://subquery.network/ [OnFinality]: https://onfinality.io/ [Space and Time]: https://www.spaceandtime.io/ [OBSRVR Flow]: https://www.withobsrvr.com/products/flow --- ## Build Your Own If none of the indexing providers mentioned [in the overview](../README.mdx) meet your needs, you can also build your own. Start with this [tutorial on how to build your own custom network ingestion pipeline](../../../build/apps/ingest-sdk/overview.mdx). Along the way, you'll use the following tools & services: ## [Galexie](./galexie/README.mdx) Galexie is a tool for acquiring Stellar ledger metadata from the network and exporting to external storage,a data lake. Galexie is the foundation of the Composable Data Pipeline (CDP) and serves as the first step in extracting raw Stellar ledger metadata and making it accessible. Learn more about CDP’s benefits and applications in [this blog post](https://stellar.org/blog/developers/composable-data-platform). **Why Use It:** - You want to maintain a data lake of pre-computed ledger metadata for historical and currently closed network ledgers. ## [Ingest SDK](./ingest-sdk/README.mdx) A set of Golang packages which can be used within application as a programmatic domain model to interact with Stellar network. **Why Use It:** - You want rapid development of applications in Golang which can acquire and parse ledger meta data and ledger entries from Stellar network. - You want an intuitive, compile-time, type-safe application developer experience. - You want to programmatically access History Archives to retrieve ledger entries. ## [Processors](./processors/README.mdx) A suite of Go packages that help you parse Stellar blockchain data. --- ## Galexie ## What is Galexie? Galexie is a tool for extracting, processing, exporting Stellar ledger metadata to external storage, and creating a data lake of pre-processed ledger metadata. Galexie is the foundation of the Composable Data Pipeline (CDP) and serves as the first step in extracting raw Stellar ledger metadata and making it accessible. Learn more about CDP’s benefits and applications in this [blog post](https://stellar.org/blog/developers/composable-data-platform). ## What Are the Key Features of Galexie? Galexie is designed to make streamlined and efficient export of ledger metadata via a simple user-friendly interface. Its key features include: - Exporting Stellar ledger metadata to cloud storage - Configurable to export a specified range of ledgers or continuously stream new ledgers as they are created on the Stellar network - Exporting ledger metadata in XDR which is Stellar Core’s native format. - Compressing data before export to optimize storage efficiency in the data lake. ![](/assets/galexie/galexie-architecture.png) ## Why XDR Format? Exporting data in XDR—the native Stellar Core format—enables Galexie to preserve full transaction metadata, ensuring data integrity while keeping storage efficient. The XDR format maintains compatibility with all Stellar components, providing a solid foundation for applications that require consistent access to historical data. Refer to the [XDR](../../../../learn/fundamentals/data-format/xdr.mdx) documentation for more information on this format. ## Why Run Galexie? Galexie enables you to make a copy of Stellar ledger metadata over which you have complete control. Galexie can continuously sync your data lake with the latest ledger data freeing you up from tedious data ingestion and allowing you to focus on building customized applications that consume and analyze exported data. ## What Can You Do with the Data Lake Created by Galexie? Once data is stored in the cloud, it becomes easily accessible for integration with modern data processing and analytics tools, enabling various workflows and insights. The pre-processed ledger data exported by Galexie can be utilized across various applications, such as: - Analytics Tools: Analyze trends over time. - Audit Applications: Retrieve historical transaction data for auditing and compliance. - Monitoring Systems: Create tools to track network metrics. --- ## Admin Guide(Admin_guide) This guide provides step-by-step instructions on installing and running the Galexie. --- ## Configuring(Admin_guide) ## Steps to Configure Galexie ### Key Settings Include #### Cloud Storage Service Specify the cloud storage service to be used to export ledger metadata. Currently only `GCS` and `S3` are supported ```toml type = "GCS" ``` #### Cloud Storage Bucket Specify the cloud storage bucket where Galexie will export Stellar ledger data. Update `destination_bucket_path` to the complete path of your bucket, including subpaths if applicable. ```toml destination_bucket_path = "stellar-network-data/testnet" ``` #### Stellar Network Set the Stellar network to be used in creating the data lake. ```toml network = "testnet" ``` #### Data Organization (Optional) Configure how the exported data is organized in the storage bucket. The example below adds 1 ledger per file and organizes them in a directory of 64000 files. ```toml # Number of ledgers stored in each file ledgers_per_file = 1 # Number of files per partition/directory files_per_partition = 64000 ``` #### Use a Custom Core Config (Optional) You can specify a custom `core.cfg` file in the Galexie `config.toml` to use that will override the default core parameters used with the Stellar Network specified in the `network` parameter. Copy an existing `core.cfg` file such as the `captive-core-pubnet.cfg` provided [here](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/ledgerbackend/configs/captive-core-pubnet.cfg) and add the following to your `config.toml` ```toml captive_core_toml_path = "my-captive-core.cfg" ``` - Please see the [Choosing Your Quorum Set](../../../../../validators/admin-guide/configuring.mdx#choosing-your-quorum-set) for more information about `core.cfg` quorum set configuration - A list of customizable core parameters can be found [here](https://github.com/stellar/stellar-core/blob/master/src/main/Config.h) The default `core.cfg` used by Galexie will enable the following core parameters: - **BACKFILL_RESTORE_META -** ledger metadata will be populated with LedgerEntryChange RESTORE type for protocol versions prior to 23 - **ENABLE_SOROBAN_DIAGNOSTIC_EVENTS -** additional diagnostic Soroban events that are not part of the protocol will be generated while applying Soroban transactions - **EMIT_SOROBAN_TRANSACTION_META_EXT_V1 -** Soroban extension V1 data will be emitted - **EMIT_LEDGER_CLOSE_META_EXT_V1 -** ledger metadata extension V1 data will be emitted - **EMIT_CLASSIC_EVENTS -** classic events will be enabled and emitted for protocol `>= 23` - **BACKFILL_STELLAR_ASSET_EVENTS -** classic events will be enabled and emitted for protocol `<= 22`. This parameter also requires `EMIT_CLASSIC_EVENTS` to be enabled :::warning When you provide a custom core configuration file, it completely replaces the Galexie's default core configuration. This means you must explicitly set any parameters you want to enable, including the ones that are enabled by default in Galexie. Any parameter not listed in your custom config will be set to false. ::: ### Running Galexie on an Instance #### 1. Copy the Sample Configuration Start with the provided sample file, [`config.example.toml`](https://github.com/stellar/stellar-galexie/blob/main/config/config.example.toml). #### 2. Rename and Update the Configuration Rename the file to `config.toml` and adjust settings as needed. --- ## Full History Exporting This page outlines best practices for using Galexie to build a data lake with the complete history of ledger metadata. ## Why Full History Export? Exporting the full history of Stellar ledger metadata provides a complete data lake of everything that has occurred on-chain. This makes it easy and fast to retrieve data at any point in the network's history. This enables: - **Analytics** - run historical trend analysis with `stellar-etl` - **Full History RPC** - data lake backend supplying ledger metadata to RPC instances to enable full history data access - **Real-Time Data** - access to real-time data on top of the full historical data access ## Costs and Storage Requirements The estimates are based on GCP Compute Engine and Google Cloud Storage costs - **Total Cost** ~= $1,100 USD - Compute Costs ~= $500 USD - GCS Class A Operations (writes) Costs ~= $600 USD - **Total Storage Size** ~= 3 TB ## Export Strategy The best way to export full history with Galexie is by running multiple individual instances of Galexie in parallel. For reference, it is estimated to take approximately 150 days to export full history using a single Galexie instance. Running in parallel with 40-50 Galexie instances takes roughly 4-5 days. 1. Make sure you have set up a storage system and have appropriate hardware available as defined in the Galexie [Prerequisites](./prerequisites.mdx) 2. Determine how many parallel instances of Galexie that you'd want to run 3. Remember to pass non-overlapping ledger ranges to each of your Galexie instances Note that earlier ledgers in history are smaller and export faster than newer, more recent ledgers. This performance difference becomes apparent around ledger 30,000,000. Because of this performance difference, it is generally better to allocate more Galexie instances for more recent ledgers. ### How this Looks in Practice Let's say there are 50,000,000 ledgers that Galexie needs to export. - For 50 instances, this split could look like: - 15 instances to process genesis to 29,999,999 - 35 instances to process 30,000,000 to 50,000,000 Each instance will follow the same [Running Galexie](./running.mdx) instructions ``` galexie append --start --end ``` Where your first instance would run ``` galexie append --start 2 --end 1999999 ``` The second would run ``` galexie append --start 2000000 --end 3999999 ``` and so on ### Methods for Running Multiple Galexie Instances There are different ways to start up multiple Galexie instances that can vary depending on your cloud provider or local hardware. #### GCP Batch Within GCP you can use [Batch](https://cloud.google.com/batch/docs/get-started) that accepts a `job` JSON or YAML file that can parameterize the start and end ledger ranges for each Galexie instance Example GCP Batch job YAML ```yaml job: taskGroups: - taskSpec: computeResource: cpuMilli: 2000 memoryMib: 8000 maxRetryCount: 1 container: imageUri: "stellar/stellar-galexie:23.0.0" entrypoint: "galexie" commands: ["append", "--start", "${START}", "--end", "#{END}"] tasks: # It is possible to use the GCP batch index instead of manually naming each task - name: "galexie-1" environments: START: "2" END: "1999999" - name: "galexie-2" environments: START: "2000000" END: "3999999" ... requireHostsFile: true requireTaskHostsFile: true allocationPolicy: instances: - policy: machineType: "e2-standard-2" disks: - newDisk: type: "pd-standard" sizeGb: 100 mountPoint: "/mnt/shared" ``` #### GCP Compute Instances You can spin up multiple individual compute instances manually Example GCP Compute Instance ```yaml #container-declaration-0.yaml spec: restartPolicy: Always containers: - name: galexie image: stellar/stellar-galexie:23.0.0 command: - galexie args: - append - --start - "2" - --end - "1999999" securityContext: privileged: true ``` Then create the instance by running the following `gcloud` command ```sh gcloud compute instances create "galexie-0" \ --zone=us-central1-a \ --machine-type=e2-standard-2 \ --image-family=cos-stable \ --image-project=cos-cloud \ --boot-disk-size=100GB \ --boot-disk-type=pd-standard \ --boot-disk-device-name="galexie-0" \ --tags=http-server,https-server \ --scopes=https://www.googleapis.com/auth/cloud-platform \ --service-account= \ --metadata-from-file=gce-container-declaration="container-declaration-0.yaml" ``` Repeat process for as many parallel instances of galexie as desired #### Local Galexie Instances You can run multiple Galexie instances locally with a locally built Galexie executable ```sh ./galexie append --start 2 --end 1999999 & \ ./galexie append --start 2000000 --end 3999999 & \ ./galexie append --start 4000000 --end 5999999 & ... ``` --- ## Installing(Admin_guide) ## Kubernetes In order to install Galexie into a Kubernetes cluster follow these steps: - Run `helm repo add stellar https://helm.stellar.org/charts` - Run `helm install stellar/galexie --set datastore.params.path=` Additional configuration can be set with the `--set` flags or by using a [helm values file](https://helm.sh/docs/chart_template_guide/values_files) with the chart. ## Install Container on a host To pull the Galexie container image from the [Stellar Docker Hub registry](https://hub.docker.com/r/stellar/stellar-galexie) using the following docker command or a similar OCI-compliant image pull command: ```shell docker pull stellar/stellar-galexie ``` --- ## Monitoring(Admin_guide) ### Metrics Galexie publishes metrics through an HTTP-based admin endpoint, which makes it easier to monitor its performance. The data is exposed in Prometheus format, enabling easy integration with existing monitoring and alerting systems. The admin port where these metrics are served can be configured by setting the `admin_port` variable. By default, the `admin_port` is set to `6061` ```toml # Admin port configuration # Specifies the port for hosting the HTTP service that publishes metrics. admin_port = 6061 ``` With this configuration, the URL to access the metrics endpoint will be: ``` http://:6061/metrics ``` Galexie emits several application-specific metrics to help track the export process: - `galexie_last_exported_ledger`: The sequence number of the most recently exported ledger. - `galexie_uploader_put_duration_seconds`: The time taken to upload objects to the data lake. - `galexie_uploader_object_size_bytes`: Compressed and uncompressed sizes of the objects being uploaded. - `galexie_upload_queue_length`: Number of objects currently queued and waiting to be uploaded. In addition to these application-specific metrics, Galexie also exports system metrics (e.g., CPU, memory, open file descriptors) and Stellar Core ingestion metrics such as `galexie_ingest_ledger_fetch_duration_seconds` Use these metrics to build queries that monitor Galexie’s performance and export process. Here are a few examples of useful queries: - Export Times: Query `galexie_uploader_put_duration_seconds` to monitor average upload times. - Queue Length: Use `galexie_upload_queue_length` to view the number of objects waiting to be uploaded. - Latest Exported Ledger: Track `galexie_last_exported_ledger` to ensure that ledger exports are up-to-date. For a quick start, download our pre-built Grafana dashboard for Galexie [here](https://grafana.com/grafana/dashboards/22285-stellar-galexie). This dashboard provides pre-configured queries and visualizations to help you monitor Galexie's health. You can customize it to fit your specific needs. ### Logging Galexie emits logs to stdout and generates a log line for every object being exported to help monitor progress. Example logs: ``` INFO[2024-11-07T17:40:37.795-08:00] Uploading: FFFFFF37--200-299/FFFFFF37--200.xdr.zstd pid=98734 service=galexie INFO[2024-11-07T17:40:37.892-08:00] Uploaded FFFFFF37--200-299/FFFFFF37--200.xdr.zstd successfully pid=98734 service=galexie ``` --- ## Prerequisites(Admin_guide) ## 1. Cloud Platform Account Galexie exports Stellar ledger metadata to Google Cloud Storage (GCS) or Amazon Simple Storage Service (S3). You will need the relevant account and credentials for the cloud storage service you choose to use ### Google Cloud Platform (GCP) Account for GCS - Permissions to create a new GCS bucket, or - Access to an existing bucket with read/write permissions. ### Amazon Web Services (AWS) Account for S3 - Permissions to create a new S3 bucket, or - Access to an existing bucket with read/write permissions. ## 2. Container Runtime (Recommended) ### Kubernetes - Kubernetes 1.19+ ### Running the Galexie Container on an Instance - Instance like AWS EC2 or GCP VM - Any host machine with an OCI-compliant container runtime installed like Docker ([Docker installation guide](https://docs.docker.com/engine/install)). :::note While it is possible to natively install Galexie (without a container runtime), this requires manual dependency management and is recommended only for advanced users. ::: ## Hardware Requirements The minimum hardware requirements for running Galexie are:\ **RAM**: 16 GB\ **CPU**: 4 vCPUs\ **Persistent Disk**: 100 GB with at least 5K IOPS ### Full History Export Please see the [Full History Exporting](./full-history-exporting.mdx) guide for more information. --- ## Running(Admin_guide) The commands and arguments described in this document can be configured in the helm chart as well as running them on an instance as described below. The helm chart handles the config file path internally to the chart. The `--config-file` argument should not be added to the arguments variable in the helm chart. With the Docker image available and the configuration file set up, you're now ready to run Galexie and start exporting Stellar ledger data to the storage bucket. ## Command Line Usage ### Append Command This is the primary way of running Galexie. The `append` command operates in two distinct modes: - In continuous/unbounded mode, it starts exporting from the specified start ledger and continuously exports new ledgers that appear on the network until the process is interrupted. - In fixed range mode, it exports the specified range of ledgers and exits when done. Syntax: ```shell stellar-galexie append --start [--end ] [--config-file ] ``` Arguments: `--start ` (required) - The starting ledger sequence number of the range being exported. `--end ` (optional) - The ending ledger sequence number of the range being exported. If unspecified or set to 0, the exporter will continuously export new ledgers as they appear on the network. `--config-file ` (optional) - The path to the configuration file. If unspecified, the application will look for a file named `config.toml` in the current directory. Example usage: ```shell docker run --platform linux/amd64 -d \ -v "$HOME/.config/gcloud/application_default_credentials.json":/.config/gcp/credentials.json:ro \ -e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json \ -v ${PWD}/config.toml:/config.toml \ stellar/stellar-galexie \ append --start 350000 --end 450000 --config-file config.toml ``` `--platform linux/amd64` - Specifies the platform architecture (adjust if needed for your system). `-v` Mounts volumes to map your local GCP credentials and config.toml file to the container: - `$HOME/.config/gcloud/application_default_credentials.json`: Your local GCP credentials file. - `${PWD}/config.toml`: Your local configuration file. `-e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json` - Sets the environment variable for credentials within the container. - Please use AWS equivalent if using S3 as your cloud storage service `stellar/stellar-galexie` - The Docker image name. #### Data Integrity and Resumability: The append command maintains strict sequential integrity within each export session. If interrupted and then restarted with the same range, it automatically resumes from where it left off before interruption, ensuring no ledgers are missed within a session. ### Scan-and-fill Command The `scan-and-fill` command is useful in cases where there are gaps in the exported ledgers in the data lake. The command works by scanning all ledgers in the specified range, identifying missing ledgers and exporting only the missing ledgers while skipping existing ledgers in the data lake. The append command ensures there are no gaps in the exported range. However, the gaps may occur in the data lake due to certain sequence of events, often due to user intervention, such as: - Manual deletion of ledgers from the data lake. For example, deleting ledgers 80-90 out of the range 1-100. - Running non-contiguous export ranges. For example, exporting ranges 1-50 and 60-100, leaving a gap between 50-60. In this case, running `append` command with the range 1-500 causes Galexie to resume export from from 101, without filling the gap. Syntax: ```shell stellar-galexie scan-and-fill --start --end [--config-file ] ``` Arguments: `--start ` (required) - The starting ledger sequence number of the range being exported. `--end ` (required) - The ending ledger sequence number of the range being exported. `--config-file ` (optional): - The path to the configuration file. If unspecified, the exporter will look for a file named “config.toml” in the current directory. Example usage: ```shell docker run --platform linux/amd64 -d \ -v "$HOME/.config/gcloud/application_default_credentials.json":/.config/gcp/credentials.json:ro \ -e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json \ -v ${PWD}/config.toml:/config.toml \ stellar/stellar-galexie \ scan-and-fill --start 64000 --end 68000 --config-file config.toml ``` ### Replace Command The `replace` command is a new addition in Galexie v24.1.0 that simplifies re-exporting ledgers that were previously processed. Unlike `append` or `scan-and-fill`, which skip existing files, `replace` will overwrite existing files within a specified range. It is primarily used when Stellar Core starts emitting new or updated metadata for previously processed ledgers (e.g., the introduction of [CAP-67](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md) Stellar Events). This allows operators to re-export affected ledgers to ensure the data lake contains the latest, most complete metadata. Syntax: ```shell stellar-galexie replace --start --end [--config-file ] ``` Arguments: `--start ` (required) - The starting ledger sequence number of the range being exported. `--end ` (required) - The ending ledger sequence number of the range being exported. `--config-file ` (optional): - The path to the configuration file. If unspecified, the exporter will look for a file named “config.toml” in the current directory. Example usage: ```shell docker run --platform linux/amd64 -d \ -v "$HOME/.config/gcloud/application_default_credentials.json":/.config/gcp/credentials.json:ro \ -e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json \ -v ${PWD}/config.toml:/config.toml \ stellar/stellar-galexie \ replace --start 64000 --end 68000 --config-file config.toml ``` ### Detect-gaps Command The `detect-gaps` command is a new addition in Galexie v25.0.0. It performs a read-only audit of the data lake and reports any missing ledger sequences ("gaps") within a given range. Under normal day-to-day operation, the `append` command maintains strict sequential integrity. However, operators commonly parallelize initial full-history exports by splitting the entire ledger range into multiple subranges and running many Galexie instances concurrently. Misconfigured ranges, failed jobs, or manual intervention can leave holes (missing files) in the datastore. The `detect-gaps` command is intended to verify completeness of a newly created data lake after an initial full-history or as a periodic audit. This command does not export or modify any data. It only scans the existing datastore and reports missing ranges. Syntax: ```shell stellar-galexie detect-gaps --start \ --end \ [--config-file ] \ [--output-file ] ``` Arguments: `--start ` (required) The starting ledger sequence number of the range to be scanned for gaps. `--end ` (required) The ending ledger sequence number of the range to be scanned for gaps. `--config-file ` (optional) The path to the configuration file. If unspecified, the application looks for a file named "config.toml" in the current directory. `--output-file ` (optional) If provided, the gap report is written as JSON to this file. If omitted, the JSON report is written to standard output. Example usage: ```shell docker run --platform linux/amd64 -d \ -v "$HOME/.config/gcloud/application_default_credentials.json":/.config/gcp/credentials.json:ro \ -e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json \ -v ${PWD}/config.toml:/config.toml \ -v ${PWD}:/reports \ stellar/stellar-galexie \ detect-gaps \ --start 2 \ --end 200000 \ --config-file config.toml \ --output-file gaps_report.json ``` Example Output: ```shell { "scan_from": 2, "scan_to": 200000, "duration_seconds": 3.42ms, "report": { "gaps": [ { "start": 144320, "end": 144383 }, { "start": 180000, "end": 180063 } ], "total_ledgers_found": 199871, "total_ledgers_missing": 128, "min_sequence_found": 2, "max_sequence_found": 200000 } } ``` --- ## Setup Galexie runs as a publisher and writes files to buckets and therefore needs to be provisioned with correct account permissions on the bucket to allow writes. For client applications that will be consumers of files in the bucket a smaller set of account permissions will be needed to allow read only activity. ## Google Cloud Platform (GCP) for GCS ### Google Cloud Storage (GCS) bucket If you already have a GCS bucket ready for Galexie to push data, you can skip this section. If not, follow these steps: 1. Visit the GCP Console's Storage section (https://console.cloud.google.com/storage) and create a new bucket. 2. Choose a descriptive name for the bucket, such as `stellar-ledger-data`. Refer to [Google Cloud Storage Bucket Naming Guideline](https://cloud.google.com/storage/docs/buckets#naming) for bucket naming conventions. Note down the bucket name, you will need it later during the configuration process. ### Google Cloud Platform (GCP) Authentication #### Google Kubernetes Engine Cluster When running Galexie inside of a GKE cluster follow the Google cloud documentation for [workload identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) to make sure Galexie has the correct bucket access #### GCP VM 1. [Create a Service Account](https://docs.cloud.google.com/iam/docs/service-accounts-create) 2. Use that Service Account when creating the GCP VM 3. Make sure the Service Account has the correct bucket access #### Credentials (Not Recommended) In order to use static credentials, find the authentication route that works best in the Galexie environment and follow the Google cloud documentation for [creating credentials](https://developers.google.com/workspace/guides/create-credentials) making sure the principal of the credentials has access to the correct bucket #### IAM Role Permissions When using GCP IAM to authenticate Galexie to access a bucket, the following permissions are required: - storage.buckets.get - storage.buckets.list - storage.multipartUploads.abort - storage.multipartUploads.create - storage.multipartUploads.list - storage.multipartUploads.listParts - storage.objects.create - storage.objects.delete - storage.objects.get - storage.objects.list - storage.objects.restore - storage.objects.update ## Amazon Web Services (AWS) for S3 ### Amazon Simple Storage Service (S3) bucket If you already have an S3 bucket ready for Galexie to push data, you can skip this section. If not, follow these steps: 1. Visit the AWS Console's Storage section (https://console.aws.amazon.com/s3) and create a new bucket. 2. Choose a descriptive name for the bucket, such as `stellar-ledger-data`. Refer to [S3 General purpose bucket naming rules](https://cloud.google.com/storage/docs/buckets#naming) for bucket naming conventions. Note down the bucket name, you will need it later during the configuration process. ### Amazon Web Services (AWS) Authentication #### EKS Cluster When running Galexie inside of a EKS cluster follow either the AWS documentation for [IAM roles for service accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) or [pod identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) #### AWS EC2 1. [Creat an IAM Role](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_job-functions_create-policies.html) 2. Use that role in an [instance profile](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html) 3. Use that instance profile in the creation of the EC2 instance 4. Make sure the instance profile has the correct bucket access #### Credentials (Not Recommended) In order to use static credentials, [create an IAM user](https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-started-workloads.html) for Galexie making sure the principal of the credentials has access to the correct bucket and generate security credentials. #### IAM Role Permissions When using AWS IAM to authenticate Galexie to access a bucket, use this example policy making sure to use the correct bucket destination: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowS3BucketOperations", "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetBucketLocation", "s3:ListBucketMultipartUploads" ], "Resource": "arn:aws:s3:::my-galexie-bucket-example" }, { "Sid": "AllowS3ObjectAccess", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts" ], "Resource": ["arn:aws:s3:::my-galexie-bucket-example/*"] } ] } ``` --- ## Example Usages This section showcases real-world use cases of Galexie. --- ## Export to GCS ## Goals - Ledger Metadata is stored on Google Cloud Storage(GCS). - Downstream consumers need access to the latest network data with minimal latency. - Ledger Metadata for each newly closed ledger on Stellar Testnet should be expediently exported to GCS. - Deployment shall be fully on-cloud, use GCP for all storage and compute needs. ## Solution - Publisher Pipeline Run the Galexie Dockerhub image, [stellar/stellar-galexie](https://hub.docker.com/r/stellar/stellar-galexie) as an instance in [GCP Compute Engines](https://cloud.google.com/run/docs/create-jobs) and export the ledger metadata to [GCS bucket](https://cloud.google.com/storage/docs/json_api/v1/buckets) storage. [Galexie](../README.mdx) in this example, performs the main roles of data pipeline. It acts as the `origin` and `publisher` of ledger metadata to the Google Cloud Storage bucket which is the `sink`. ### Prepare the Galexie configuration file locally #### `testnet-config.toml` ``` [datastore_config] type = "GCS" [datastore_config.params] destination_bucket_path = "galexie-data/ledgers/testnet" [datastore_config.schema] ledgers_per_file = 1 files_per_partition = 10 [stellar_core_config] network = "testnet" ``` ### Set default zone and project on gcloud Do this once, so, you don't have to repeat it on all further commands. Example commands assume this is done, ensuring all resources created are in the same GCP project and zone if applicable such as for compute. ``` gcloud config set compute/zone {your zone here} gcloud config set project {your GCP project name} ``` ### Store the galexie configuration file on a Compute Disk Create a new GCP Compute disk which just holds the configuration file for Galexie. It will be used in proceeding step as a volume mount for the Galexie container to access it. ``` // create the raw disk in GCP project $ gcloud compute disks create galexie-config-disk \ --size=10GB \ --type=pd-standard // need to format this raw disk // create a temp instance, attach the new galexie disk to the instance $ gcloud compute instances create temp-instance \ --machine-type=e2-medium \ --disk=name=galexie-config-disk,device-name=galexie-config-disk,mode=rw,auto-delete=no // shell into the temp instance $ gcloud compute ssh temp-instance // find the unformatted, attached disk device // it will be listed with no mountpoint and 10GB temp-instance:~$ lsblk // format the empty disk temp-instance:~$ sudo mkfs.ext4 -F /dev/sda // mount the formatted disk in the instance temp-instance:~$ sudo mkdir -p /mnt/my-disk; chmod a+rw /mnt/my-disk temp-instance:~$ sudo mount /dev/sda /mnt/my-disk temp-instance:~$ exit // copy the local testnet-config.toml file onto the formatted galexie-config-disk $ gcloud compute scp testnet-config.toml temp-instance:/mnt/my-disk // discard the temp instance, no longer needed, the disk will remain. $ gcloud compute instances delete temp-instance ``` ### Create a new gcloud bucket for storage of exported ledger metadata ``` $ gcloud storage buckets create gs://galexie-data ``` ### Use gcloud to deploy and run Galexie as compute instance. Configure the volume mount on the instance for Galexie to load configuration file from existing compute disk created in prior step. Specify the starting ledger sequence for Galexie to begin exporting ledger metadata, the requirements for this deloyment are to start with latest from network, which can be initially obtained from any block explorer, such as reported from [steller.expert/explorer/testnet](https://stellar.expert/explorer/testnet). In this example the `e2-medium` machine type, should suffice for [Galexie Prerequisites](../admin_guide/prerequisites.mdx). ``` gcloud compute instances create-with-container galexie-instance \ --scopes=cloud-platform \ --machine-type=e2-medium \ --container-image=stellar/stellar-galexie \ --disk=name=galexie-config-disk,device-name=galexie-config-disk,mode=ro,auto-delete=no \ --container-mount-disk=mount-path=/mnt/config,mode=ro,name=galexie-config-disk \ --container-arg="append" \ --container-arg="--start" \ --container-arg="1554952" \ --container-arg="--config-file" \ --container-arg="/mnt/config/testnet-config.toml" ``` ### Monitor Galexie export status Proceed to the GCP console: - `Cloud Storage->Buckets`, view the contents of the GCS `galexie-data` bucket, should see new files representing the latest ledger metadata from Testnet arriving in the bucket every minute. - `Compute Engine->Virtual Machines`, check the log output of `galexie-instance`, you'll see `level=info msg="Uploaded ..` lines indicating each time a new file of ledger metadata is uploaded to the GCS bucket. ## Next step - Consumer Pipelines Ledger metadata is now accumulating as files in your GCS bucket, you can start to explore the options for applications to consume this pre-computed network data using the [Ingest SDK](../../ingest-sdk/README.mdx) to assemble consumer driven data pipelines capable of importing and parsing the data to derive custom, enriched data models. Refer to [GCS bucket consumer pipeline](../../../../../build/apps/ingest-sdk/overview.mdx#ledger-metadata-consumer-pipeline) for relevant example code. --- ## Providers(Galexie) Multiple infrastructure providers have made the Galexie service and data lake available, and offer plans ranging from free to paid access. These providers can be used for development, testing, and production. These providers allow access to the Futurenet, Testnet and Mainnet network. | Provider | Futurenet | Testnet | Mainnet | URI | | --- | --- | --- | --- | --- | | [AWS Public Blockchain\*](https://registry.opendata.aws/aws-public-blockchain) | ❌ | ✅ | ✅ | `s3://aws-public-blockchain/v1.1/stellar/ledgers/` | | [Lightsail Network - Quasar](https://quasar.lightsail.network) | ❌ | ❌ | ✅ | `https://galexie.lightsail.network/v1/` | \*AWS Public Blockchain populated by Goldsky ## Run Your Own Galexie If you are interested in running your own Galexie service and data lake, please checkout the [Admin Guide](./admin_guide/README.mdx). --- ## Ingest SDK ## What is the Ingest SDK? The SDK is composed of several published Golang packages under `github.com/stellar/go-stellar-sdk` for acquiring and parsing data from the Stellar network. It provides language level bindings which convert the [binary XDR encoded](../../../../learn/fundamentals/data-format/xdr.mdx) streams emitted from the network into fluent programmatic data model bindings. ## Why use the Ingest SDK? Applications can leverage the SDK to rapidly develop ingestion pipelines capable of acquiring real-time or historical Stellar network data and deriving custom data models. The SDK enables applications to traverse the hierarchal data structures of the network: [history archives](../../../../validators/admin-guide/environment-preparation.mdx#history-archives), [ledgers](../../../../learn/fundamentals/stellar-data-structures/ledgers.mdx), transactions, operations, ledger state changes, and events. Use the SDK for an intuitive, compile-time, type-safe developer experience to work with the main types of network data: ### Ledger Entries Obtain the final state of [ledger entries](../../../../learn/fundamentals/stellar-data-structures/ledgers.mdx) on the network at close of any recent or historically aged checkpoint ledger sequence. A Checkpoint ledger occurs once every 64 ledgers, during which the network will publish this data to [history archives](../../../../validators/admin-guide/environment-preparation.mdx#history-archives) in the format of compressed files which contain lists of `BucketEntry`, wherein each contains one `LedgerEntry` and the `LedgerKey`. Ledger entries are cryptographically signed as part of each ledger and therefore represent the trusted, cumulative state at a point in time for [assets](../../../../learn/fundamentals/stellar-data-structures/assets.mdx) related to an [account](../../../../learn/fundamentals/stellar-data-structures/accounts.mdx) or [contract](../../../../learn/fundamentals/contract-development/storage/persisting-data.mdx). Examples of asset types: - trustlines which hold token balances - offers which hold bid and asks on the [Stellar DEX](../../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#sdex) - contract data which holds key/value stores for contracts ### Ledger Metadata Gain access to all transactions and their nested operations which were included in each closed ledger. It also contains the changes(pre/post) for each ledger entry when it is changed due to transaction activity and all [Soroban contract events](../../../../learn/fundamentals/stellar-data-structures/events.mdx) emitted as a result of [contract](../../../../learn/fundamentals/stellar-data-structures/contracts.mdx) invocations during transaction execution. It is effectively a commit log for ledger entries. Use the metadata to detect incremental changes in ledger entries that occur as a result of each transaction. These changes and the events emitted after each transaction represent valuabe data models specific to a point in time(expressed as a ledger sequence on the network), some real world examples: - a payment in the form of a token transfer between accounts - an offer is placed to trade one token for another token at a given price on the dex - token transfers between accounts and contracts The SDK provides packages which clients can use to acquire and parse the metadata in streaming data format, with readers and callback functions. The streams of ledger metadata can be sourced in unbounded fashion as ongoing real-time ledgers close on the Stellar network, and also in a historical replay mode with a bounded range of past ledger sequences. --- ## Developer Guide(Developer_guide) The Stellar [ingest SDK](https://github.com/stellar/go-stellar-sdk/tree/main/ingest) is a set of packages for retrieving and processing ledger metadata from the Stellar network. Developers can use this SDK to build custom ingestion engines. This guide explains how to set up, configure, and use ingest SDK effectively. --- ## Architecture(Developer_guide) ![](/assets/ingest-sdk/architecture.png) ## Ingest SDK Structure Ingest SDK is organized into three main components: 1. **[Ledger Backends](./ledgerbackends/README.mdx)**: Used to stream ledgers from the Stellar network. `https://github.com/stellar/go-stellar-sdk/tree/main/ingest/ledgerbackend` 2. **[Ledger Readers](./ledgerreaders.mdx)** : Iterators to extract individual changes or transactions from a ledger. `https://github.com/stellar/go-stellar-sdk/tree/main/ingest` 3. **[Ledger Processors](https://github.com/stellar/go-stellar-sdk/tree/main/processors)** : Functions to process individual changes or transactions and extract meaningful data such as accounts, offers, claimable balance, etc. They are used in conjunction with `Ledger Readers` to interpret and transform ledger data. --- ## Ledger Backends A ledger backend is a source of Stellar network ledger data. The ingest SDK supports two primary ledger backends, both implementing the [ledgerbackend](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/ledgerbackend/ledger_backend.go) interface. This interface always returns ledger data as [LedgerCloseMeta](#ledgerclosemeta-structure), which is XDR-encoded ledger metadata object. 1. **[Captive Core](./captivecore.mdx)** – Invokes the `stellar-core` binary as a subprocess which connects to the live Stellar network and fetches network data (i.e., ledgers). 2. **[BufferedStorageBackend](./bufferedstoragebackend.mdx)** – Retrieves ledger metadata from cloud storage. 3. **[RPCLedgerBackend](./rpcledgerbackend.mdx)** – Retrieves ledger metadata from an RPC server. Each backend has its own setup and configuration requirements, which are covered in the following sections. ### LedgerCloseMeta Structure `xdr.LedgerCloseMeta` captures a detailed record of all state changes during the closing of a Stellar ledger. It includes: - `LedgerHeader` – Metadata about the ledger, including: - Ledger sequence number - Previous ledger hash - Close time - Bucketlist hash - `TxSet` – The set of transactions included in the ledger. - `TxProcessing` – Execution results of each transaction, including: - Success or failure of operations within transactions - `OperationMeta`, which tracks `LedgerEntryChanges` caused by transactions - `UpgradesProcessing` – Any protocol upgrades applied in this ledger. - `ScpInfo` – Details of the consensus process that finalized this ledger. - `EvictedLedgerKeys` – Keys of ledger entries removed due to expiration. --- ## BufferedStorageBackend [BufferedStorageBackend](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/ledgerbackend/buffered_storage_backend.go) is a ledger backend in Stellar [ingest SDK](https://github.com/stellar/go-stellar-sdk/tree/main/ingest) that retrieves ledger metadata from a cloud-based data lake, typically populated by [Galexie](../../../galexie/README.mdx). While Galexie currently supports only [GCS](../../../galexie/admin_guide/configuring.mdx), `BufferedStorageBackend` is designed to work with any datastore that implements the [datastore interface](https://github.com/stellar/go-stellar-sdk/blob/main/support/datastore/datastore.go). It returns ledger metadata in [XDR](../../../../../../learn/fundamentals/data-format/xdr.mdx) format. ![](/assets/ingest-sdk/bufferedstoragebackend_architecture.png) ## Key Features - **Parallel Downloads**: Downloads multiple ledgers concurrently and buffers them in memory for fast access. This is particularly useful for fetching large historical ledger ranges. - **Schema-Aware**: Reads multi-ledger files based on the datastore schema, extracting one ledger at a time. - **Automatic Retries**: Handles request failures by retrying failed requests. - **XDR Output**: Returns ledger metadata in XDR format, enabling easy integration with other packages in ingest SDK (e.g., [processors](https://github.com/stellar/go-stellar-sdk/tree/main/processors)). ## Prerequisites ### Installation & Setup - Run Galexie to export ledger data to GCS cloud storage. Follow the [Galexie admin guide](../../../galexie/README.mdx) for instructions on running Galexie. - For purposes of the example code, ensure access to a data lake populated by Galexie, configured as a GCS bucket. For instructions on creating a data lake, refer to the [Galexie admin guide](../../../galexie/README.mdx). ## Configuration ### Datastore Configuration Configure the datastore to match the schema used during the Galexie export. This schema defines how many ledgers per file, and how many files per partition. ```go // Datastore configuration structure type DataStoreConfig struct { Type string `toml:"type"` // Data storage type (e.g., GCS) Params map[string]string `toml:"params"` // Configuration parameters for the datastore Schema DataStoreSchema `toml:"schema"` // Defines the ledger storage schema } ``` **Example Configuration** ```go datastoreConfig := datastore.DataStoreConfig{ Type: "GCS", // Using Google Cloud Storage as the backend Params: map[string]string{ "destination_bucket_path": "your-gcs-bucket/data", // GCS bucket path to the data }, Schema: datastore.DataStoreSchema{ LedgersPerFile: 1, // 1 ledger per file FilesPerPartition: 64000, // Number of files per partition }, } ``` ### BufferedStorageBackend Configuration Configure the `BufferedStorageBackend` to control download concurrency, buffering, and retry behavior. ```go // BufferedStorageBackend configuration structure type BufferedStorageBackendConfig struct { BufferSize uint32 `toml:"buffer_size"` // Number of files to buffer in memory NumWorkers uint32 `toml:"num_workers"` // Number of concurrent workers for downloading ledgers RetryLimit uint32 `toml:"retry_limit"` // Number of retry attempts on failure RetryWait time.Duration `toml:"retry_wait"` // Time to wait between retry attempts } ``` **Example Configuration** ```go // BufferedStorageBackend configuration instance backendConfig := ledgerbackend.BufferedStorageBackendConfig{ BufferSize: 100, // Buffer upto 100 files in memory NumWorkers: 10, // 10 parallel download workers RetryLimit: 3, // Retry up to 3 times on failure RetryWait: 5 * time.Second, // Wait 5 seconds between retries } ``` You can specify these values individually or use the [default configuration](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/cdp/producer.go#L29-L46). The default settings automatically adjust the number of parallel download workers and buffer size based on the object size (ledger count per file). _These values are based on empirical testing, but the optimal configuration may vary depending on hardware and network conditions._ ### Usage BufferedStorageBackend can be used for batch processing a historical range of ledgers as well as for fetching new ledgers in real-time as they become available. Here is a sample code that uses `BufferedStorageBackend` for batch processing of historical ledger range. ```go package main "context" "log" "time" "github.com/stellar/go-stellar-sdk/ingest/ledgerbackend" "github.com/stellar/go-stellar-sdk/support/datastore" "github.com/stellar/go-stellar-sdk/support/errors" ) func main() { ctx := context.Background() // Configure the datastore datastoreConfig := datastore.DataStoreConfig{ Type: "GCS", // Google Cloud Storage as the backend Params: map[string]string{ "destination_bucket_path": "your-gcs-bucket/data", // Replace with actual GCS bucket path }, } // Initialize the datastore dataStore, err := datastore.NewDataStore(ctx, datastoreConfig) if err != nil { log.Fatal(errors.Wrap(err, "failed to create datastore")) } defer dataStore.Close() // Configure the BufferedStorageBackend backendConfig := ledgerbackend.BufferedStorageBackendConfig{ BufferSize: 100, // Number of files to buffer in memory NumWorkers: 10, // Concurrent download workers RetryLimit: 3, // Maximum retry attempts on failure RetryWait: 5 * time.Second, // Wait time between retries } // Per SEP-54 (https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0054.md), // the schema is stored in the datastore; LoadSchema retrieves it. schema, err := datastore.LoadSchema(context.Background(), dataStore, datastoreConfig) if err != nil { log.Fatal(errors.Wrap(err, "failed to retrieve datastore schema")) } // Initialize the backend backend, err := ledgerbackend.NewBufferedStorageBackend(backendConfig, dataStore, schema) if err != nil { log.Fatal(errors.Wrap(err, "failed to create buffered storage backend")) } defer backend.Close() // Define the ledger range to process ledgerRange := ledgerbackend.BoundedRange(1000, 2000) log.Printf("Starting ledger retrieval for range: %d - %d", ledgerRange.From(), ledgerRange.To()) // Iterate through the ledger sequence for ledgerSeq := ledgerRange.From(); ledgerSeq <= ledgerRange.To(); ledgerSeq++ { ledgerCloseMeta, err := backend.GetLedger(ctx, ledgerSeq) if err != nil { log.Printf("Warning: Failed to retrieve ledger %d: %v", ledgerSeq, err) continue } log.Printf("Successfully retrieved ledger %d. Ledger sequence: %d", ledgerSeq, ledgerCloseMeta.LedgerSequence()) // Add your logic to process the XDR data // Example: Parsing transactions, operations, etc. } log.Println("Ledger retrieval process completed successfully.") } ``` For real-time streaming of new ledgers using `BufferedStorageBackend`, refer to the [Ingestion Pipeline Code](../../../../../../build/apps/ingest-sdk/ingestion-pipeline-code.mdx). --- ## Captive Core Captive Core invokes the `stellar-core` binary as a subprocess to stream ledgers from the Stellar network. It can be used to stream a ledger range from the past or to stream new ledgers whenever they are confirmed by the network. ## Prerequisites Using captive Core requires stellar-core binary to be [installed](../../../../../../validators/admin-guide/installation.mdx) first. ### Installation 1. Install Stellar Core: - Option 1: Build from source by following the [Installation Guide](../../../../../../validators/admin-guide/installation.mdx#installing-from-source) - Option 2: Install via a package manager by referring to the [Package-based Installation Guide](../../../../../../validators/admin-guide/installation.mdx#package-based-installation) 2. Verify installation: ```bash ./stellar-core version ``` ## Configuration and Usage Set the captive core configuration for target Stellar network in TOML format. This configuration requires at a minimum: - The **passphrase** of the Stellar network you want to connect to. - The path to **history archives**, necessary for initialization. **Step 1: Generate a `CaptiveCoreToml` configuration** ```go captiveCoreToml, err := ledgerbackend.NewCaptiveCoreTomlFromData( ledgerbackend.PubnetDefaultConfig, ledgerbackend.CaptiveCoreTomlParams{ NetworkPassphrase: network.PublicNetworkPassphrase, HistoryArchiveURLs: network.PublicNetworkhistoryArchiveURLs, }, ) if err != nil { // Handle error } ``` **Step 2: Construct a `CaptiveCoreConfig` object** Next, create a CaptiveCoreConfig object. This object combines the CaptiveCoreToml configuration with the path to your stellar-core binary and other necessary parameters. ```go config := ledgerbackend.CaptiveCoreConfig{ BinaryPath: "/usr/local/bin/stellar-core", // Adjust to your stellar-core binary path NetworkPassphrase: network.PublicNetworkPassphrase, HistoryArchiveURLs: network.PublicNetworkhistoryArchiveURLs, Toml: captiveCoreToml, } ``` **Step 3: Instantiate `CaptiveStellarCore`** Finally, create a `CaptiveStellarCore` instance using the `NewCaptive` function. This function manages the complete lifecycle of the stellar-core process, including communication with your application. ```go captiveStellarCoreBackend, err := ledgerbackend.NewCaptive(config) if err != nil { // Handle error } ``` The `captiveStellarCoreBackend` can now be used to retrieve ledger data within a specified range. For detailed usage, refer to the [code samples](../../examples/README.mdx). --- ## RPC Ledger Backend The [RPCLedgerBackend](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/ledgerbackend/rpc_backend.go) is a [LedgerBackend](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/ledgerbackend/ledger_backend.go) implementation in Stellar [Ingest SDK](https://github.com/stellar/go-stellar-sdk/tree/main/ingest) which uses an RPC Server as the backing source of ledger meta data. Applications can use this ledger backend in order to obtain ledger meta data from the Stellar network through the standard [LedgerBackend](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/ledgerbackend/ledger_backend.go) interface methods. This ledger backend is considered the most lightweight of the available ledger backends as it only requires an HTTP client to access a remote RPC server. Usage of the RPCLedgerBackend implies having awareness of the retention window and data lake settings on the remote RPC server as these aspects determine the range of ledgers which are accessible through the RPCLedgerBackend. - `HISTORY_RETENTION_WINDOW` - This is a setting on the RPC server which determines the range of ledgers retained by the RPC in a sliding window from latest on Stellar network. The default is to retain the latest 7 days worth of ledger data from the Stellar network. - [Data Lake Integration](../../../../../apis/rpc/admin-guide/data-lake-integration.mdx) - If the RPC has the Data Lake integration enabled then the range of ledgers available will be a minimum of RPC's Retention Window and extends further back in history out to the larger range provided by the data lake. ## Example SDK Usage ### Prerequisites 1. A URL of an RPC Server. 2. Go 1.2x runtime ### Code A working example demonstrating programmatic usage of [RPCLedgerBackend](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/ledgerbackend/rpc_backend.go) to print a stream of latest ledgers emitted from the Stellar Testnet Network. Note the additional usage of the [RPC Go Client](https://github.com/stellar/go-stellar-sdk/blob/main/clients/rpcclient/main.go) which uses the same RPC URL to get the latest ledger from the RPC server. This is used to prime our live streaming example to use the latest known ledger on RPC as the starting ledger when requesting the unbounded range. Prerequisites: 1. Create an empty directory `rpc-backend` and`cd` into the directory. 2. Run `go mod init example/rpc-backend` 3. Run `go get github.com/stellar/go-stellar-sdk@latest` 4. Copy this code snippet to `rpc_ledger_backend_demo.go` ```go package main "context" "fmt" "log" "github.com/stellar/go-stellar-sdk/ingest/ledgerbackend" client "github.com/stellar/go-stellar-sdk/clients/rpcclient" ) func main() { ctx := context.Background() // Use the public SDF Testnet RPC for demo purpose endpoint := "https://soroban-testnet.stellar.org" // Create a new RPC client rpcClient := client.NewClient(endpoint, nil) // Get the latest ledger sequence from the RPC server health, err := rpcClient.GetHealth(ctx) if err != nil { log.Fatalf("Failed to get RPC health: %v", err) } startSeq := health.LatestLedger // Configure the RPC Ledger Backend backend := ledgerbackend.NewRPCLedgerBackend(ledgerbackend.RPCLedgerBackendOptions{ RPCServerURL: endpoint, }) defer backend.Close() fmt.Printf("Prepare unbounded range starting with Testnet ledger sequence %d: \n", startSeq) // Prepare an unbounded range starting from the latest ledger if err := backend.PrepareRange(ctx, ledgerbackend.UnboundedRange(startSeq)); err != nil { log.Fatalf("Failed to prepare range: %v", err) } fmt.Println("Iterating over Testnet ledgers:") seq := startSeq for { ledger, err := backend.GetLedger(ctx, seq) if err != nil { fmt.Printf("No more ledgers or error at sequence %d: %v\n", seq, err) break } fmt.Printf("Ledger %d: Hash=%x, CloseTime=%d\n", ledger.LedgerSequence(), ledger.LedgerHash(), ledger.LedgerCloseTime()) seq++ } fmt.Println("Done.") } ``` 5. Run `go mod tidy` 6. Run `go run rpc_ledger_backend_demo.go` --- ## Ledger Readers These readers are designed to extract data from the Stellar ledger in a structured way and enables iterating over ledger changes. - [LedgerTransactionReader](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/ledger_transaction_reader.go): Extracts transactions from a specific ledger sequence. It retrieves the transaction set from `LedgerCloseMeta` and provides an iterator for accessing each individual transaction. - [LedgerChangeReader](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/ledger_change_reader.go): Provides a complete record of all ledger entry changes within a ledger, including those from transactions, fees, upgrades, and other network-level operations. It processes `LedgerCloseMeta` to extract `LedgerEntryChanges`. - [CheckpointChangeReader](https://github.com/stellar/go-stellar-sdk/blob/main/ingest/checkpoint_change_reader.go): Reads ledger entries from history archive buckets at a checkpoint ledger, enabling developers to reconstruct the state (accounts, trust lines etc.) at a specific point in time. --- ## Prerequisites(Developer_guide) To build using ingest SDK, you need: - **Go (>=1.23)** compiler and runtime – to build and run the packages - **Git** – to download the source code _(not needed if installing via package manager)_ - **IDE** (e.g. VS Code, Goland) _(optional)_ --- ## Example Usages(Examples) This section showcases real-world use cases of the Ingest SDK, focusing on key components like `ingest.LedgerTransaction`, `ingest.LedgerChange`, `ingest.LedgerTransactionReader`, and `ingest.ChangeReader`. These examples take a Stellar Ledger (`xdr.LedgerCloseMeta`) or a ledger sequence range (start-end) as input. Each of the examples listed here is complete, and can be compiled and run as-is in your IDE/local dev. ## Prerequisites Some of the examples listed here might invoke the `stellar-core` binary. Refer to the "admin guide" for more details on how to compile `stellar-core` for your platform. The [Stellar Ingest SDK](https://github.com/stellar/go-stellar-sdk) is a go module and includes several packages in addition to the ingest SDK. You will need to include the module in your go project `go.mod`. ```bash go get github.com/stellar/go-stellar-sdk@latest go mod tidy ``` --- ## Retrieve ledger entry changes within a range This code invokes the `stellar-core` binary (as configured in `ledgerbackend.CaptiveCoreConfig`) to replay ledgers from a given ledger range. It uses the `ingest.LedgerChangeReader` to read changes from each ledger (`xdr.LedgerCloseMeta`) and categorizes the changes based on whether they represent created, updated, or deleted ledger entries. The `ingest.Change` structure is used to capture individual changes, and the code tracks various types of changes such as those related to fees, transactions, or operations. ```go // Filename: change_entries.go package main "context" "encoding/json" "fmt" "github.com/stellar/go-stellar-sdk/ingest" "github.com/stellar/go-stellar-sdk/ingest/ledgerbackend" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/xdr" "io" ) func panicIf(err error) { if err != nil { panic(fmt.Errorf("An error occurred, panicking: %s\n", err)) } } func getChangesFromLedger(passphrase string, ledger xdr.LedgerCloseMeta) []ingest.Change { changeReader, err := ingest.NewLedgerChangeReaderFromLedgerCloseMeta(passphrase, ledger) panicIf(err) defer changeReader.Close() changes := make([]ingest.Change, 0) for { change, err := changeReader.Read() if err == io.EOF { break } panicIf(err) changes = append(changes, change) } return changes } func getLedgers(config ledgerbackend.CaptiveCoreConfig, startingLedger uint32, endLedger uint32) []xdr.LedgerCloseMeta { captiveCore, err := ledgerbackend.NewCaptive(config) panicIf(err) ctx := context.Background() err = captiveCore.PrepareRange(ctx, ledgerbackend.BoundedRange(startingLedger, endLedger)) panicIf(err) defer captiveCore.Close() var ledgers []xdr.LedgerCloseMeta for ledgerSeq := startingLedger; ledgerSeq <= endLedger; ledgerSeq++ { ledger, err := captiveCore.GetLedger(ctx, ledgerSeq) panicIf(err) ledgers = append(ledgers, ledger) } return ledgers } type ChangesInfo struct { LedgerEntriesCreated int32 LedgerEntriesUpdated int32 LedgerEntriesDeleted int32 FeeRelatedChanges int32 TxRelatedChanges int32 OperationRelatedChanges int32 } func changeCausedBy(change ingest.Change) xdr.LedgerEntryChangeType { if change.Pre == nil && change.Post != nil { return xdr.LedgerEntryChangeTypeLedgerEntryCreated } else if change.Pre != nil && change.Post == nil { return xdr.LedgerEntryChangeTypeLedgerEntryRemoved } else if change.Pre != nil && change.Post != nil { return xdr.LedgerEntryChangeTypeLedgerEntryUpdated } // this should not happen panic(fmt.Errorf("unable to dertermine LedgerEntryChangeType from change")) } func main() { archiveURLs := network.PublicNetworkhistoryArchiveURLs networkPassphrase := network.PublicNetworkPassphrase captiveCoreToml, err := ledgerbackend.NewCaptiveCoreToml(ledgerbackend.CaptiveCoreTomlParams{ NetworkPassphrase: networkPassphrase, HistoryArchiveURLs: archiveURLs, }) panicIf(err) config := ledgerbackend.CaptiveCoreConfig{ // Change these based on your environment: BinaryPath: "/usr/local/bin/stellar-core", NetworkPassphrase: networkPassphrase, HistoryArchiveURLs: archiveURLs, Toml: captiveCoreToml, } startingLedger := uint32(28921599) // Replace with your desired ledger number endLedger := startingLedger + 5 // NOTE: connecting to pubnet and getting ledgers might take a while. ledgers := getLedgers(config, startingLedger, endLedger) for seq, ledgerMeta := range ledgers { fmt.Printf("Processing ledger: %d\n", startingLedger+uint32(seq)) changes := getChangesFromLedger(config.NetworkPassphrase, ledgerMeta) info := ChangesInfo{} for _, change := range changes { /* Uncomment this code to print the change struct jsonData, err := json.MarshalIndent(change, "", " ") panicIf(err) fmt.Println(string(jsonData)) */ switch changeCausedBy(change) { case xdr.LedgerEntryChangeTypeLedgerEntryCreated: info.LedgerEntriesCreated++ case xdr.LedgerEntryChangeTypeLedgerEntryRemoved: info.LedgerEntriesDeleted++ case xdr.LedgerEntryChangeTypeLedgerEntryUpdated: info.LedgerEntriesUpdated++ } switch change.Reason { case ingest.LedgerEntryChangeReasonFee: info.FeeRelatedChanges++ case ingest.LedgerEntryChangeReasonTransaction: info.TxRelatedChanges++ case ingest.LedgerEntryChangeReasonOperation: info.OperationRelatedChanges++ default: } } jsonData, err := json.MarshalIndent(info, "", " ") panicIf(err) fmt.Println(string(jsonData)) } } ``` **Sample Response:** ```bash >> go run ./change_entries.go // Log lines truncated for clarity Processing ledger: 28921599 { "LedgerEntriesCreated": 59, "LedgerEntriesUpdated": 907, "LedgerEntriesDeleted": 62, "FeeRelatedChanges": 227, "TxRelatedChanges": 227, "OperationRelatedChanges": 574 } Processing ledger: 28921600 { "LedgerEntriesCreated": 58, "LedgerEntriesUpdated": 679, "LedgerEntriesDeleted": 58, "FeeRelatedChanges": 120, "TxRelatedChanges": 120, "OperationRelatedChanges": 555 } Processing ledger: 28921601 { "LedgerEntriesCreated": 61, "LedgerEntriesUpdated": 885, "LedgerEntriesDeleted": 63, "FeeRelatedChanges": 224, "TxRelatedChanges": 224, "OperationRelatedChanges": 561 } Processing ledger: 28921602 { "LedgerEntriesCreated": 57, "LedgerEntriesUpdated": 609, "LedgerEntriesDeleted": 56, "FeeRelatedChanges": 106, "TxRelatedChanges": 106, "OperationRelatedChanges": 510 } Processing ledger: 28921603 { "LedgerEntriesCreated": 62, "LedgerEntriesUpdated": 922, "LedgerEntriesDeleted": 59, "FeeRelatedChanges": 230, "TxRelatedChanges": 230, "OperationRelatedChanges": 583 } Processing ledger: 28921604 { "LedgerEntriesCreated": 61, "LedgerEntriesUpdated": 670, "LedgerEntriesDeleted": 58, "FeeRelatedChanges": 119, "TxRelatedChanges": 119, "OperationRelatedChanges": 551 } ``` --- ## Get count of each ledger entry type from history archive This example connects to the Stellar History Archive and initializes a checkpoint change reader with `ingest.NewCheckpointChangeReader` to read ledger entry changes from a history archive snapshot occurring at a given checkpoint ledger. It iterates over the ledger changes, processing different entry types such as `Account`, `ClaimableBalance`, `Trustline`, and `Offer`, using the `ingest.Change` structure. The code tracks statistics in an `entriesInfo` struct, counting the total entries and specific types. ```go // Filename: ledger_entries.go package main "context" "encoding/json" "fmt" "github.com/stellar/go-stellar-sdk/historyarchive" "github.com/stellar/go-stellar-sdk/ingest" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/support/storage" "github.com/stellar/go-stellar-sdk/xdr" "io" ) func panicIf(err error) { if err != nil { panic(fmt.Errorf("An error occurred, panicking: %s\n", err)) } } // It helps to have a type to distinguish between the type of key and value, when defining a map type AccountIdStr string type entriesInfo struct { TotalEntries int32 NumAccounts int32 NumAccountsWithTrustlines int32 NumClaimableBalances int32 NumOffers int32 } func main() { archiveURLs := network.PublicNetworkhistoryArchiveURLs networkPassphrase := network.PublicNetworkPassphrase // Open a history archive using our existing configuration details. historyArchive, err := historyarchive.NewArchivePool( archiveURLs, historyarchive.ArchiveOptions{ NetworkPassphrase: networkPassphrase, ConnectOptions: storage.ConnectOptions{ S3Region: "us-west-1", UnsignedRequests: false, }, }, ) panicIf(err) // We pass 48921599 because given a checkpoint frequency of 64 ledgers (the // default in `ConnectOptions`, above), 48921599+1 mod 64 == 0. Incompatible // sequence numbers will likely result in 404 errors. // Ledger seq 48921599 corresponds to about 8 years var reader *ingest.CheckpointChangeReader // NOTE: Connecting to pubnet and reading the checkpoint might take a while. reader, err = ingest.NewCheckpointChangeReader(context.Background(), historyArchive, 48921599) panicIf(err) defer reader.Close() trustlinesPerAccount := make(map[AccountIdStr]int32) info := entriesInfo{} for { var entry ingest.Change entry, err = reader.Read() if err == io.EOF { break } panicIf(err) info.TotalEntries++ switch entry.Type { case xdr.LedgerEntryTypeClaimableBalance: info.NumClaimableBalances++ case xdr.LedgerEntryTypeAccount: info.NumAccounts++ case xdr.LedgerEntryTypeTrustline: accountId := entry.Post.Data.MustTrustLine().AccountId trustlinesPerAccount[AccountIdStr(accountId.Address())]++ case xdr.LedgerEntryTypeOffer: info.NumOffers++ default: // there are other types as well, but we are not including them in this example } // Print progress after every 1000 entries are read if info.TotalEntries%1000 == 0 { fmt.Printf("Processed %d ledger entry changes...\r", info.TotalEntries) } } info.NumAccountsWithTrustlines = int32(len(trustlinesPerAccount)) fmt.Println() // Marshal the struct into JSON with indentation jsonData, err := json.MarshalIndent(&info, "", " ") panicIf(err) // Print the indented JSON fmt.Println(string(jsonData)) } ``` **Sample Response:** ```bash >> go run ./ledger_entries.go Processed 40123000 ledger entry changes... { "TotalEntries": 40123409, "NumAccounts": 7699007, "NumAccountsWithTrustlines": 3414587, "NumClaimableBalances": 7559287, "NumOffers": 1053870 } ``` --- ## Get successful/failed transactions from a ledger range This example illustrates how to run and connect with a Stellar network watcher node, referred to as 'captive core' using the `ledgerbackend.CaptiveStellarCore`. It then requests a historical bounded range of ledgers to be replayed. The captive core instance will emit a stream of ledger metadata (`xdr.LedgerCloseMeta`) which contains the transactions per ledger in the range. It reads each ledger's transactions using the `ingest.LedgerTransactionReader`, categorizes them as successful or failed, and tracks the operations associated with each transaction. ```go // Filename: transaction_statistics.go package main "context" "fmt" "github.com/stellar/go-stellar-sdk/network" "io" "github.com/sirupsen/logrus" "github.com/stellar/go-stellar-sdk/ingest" "github.com/stellar/go-stellar-sdk/ingest/ledgerbackend" "github.com/stellar/go-stellar-sdk/support/log" ) func panicIf(err error) { if err != nil { panic(fmt.Errorf("An error occurred, panicking: %s\n", err)) } } func main() { archiveURLs := network.PublicNetworkhistoryArchiveURLs networkPassphrase := network.PublicNetworkPassphrase captiveCoreToml, err := ledgerbackend.NewCaptiveCoreToml(ledgerbackend.CaptiveCoreTomlParams{ NetworkPassphrase: networkPassphrase, HistoryArchiveURLs: archiveURLs, }) panicIf(err) config := ledgerbackend.CaptiveCoreConfig{ // Change these based on your environment: BinaryPath: "/usr/local/bin/stellar-core", NetworkPassphrase: networkPassphrase, HistoryArchiveURLs: archiveURLs, Toml: captiveCoreToml, } ctx := context.Background() // Only log errors from the backend to keep output cleaner. lg := log.New() lg.SetLevel(logrus.ErrorLevel) config.Log = lg backend, err := ledgerbackend.NewCaptive(config) panicIf(err) defer backend.Close() // Prepare a range to be ingested: var startingSeq uint32 = 7000000 // can't start with genesis ledger var ledgersToRead uint32 = 10000 fmt.Printf("Preparing range (%d ledgers)...\n", ledgersToRead) ledgerRange := ledgerbackend.BoundedRange(startingSeq, startingSeq+ledgersToRead) err = backend.PrepareRange(ctx, ledgerRange) panicIf(err) // These are the statistics that we're tracking. var successfulTransactions, failedTransactions int var operationsInSuccessful, operationsInFailed int for seq := startingSeq; seq <= startingSeq+ledgersToRead; seq++ { fmt.Printf("Processed ledger %d...\r", seq) var txReader *ingest.LedgerTransactionReader var err error txReader, err = ingest.NewLedgerTransactionReader( ctx, backend, config.NetworkPassphrase, seq, ) panicIf(err) // Read each transaction within the ledger, extract its operations, and // accumulate the statistics we're interested in. for { var tx ingest.LedgerTransaction tx, err = txReader.Read() if err == io.EOF { break } panicIf(err) envelope := tx.Envelope operationCount := len(envelope.Operations()) if tx.Result.Successful() { successfulTransactions++ operationsInSuccessful += operationCount } else { failedTransactions++ operationsInFailed += operationCount } } panicIf(txReader.Close()) } fmt.Println("\nDone. Results:") fmt.Printf(" - total transactions: %d\n", successfulTransactions+failedTransactions) fmt.Printf(" - succeeded / failed: %d / %d\n", successfulTransactions, failedTransactions) fmt.Printf(" - total operations: %d\n", operationsInSuccessful+operationsInFailed) fmt.Printf(" - succeeded / failed: %d / %d\n", operationsInSuccessful, operationsInFailed) } ``` **Sample Response:** ```bash >> go run ./transaction_statistics.go Preparing range (10000 ledgers)... Processed ledger 7010000... Done. Results: - total transactions: 108 - succeeded / failed: 107 / 1 - total operations: 175 - succeeded / failed: 174 / 1 ``` --- ## Processors Learn more about the processors library in the [Stellar Go SDK](https://github.com/stellar/go) ## [Token Transfer Processor](./token-transfer-processor/README.mdx) Track all asset movement on the Stellar blockchain --- ## Token Transfer Processor ## Overview The Token Transfer Processor (TTP) is a [Go package](https://github.com/stellar/go-stellar-sdk/tree/main/processors/token_transfer) which uses the [ingest-sdk](../../ingest-sdk/README.mdx) to parse Stellar network transaction data and derive token transfer events. Before TTP, developers had to manually parse complex ledger data, operation results, and ledger entry changes to understand when and how value moved between accounts, contracts, and other entities on the network. Prior to [CAP-67 Unified Events](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md), tracking token transfers required significant custom logic to handle different operation types, interpret ledger changes, and reconstruct the flow of assets. CAP-67 introduced a standardized event format that simplifies this process by providing a unified way to represent all token transfer activities. TTP serves as a facade to CAP-67, automatically generating these standardized events from Stellar ledger data. It can operate in two modes: - **Standalone mode**: TTP analyzes operations, operation results, and ledger entry changes to derive transfer events - **Unified events mode**: TTP reads directly from CAP-67 compliant unified events when available in the ledger data For more details on operational modes, see the [Modes of Operation](#modes-of-operation) section. ## Key Features - Captures token movements resulting from: - Simple payments - Path payments - DEX operations - Account merges - Trustline revocations - Claimable balance operations - Liquidity pool operations - Clawback operations - Stellar Asset Contract events - [SEP-41](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md) compliant token events - Generates CAP-67 standardized token events: - Transfer: Movement of tokens between accounts - Mint: Creation of new tokens - Burn: Destruction of tokens - Clawback: Asset issuer reclaiming tokens - Fee: Network fees paid - Handles muxed account information in compliance with CAP-67 multiplexing support - Reconciliation for older protocol versions to ensure consistency between operation changes and generated events ## Events TTP generates events in Go bindings based on [protobuf definitions](https://github.com/stellar/go-stellar-sdk/blob/main/protos/processors/token_transfer/token_transfer_event.proto). The definitions codify an IDL for the standardized token transfer event models put forth in CAP-67. Each event contains metadata and type-specific information structured as follows: | Event Type | Description | Key Fields | When Generated | | --- | --- | --- | --- | | **Transfer** | Asset movement between two entities | `from`, `to`, `amount`, `asset`, `toMuxedInfo` | When assets move between accounts, contracts, or other entities | | **Mint** | Asset creation by the issuer | `to`, `amount`, `asset`, `toMuxedInfo` | When an issuer creates new tokens or when assets are sent from the issuer | | **Burn** | Asset destruction to the issuer | `from`, `amount`, `asset` | When assets are returned to the issuer for destruction | | **Clawback** | Forced asset recovery by issuer | `from`, `amount`, `asset` | When an issuer uses clawback operations to recover assets | | **Fee** | Network fee payment or refund | `account`, `amount` | For all transaction fees and Soroban fee refunds | ### Fee Events TTP generates fee events to track network fees associated with transaction processing. Understanding the different types of fee events is important for accurate accounting: Fee events are generated for all transactions, whether they succeed or fail. These represent the network fees that accounts pay to submit transactions to the Stellar network. - **Present for**: Every transaction - **Amount representation**: Positive values indicating fees paid - **Asset**: Always XLM (Stellar's native asset) Fee refund events are generated only for Soroban (smart contract) transactions and only when there are unused resources that qualify for a refund. - **Present for**: Soroban transactions with unused resource fees - **Amount representation**: Negative values to indicate money being returned - **Asset**: Always XLM - **Event type**: Uses the same `Fee` event type, distinguished by the negative amount :::note TTP uses negative amounts in fee events to represent refunds rather than creating a separate refund event type. This approach maintains consistency with the CAP-67 specification while clearly indicating the direction of the fee transaction. ::: ### Event Metadata Every token transfer event includes comprehensive metadata to provide context about when and where the event occurred: | Field | Type | Description | | --- | --- | --- | | `ledgerSequence` | `uint32` | The ledger number where this event occurred. This provides chronological ordering across the entire network. | | `txHash` | `string` | The transaction hash that generated this event. This allows you to trace events back to their originating transaction. | | `operationIndex` | `uint32*` | The one-based index of the operation within the transaction that caused this event as defined by [SEP-35](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0035.md). This field is `nil` for transaction-level events like fees. | | `contractAddress` | `string` | The contract address associated with the asset/token being moved. For classic operations or Stellar Asset Contract Events, this field will be the contractId of the underlying classic asset. This enables integration with Stellar's smart contract ecosystem. | :::note The `toMuxedInfo` field is included in Transfer and Mint events when the destination uses a muxed account (M-address) and/or when transaction-level memo is set (in the case of non-smart contract transactions), providing additional routing information. ::: Please refer to [this](../../../../../build/guides/transactions/pooled-accounts-muxed-accounts-memos.mdx) section for more information on muxed account/memo usage. Please refer to [this](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md#prohibit-the-transaction-memo-and-muxed-source-accounts-from-being-set-on-soroban-transactions) section in CAP-67 to learn more on what to expect in the `toMuxedInfo` field. :::note The `contractAddress` field is particularly important for DeFi applications as it provides the bridge between classic Stellar assets and their smart contract representations. For events from classic transactions and SAC events from smart contract transactions, the `contractAddress` field reflects the SAC address for the asset. ::: ## Go API Overview TTP provides three distinct functions which derive events from different levels of granularity from the underlying Stellar network data. ### EventsFromLedger ```go func (p *EventsProcessor) EventsFromLedger(lcm xdr.LedgerCloseMeta) ([]*TokenTransferEvent, error) ``` This function processes an entire ledger and returns a flattened list of `TokenTransferEvent` objects. The order of events in the returned slice represents the chronological ordering of debits, credits, and fees as they were applied to accounts, trustlines, and contracts during ledger processing. The chronological ordering is critical for applications that need to maintain accurate balance tracking or audit trails. For detailed information about how events are ordered, see the [Event Ordering](#event-ordering) section. ### EventsFromTransaction ```go func (p *EventsProcessor) EventsFromTransaction(tx ingest.LedgerTransaction) (TransactionEvents, error) ``` This function processes a single transaction and returns a `TransactionEvents` structure that separates fee-related events from operation-related events: - `FeeEvents`: Contains fee charges and refunds associated with the transaction - `OperationEvents`: Contains all events generated by the transaction's operations This separation is useful when you need to handle fees differently from operational transfers, such as for accounting or analytics purposes. ### EventsFromOperation ```go func (p *EventsProcessor) EventsFromOperation( tx ingest.LedgerTransaction, opIndex uint32, op xdr.Operation, opResult xdr.OperationResult ) ([]*TokenTransferEvent, error) ``` This function processes a single operation within a transaction and returns a list of events generated by that specific operation. This granular approach is useful for applications that need to analyze or react to specific types of operations. ## Modes of Operation TTP can operate in two distinct modes depending on how the ledger data was generated and what information is available. ### Default Mode (Recommended) In default mode, TTP analyzes three sources of information to derive token transfer events: - **Operations**: The operations submitted in transactions - **Operation Results**: The success/failure results of each operation - **Ledger Entry Changes**: The changes made to the ledger state This mode works with all Stellar ledgers regardless of how they were generated or which stellar-core version produced them. It is the safest and most compatible option. ```go // Default mode - works with all ledgers processor := token_transfer.NewEventsProcessor(networkPassphrase) ``` ### Unified Events Stream Mode In unified events stream mode, TTP reads token transfer events directly from the unified events stream embedded in the ledger data. This mode is more efficient but requires ledgers that were generated with specific stellar-core configuration flags. ```go // Unified events mode - only for specially configured ledgers processor := token_transfer.NewEventsProcessorForUnifiedEvents(networkPassphrase) ``` Only use unified events stream mode if you are certain that your ledgers contain unified events. These ledgers must be generated by stellar-core with both `EMIT_CLASSIC_EVENTS=true` and `BACKFILL_STELLAR_ASSET_EVENTS=true` configuration flags enabled. TTP cannot dynamically determine whether a ledger contains unified events or not. :::caution If you configure TTP for unified events mode and then provide it ledgers without unified events, TTP will silently produce no events. ::: **When in doubt, always use the default mode**, as it works reliably with all ledger types. ## Event Ordering The order of events returned by TTP depends on the Stellar protocol version that was active when the ledger was created. This ordering is crucial for maintaining accurate chronological records of asset movements. ### Pre-Protocol 23 Ordering Before Whisk, Protocol 23, events follow this chronological pattern: ``` All Fee Events (from all transactions) ↓ For each transaction in ledger: - Operation Events (from all operations in the transaction) - Fee Refund Event (if applicable, immediately after operation events) ``` In this ordering, fee refunds appear immediately after the operation events for each individual transaction. ### Protocol 23+ Ordering Starting with Whisk, Protocol 23, events follow this chronological pattern: ``` All Fee Events (from all transactions) ↓ All Operation Events (from all transactions, maintaining transaction and operation order) ↓ All Fee Refund Events (from all transactions) ``` In this newer ordering, all fee refunds are grouped together at the end, after all transactions have been processed. :::note TTP automatically detects the protocol version and applies the correct ordering rules. You don't need to configure this manually, but understanding the ordering differences is important for applications that depend on event sequence. ::: The chronological ordering ensures that when you process events in the order returned by TTP, you're following the exact sequence in which debits and credits were applied to accounts during ledger processing. This is essential for maintaining accurate balance calculations and audit trails. ## References - [CAP-67: Unified Events](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md) - [SEP-41: Asset Token Contract Specification](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md) - [CAP-38: Automated Market Makers](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0038.md) - [SEP-35: ID Scheme for Stellar Operations](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0035.md) --- ## Example Usages(3) This section contains examples of how Token Transfer Processor can be used in your application logic ## Prerequisites You have an existing go module in current directory or create a new module locally for this example exercise. ```bash go mod init example/ttp_processor ``` Include the Stellar Ingest SDK into your go application module which contains the Token Transfer Processor. ```bash go get github.com/stellar/go-stellar-sdk@latest go mod tidy ``` ## Helper Code This section contains some helper code that will be used in all the examples. ```go package main "context" "fmt" "github.com/stellar/go-stellar-sdk/ingest/ledgerbackend" "github.com/stellar/go-stellar-sdk/processors/token_transfer" "github.com/stellar/go-stellar-sdk/support/log" "github.com/stellar/go-stellar-sdk/xdr" "google.golang.org/protobuf/encoding/protojson" ) func panicIf(err error) { if err != nil { panic(err) } } // fetchLedgerFromRPC retrieves a ledger using RPCLedgerBackend func fetchLedgerFromRPC(ledgerSeq uint32) xdr.LedgerCloseMeta { ctx := context.Background() // Using a publicly hosted RPC instance endpoint := "https://mainnet.sorobanrpc.com" // Configure the RPC Ledger Backend backend := ledgerbackend.NewRPCLedgerBackend(ledgerbackend.RPCLedgerBackendOptions{ RPCServerURL: endpoint, }) defer backend.Close() // Prepare an unbounded range starting from the latest ledger if err := backend.PrepareRange(ctx, ledgerbackend.BoundedRange(ledgerSeq, ledgerSeq)); err != nil { log.Fatalf("Failed to prepare range: %v", err) } ledger, err := backend.GetLedger(ctx, ledgerSeq) panicIf(err) return ledger } func printProtoEvent(event *token_transfer.TokenTransferEvent) { jsonBytes, _ := protojson.MarshalOptions{ Multiline: true, EmitDefaultValues: true, Indent: " ", }.Marshal(event) fmt.Printf("### Event Type : %v\n", event.GetEventType()) fmt.Println(string(jsonBytes)) } ``` --- ## Retrieve token transfer events from a ledger This example fetches a specific Stellar ledger and uses the Token Transfer Processor to extract all token movement events from it. The code processes each event, categorizes them by type (transfers, mints, burns, clawbacks, fees), and prints both the individual event details and summary statistics. This demonstrates the basic usage of EventsFromLedger() to analyze all asset activity within a single ledger. ```go package main "fmt" "strings" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/processors/token_transfer" ) func main() { ledgerSeq := uint32(58155263) ledger := fetchLedgerFromRPC(ledgerSeq) ttp := token_transfer.NewEventsProcessor(network.PublicNetworkPassphrase) // Process events from a single ledger events, err := ttp.EventsFromLedger(ledger) panicIf(err) // Statistics counters var transferCount, mintCount, burnCount, clawbackCount, feeCount, refundCount int // Process events to analyze token transfers for _, event := range events { switch { case event.GetTransfer() != nil: transfer := event.GetTransfer() fmt.Printf("Transfer: %s -> %s, Amount: %s, Asset: %s\n", transfer.From, transfer.To, transfer.Amount, transfer.Asset) transferCount++ case event.GetMint() != nil: mint := event.GetMint() fmt.Printf("Mint: %s, Amount: %s, Asset: %s\n", mint.To, mint.Amount, mint.Asset) mintCount++ case event.GetBurn() != nil: burn := event.GetBurn() fmt.Printf("Burn: %s, Amount: %s, Asset: %s\n", burn.From, burn.Amount, burn.Asset) burnCount++ case event.GetClawback() != nil: clawback := event.GetClawback() fmt.Printf("Clawback: %s, Amount: %s, Asset: %s\n", clawback.From, clawback.Amount, clawback.Asset) clawbackCount++ case event.GetFee() != nil: fee := event.GetFee() if strings.HasPrefix(fee.Amount, "-") { fmt.Printf("Fee Refund: %s, Amount: %s, Asset: %s\n", fee.From, fee.Amount, fee.Asset) refundCount++ } else { fmt.Printf("Fee: %s, Amount: %s, Asset: %s\n", fee.From, fee.Amount, fee.Asset) feeCount++ } } } // Print statistics fmt.Printf("\n--- Ledger %d Statistics ---\n", ledgerSeq) fmt.Printf("Total Events: %d\n", len(events)) fmt.Printf("Transfers: %d\n", transferCount) fmt.Printf("Mints: %d\n", mintCount) fmt.Printf("Burns: %d\n", burnCount) fmt.Printf("Clawbacks: %d\n", clawbackCount) fmt.Printf("Fees: %d\n", feeCount) fmt.Printf("Refunds: %d\n", refundCount) } ``` --- ## Filter events from transaction This code demonstrates a practical implementation of event filtering using the Token Transfer Processor's `EventsFromTransaction` function. The code processes each transaction individually by calling ttp.EventsFromTransaction(tx), which returns a TransactionEvents structure containing: - `FeeEvents`: Transaction fees and refunds - `OperationEvents`: All token transfer events generated by the transaction's operations The events from both categories are combined into a single slice (allEvents) for unified filtering, allowing you to apply the same filter criteria across all event types within each transaction. ## Filtering The filtering system uses Stellar data types from the Stellar Go SDK for accurate matching: - **Asset Matching**: Converts filter criteria into `xdr.Asset` objects and uses `assetPkg.NewProtoAsset()` for accurate protobuf asset comparison via `event.GetAsset().Equals(protoAsset)` - **Event Type Matching**: Uses the actual event type constants from the token_transfer package (e.g., `token_transfer.TransferEvent`) - **Contract Filtering**: Matches exact contract addresses from the event metadata ```go package main "fmt" "io" "log" "strings" assetPkg "github.com/stellar/go-stellar-sdk/asset" "github.com/stellar/go-stellar-sdk/ingest" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/processors/token_transfer" "github.com/stellar/go-stellar-sdk/xdr" ) // FilterOptions defines the filtering criteria type FilterOptions struct { EventType string // "transfer", "mint", "burn", "clawback", "fee", or "" for all AssetCode string // Asset code like "USDC", "XLM", or "" to ignore Issuer string // Issuer address or "" to ignore ContractId string // Contract address or "" to ignore } // filterEvents processes a ledger and returns events matching the filter criteria func filterEvents(ledger xdr.LedgerCloseMeta, filter FilterOptions) { ttp := token_transfer.NewEventsProcessor(network.PublicNetworkPassphrase) // Create transaction reader txReader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta( network.PublicNetworkPassphrase, ledger) if err != nil { log.Fatal("Error creating transaction reader:", err) } // Print filter configuration fmt.Printf("Filtering ledger %d with criteria:\n", ledger.LedgerSequence()) if filter.EventType != "" { fmt.Printf(" Event Type: %s\n", filter.EventType) } if filter.AssetCode != "" { fmt.Printf(" Asset Code: %s\n", filter.AssetCode) } if filter.Issuer != "" { fmt.Printf(" Issuer: %s\n", filter.Issuer) } if filter.ContractId != "" { fmt.Printf(" Contract ID: %s\n", filter.ContractId) } fmt.Println() var matchedEvents, totalEvents int // Process each transaction for { tx, err := txReader.Read() if err == io.EOF { break } if err != nil { log.Fatal("Error reading transaction:", err) } // Save information about tx, if needed in DB // Process events from this transaction txEvents, err := ttp.EventsFromTransaction(tx) if err != nil { log.Printf("Error processing transaction: %v", err) continue } // Combine all events from the transaction allEvents := append(txEvents.FeeEvents, txEvents.OperationEvents...) totalEvents += len(allEvents) // Apply filter to each event for _, event := range allEvents { if matchesFilter(event, filter) { printProtoEvent(event) matchedEvents++ } } } // Print summary fmt.Printf("\n--- Filter Results ---\n") fmt.Printf("Total events: %d\n", totalEvents) fmt.Printf("Matched events: %d\n", matchedEvents) } // matchesFilter checks if an event matches the specified filter criteria func matchesFilter(event *token_transfer.TokenTransferEvent, filter FilterOptions) bool { // Check event type filter if filter.EventType != "" { if !matchesEventType(event, filter.EventType) { return false } } // Check contract ID filter if filter.ContractId != "" { meta := event.GetMeta() if meta.ContractAddress != filter.ContractId { return false } } // Check asset filters if filter.AssetCode != "" || filter.Issuer != "" { var asset xdr.Asset if filter.AssetCode == "native" { asset = xdr.MustNewNativeAsset() } else { asset = xdr.MustNewCreditAsset(filter.AssetCode, filter.Issuer) } if !matchesAsset(event, asset) { return false } } return true } // matchesEventType checks if event matches the specified event type func matchesEventType(event *token_transfer.TokenTransferEvent, eventType string) bool { switch strings.ToLower(eventType) { case token_transfer.TransferEvent: return event.GetTransfer() != nil case token_transfer.MintEvent: return event.GetMint() != nil case token_transfer.BurnEvent: return event.GetBurn() != nil case token_transfer.ClawbackEvent: return event.GetClawback() != nil case token_transfer.FeeEvent: return event.GetFee() != nil default: return false } } // matchesAsset checks if xdr.Asset specified matches the protobuf asset func matchesAsset(event *token_transfer.TokenTransferEvent, asset xdr.Asset) bool { protoAsset := assetPkg.NewProtoAsset(asset) return event.GetAsset().Equals(protoAsset) } func main() { ledgerSeq := uint32(58155263) ledger := fetchLedgerFromRPC(ledgerSeq) // Example 1: Filter by event type only fmt.Println("=== Example 1: Only Transfer Events ===") filterEvents(ledger, FilterOptions{ EventType: "transfer", }) // Example 2: Filter by custom token (asset code + issuer) fmt.Println("\n=== Example 2: Only USDC Events ===") filterEvents(ledger, FilterOptions{ AssetCode: "USDC", Issuer: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", }) // Example 3: Filter by event type + contract ID fmt.Println("\n=== Example 3: Transfer Events from Specific Contract (USDC in this case) ===") filterEvents(ledger, FilterOptions{ EventType: "transfer", // This is the SAC id for the USDC asset on pubnet // https://stellar.expert/explorer/public/contract/CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75 ContractId: "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75", }) // Example 4: Filter by event type + asset code + issuer fmt.Println("\n=== Example 4: Only KALE mints ===") filterEvents(ledger, FilterOptions{ EventType: "mint", AssetCode: "KALE", Issuer: "GBDVX4VELCDSQ54KQJYTNHXAHFLBCA77ZY2USQBM4CSHTTV7DME7KALE", }) // Example 5: Filter by XLM events only fmt.Println("\n=== Example 5: Only XLM Events ===") filterEvents(ledger, FilterOptions{ AssetCode: "native", }) // Example 6: Filter by fee events only fmt.Println("\n=== Example 6: Only Fee Events ===") filterEvents(ledger, FilterOptions{ EventType: "fee", }) } ``` --- ## Oracles Oracles are services that connect blockchain systems to external, off-chain data sources, enabling smart contracts to interact with real-world information. They act as intermediaries, fetching and verifying data such as market prices, weather conditions, or event outcomes, and then delivering it to the blockchain in a secure and reliable manner. This allows decentralized applications (dApps) to execute based on real-world events, expanding their functionality beyond on-chain data. --- ## Oracle Providers Oracles exist on the Stellar network to bring off-chain data onto the blockchain. For example, a popular use-case of oracles is the inclusion of token pricing into smart contract logic. Since a smart contract can't access data from outside the Stellar network, oracles provide a means by which the data can be accessed on-chain. ## [Reflector Network](https://reflector.network) The Reflector oracle protocol is a combination of specialized smart contracts and peer-to-peer consensus of data provider nodes maintained by trusted Stellar ecosystem organizations. Feeds include on-chain and off-chain asset prices, CEX & DEX exchange rates, foreign exchange rates, etc. Reflector nodes process, normalize, aggregate, and store trades information from Stellar Classic DEX, Soroban protocols, as well as external sources. Publicly availble free price oracles are compatible with [SEP40](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0040.md) ecosystem standard interface. Learn how to integrate Reflector feeds [in their documentation](https://reflector.network/docs). [Reflector Subscriptions](https://reflector.network/subscription) provide a service for user-defined customized triggers invoked automatically once the price change for a specified symbol reaches a certain threshold. When the condition is met, cluster nodes simultaneously push a notification to the WebHook URL provided in the subscription and publish an on-chain proof of the triggered event. ### Integrations - [Blend](https://www.blend.capital) [Lending] - [OrbitCDP](https://orbitcdp.finance) [Collateralized Stablecoin] - [DeFindex](https://www.defindex.io) [Yield Aggregator] - [Laina](https://laina-de.fi) [Lending] - [EquitX](https://equitx.com) [Synthetics] - [Slender](https://slender.fi) [Lending] - [SorobanDomains](https://sorobandomains.org) [Infrastructure] ### Deployed Reflector public oracles | Address | Data Source | Network | | --- | --- | --- | | [`CALI2BYU2JE6WVRUFYTS6MSBNEHGJ35P4AVCZYF3B6QOE3QKOB2PLE6M`](https://stellar.expert/explorer/public/contract/CALI2BYU2JE6WVRUFYTS6MSBNEHGJ35P4AVCZYF3B6QOE3QKOB2PLE6M) | Stellar Mainnet DEX | Mainnet | | [`CAFJZQWSED6YAWZU3GWRTOCNPPCGBN32L7QV43XX5LZLFTK6JLN34DLN`](https://stellar.expert/explorer/public/contract/CAFJZQWSED6YAWZU3GWRTOCNPPCGBN32L7QV43XX5LZLFTK6JLN34DLN) | External CEXs & DEXs | Mainnet | | [`CBKGPWGKSKZF52CFHMTRR23TBWTPMRDIYZ4O2P5VS65BMHYH4DXMCJZC`](https://stellar.expert/explorer/public/contract/CBKGPWGKSKZF52CFHMTRR23TBWTPMRDIYZ4O2P5VS65BMHYH4DXMCJZC) | Fiat exchange rates | Mainnet | | | | | [`CAVLP5DH2GJPZMVO7IJY4CVOD5MWEFTJFVPD2YY2FQXOQHRGHK4D6HLP`](https://stellar.expert/explorer/testnet/contract/CAVLP5DH2GJPZMVO7IJY4CVOD5MWEFTJFVPD2YY2FQXOQHRGHK4D6HLP) | Stellar Mainnet DEX | Testnet | | [`CCYOZJCOPG34LLQQ7N24YXBM7LL62R7ONMZ3G6WZAAYPB5OYKOMJRN63`](https://stellar.expert/explorer/testnet/contract/CCYOZJCOPG34LLQQ7N24YXBM7LL62R7ONMZ3G6WZAAYPB5OYKOMJRN63) | External CEXs & DEXs | Testnet | | [`CCSSOHTBL3LEWUCBBEB5NJFC2OKFRC74OWEIJIZLRJBGAAU4VMU5NV4W`](https://stellar.expert/explorer/testnet/contract/CCSSOHTBL3LEWUCBBEB5NJFC2OKFRC74OWEIJIZLRJBGAAU4VMU5NV4W) | Fiat exchange rates | Testnet | ## [Band](https://www.bandprotocol.com) Band is a cross-chain data oracle aggregating and connecting real-world data and APIs to smart contracts. The protocol is built on top of BandChain, a Cosmos-SDK-based blockchain designed to be compatible with most smart contract and blockchain development frameworks. The network is designed to modularize and offload the task of constatly monitoring price data and react to price changes across all asset classes from the smart contract platforms onto itself. This not only prevents such tasks from congesting or causing high transaction fees on the destination network, but the same data points can be packaged, used, and verified efficiently across multiple blockchains. | Address | Network | | --- | --- | | [`CCQXWMZVM3KRTXTUPTN53YHL272QGKF32L7XEDNZ2S6OSUFK3NFBGG5M`](https://stellar.expert/explorer/public/contract/CCQXWMZVM3KRTXTUPTN53YHL272QGKF32L7XEDNZ2S6OSUFK3NFBGG5M) | Mainnet | | [`CBRV5ZEQSSCQ4FFO64OF46I3UASBVEJNE5C2MCFWVIXL4Z7DMD7PJJMF`](https://stellar.expert/explorer/testnet/contract/CBRV5ZEQSSCQ4FFO64OF46I3UASBVEJNE5C2MCFWVIXL4Z7DMD7PJJMF) | Testnet | To learn more about this oracle, please see their [docs](https://docs.bandchain.org) or visit their [GitHub](https://github.com/bandprotocol/band-std-reference-contracts-soroban). ## [DIA Oracles](https://www.diadata.org) [DIA](https://www.diadata.org) is a cross-chain, trustless oracle network that delivers verifiable price feeds. dApps on Stellar can consume these feeds from DIA's deployed contracts. DIA sources raw trade data directly from primary markets and supplies aggregated values on-chain, ensuring transparency and data integrity. ### Key Features - Complete verifiability from source to destination smart contract. - Direct data sourcing from 100+ primary markets eliminating intermediary risk. - Support for 20,000+ assets across all major asset classes. - Custom oracle configuration with tailored sources and methodologies. ### Deployed DIA Oracles #### Oracle Contracts | Address | Data Source | Network | | --- | --- | --- | | [`CAEDPEZDRCEJCF73ASC5JGNKCIJDV2QJQSW6DJ6B74MYALBNKCJ5IFP4`](https://stellar.expert/explorer/testnet/contract/CAEDPEZDRCEJCF73ASC5JGNKCIJDV2QJQSW6DJ6B74MYALBNKCJ5IFP4) | External CEXs & DEXs | Testnet | #### Supported Assets The DIA oracle on Stellar includes price feeds for the following assets: | Asset | Blockchain | Address | Markets Overview | | --- | --- | --- | --- | | USDC | Ethereum | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | [USDC Markets](https://www.diadata.org/app/price/asset/Ethereum/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | | BTC | Bitcoin | 0x0000000000000000000000000000000000000000 | [BTC Markets](https://www.diadata.org/app/price/asset/Bitcoin/0x0000000000000000000000000000000000000000) | | DIA | Ethereum | 0x84cA8bc7997272c7CfB4D0Cd3D55cd942B3c9419 | [DIA Markets](https://www.diadata.org/app/price/asset/Ethereum/0x84cA8bc7997272c7CfB4D0Cd3D55cd942B3c9419) | ### Request a Custom Oracle For assets not currently available or dApps requiring specific configurations, DIA deploys production-grade custom oracles tailored to your requirements with configurable data sources, pricing methodologies, update triggers, and coverage for any of 20,000+ supported assets. → [Request a Custom Oracle](https://www.diadata.org/docs/guides/how-to-guides/request-a-custom-oracle) ### Resources - Developer Support: [Discord](https://discord.com/invite/ZvGjVY5uvs) | [Telegram](https://t.me/diadata_org) - [Stellar Integration Guide](https://www.diadata.org/docs/guides/chain-specific-guide/stellar) - [DIA Documentation](https://www.diadata.org/docs) --- ## Fundamentals of the Network; Data, Fees, Consensus, SEPs & More # Core Concepts Build your foundational understanding of what makes the Stellar network tick. --- ## Learn About Anchors: On/Off Ramps for Bridging Traditional Finance & Blockchain # Anchors ## Overview An anchor is a Stellar-specific term for the on and off-ramps that connect the Stellar network to traditional financial rails, such as financial institutions or fintech companies. Anchors accept deposits of fiat currencies (such as the US dollar, Argentine peso, or Nigerian naira) via existing rails (such as bank deposits or cash-in points), then sends the user the equivalent digital tokens on the Stellar network. The equivalent digital tokens can either represent that same fiat currency or another digital token altogether. Alternatively, anchors allow token holders to redeem their tokens for the real-world assets they represent. Stellar has anchor services operating worldwide. View the [Anchor Directory](https://anchors.stellar.org) for more information on existing Stellar anchors. Anchors can issue their own assets on the Stellar network, or they can honor assets that already exist. You can set up an anchor by using the SDF-maintained [Anchor Platform](../../platforms/anchor-platform/README.mdx), which is the easiest way to deploy an anchor service compatible with Stellar Ecosystem Proposals (SEPs). Learn how to integrate anchor services into your blockchain-based application by viewing the [Build Apps section](../../build/apps/overview.mdx). If you’re looking for MoneyGram Ramps, see the Integrate with [MoneyGram Ramps tutorial](https://developer.moneygram.com/moneygram-developer/docs/integrate-moneygram-ramps). ## Stellar Ecosystem Proposals (SEPs) Stellar is an open-source network that is designed to interoperate with traditional financial institutions, various types of assets, and other networks. Network participants implement Stellar Ecosystem Proposals (SEPs) to ensure they can interoperate with other products and services on the network. SEPs are publicly created, open-source documents that live in a [GitHub repository](https://github.com/stellar/stellar-protocol/tree/master/ecosystem#stellar-ecosystem-proposals-seps.mdx) and they define how anchors, asset issuers, applications, exchanges, and other service providers should interact and interoperate. Read more about SEPs in the [SEPs section](../fundamentals/stellar-ecosystem-proposals.mdx). For anchors, the most important SEPs are [SEP-6]: Programmatic Deposit and Withdrawal, [SEP-24]: Hosted Deposit and Withdrawal, and [SEP-31]: Cross Border Payments API. You’ll also work with [SEP-10]: Stellar Authentication, [SEP-12]: KYC API, and [SEP-38]: Anchor RFQ API. [sep-1]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md [sep-6]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md [sep-9]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md [sep-10]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md [sep-12]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md [sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md [sep-31]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md [sep-38]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md ### Using SEP-6: Programmatic Deposit and Withdrawal versus SEP-24: Hosted Deposit and Withdrawal A user typically must decide whether they want to set up an anchor using [SEP-6: Programmatic Deposit and Withdrawal](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md) or [SEP-24: Hosted Deposit and Withdrawal](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md). Here are the differences and what to consider when choosing one or the other. #### SEP-6: Programmatic Deposit and Withdrawal Defines the standard way for anchors and wallets to interact on behalf of users. With this SEP’s guidance, wallets and other clients can interact with anchors directly without the user needing to leave the wallet to go to the anchor’s site. With SEP-6, the client collects KYC information from the user. Wallets (clients) must take into consideration when using SEP-6: - Clients must collect KYC information they may not need - Clients must know what information to collect per-anchor - Clients must send the information in a standardized format [(SEP-12: KYC API)](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) - Anchors must receive the information via SEP-12’s API #### SEP-24: Hosted Deposit and Withdrawal Defines the standard way for anchors and wallets to interact on behalf of users interactively. This means that the user’s application must open a webview hosted by a third-party anchor for the user to provide the information necessary to complete the transaction. With SEP-24, the anchor collects KYC information from the user. Wallets (clients) must take into consideration when using SEP-24: - Clients don’t have to collect the anchor’s required KYC information - Clients & anchors don’t have to implement [SEP-12: KYC API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) - Anchors must create a UI to be rendered by many clients - Clients must allow anchors to temporarily control UX --- ## Smart Contracts --- ## Authorization Authorization is the process of judging which operations "should" or "should not" be allowed to occur; it is about judging _permission_. Authorization differs from _authentication_, which is the narrower problem of judging whether a person "is who they say they are", or whether a message claiming to come from a person "really" came from them. Authorization often uses cryptographic authentication (via signatures) to support its judgments, but is a broader, more general process. View the [authorization starter guide](../../../build/guides/auth/contract-authorization.mdx) to learn more about smart contract authorization on Stellar. ## Soroban Authorization Framework Soroban aims to provide a light-weight, but flexible and extensible framework that allows contracts to implement arbitrarily complex authorization rules, while providing built-in implementation for some common tasks (such as replay prevention). The framework consists of the following components: - Contract-specific authorization - custom authorization rules implemented by contracts using the private contract storage and abstract accounts. - Account abstraction - allows users to customize their authentication rules and define universal authorization policies via contract account. This includes the built-in support for Stellar accounts. - Host-based authorization library - ensures integrity between contract accounts and regular contracts. Also defines the structured signature payload format, ensures replay prevention and takes care of providing the correct signature contexts. Soroban host also provides some cryptographic functions (signature verification, hashing) which may be useful for contract account implementations. Contracts that use Soroban authorization framework are interoperable with each other. Also it is easier for the client applications to write generic code for interaction with Soroban authorization framework. For example, wallets can implement a generalized way to present and sign Soroban payloads. We realize that it's not possible to cover each and every case, but we hope that the vast majority of the contracts can operate within the framework and thus contribute to building a more cohesive ecosystem. Custom authorization frameworks are still possible to implement, but are not encouraged (unless there are no alternatives). ### Contract-Specific Authorization #### Contract storage Contracts have an exclusive read and write access to their [storage](../../../build/smart-contracts/getting-started/storing-data.mdx) in the ledger. This allows contracts to safely control and manage user access to their data. For example, a token contract may ensure that only the administrator can mint more of the token by storing the administrator identity in its storage. Similarly, it can make sure that only an owner of the balance may transfer that balance. #### `Address` The storage-based approach described in the previous section requires a way to represent the user identities and authenticate them. `Address` type is a host-managed type that performs these functions. From the contract perspective `Address` is an opaque identifier type. The contract logic doesn't need to depend on the internal representation of the `Address` (see [Account Abstraction](#account-abstraction) section below for more details). `Address` type has two similar methods in Soroban SDK: `require_auth` and `require_auth_for_args` (these methods call the respective Soroban host function). The only difference between the functions is the ability to customize the invocation arguments. See [auth example] that demonstrates how to use these functions. Both functions ensure that the `Address` has authorized the call of the current function within the current context (where context is defined by `require_auth` calls in the current call stack; see more formal definition in the [section below](#require_auth-implementation-details)). The authentication rules for this authorization are defined by the `Address` and are enforced by the Soroban host. Replay protection is also implemented in the host, i.e., there is normally no need for a contract to manage its own nonces. [auth example]: ../../../build/smart-contracts/example-contracts/auth.mdx #### Authorizing Sub-contract Calls One of the key features of Soroban Authorization Framework is the ability to easily make authorized sub-contract calls. For example, it is possible for a contract to call `require_auth` for an `Address` and then call `token.xfer` authorized for the same `Address` (see [timelock example] that demonstrates this pattern). Contracts don't need to do anything special to benefit from this feature. Just calling a sub-contract that calls `require_auth` will ensure that the sub-contract call has been properly authorized. [timelock example]: ../../../build/smart-contracts/example-contracts/timelock.mdx #### When to `require_auth` The main authorization-related decision a contract writer needs to make for any given `Address` is whether they need to call `require_auth` for it. While the decision needs to be made on case-by-case basis, here are some rules of thumb: - If the access to the `Address` data in this contract is read-only, then `require_auth` is probably not needed. - If the `Address` data in this contract is being modified in a way that's not strictly beneficial to the user, then `require_auth` is probably needed (e.g. reducing the user's token balance needs to be authorized, while increasing it doesn't need to be authorized) - If a contract calls another contract that will call `require_auth` for the `Address` (e.g. `token.xfer`), then adding `require_auth` in the caller would ensure that the authorization for the inner call can't be reused outside of your contract. For example, if you want to do something positive for the user, but only when they have transferred some token to your contract, then the contract call itself should `require_auth`. #### Authorizing Multiple `Address`es There is no explicit restriction on how many `Address` entities the contract uses and how many `Address`es have `require_auth` called. That means that it is possible to authorize a contract call on behalf of multiple users, which may even have different authorization contexts (customized via arguments in `require_auth_for_args`). [Atomic swap] is an example that deals with authorization of two `Address`es. [atomic swap]: ../../../build/smart-contracts/example-contracts/atomic-swap.mdx Note though, that contracts that deal with multiple authorized `Address`es need a bit more complex support on the client side (to collect and attach the proper signatures). ### Account Abstraction Account abstraction is a way to decouple the authentication logic from the contract-specific authorization rules. The `Address` defined above is in fact an identifier of an 'abstract' account. That is, the contracts know the `Address` and can require authorization from it, but they don't know how exactly it is implemented. For example, imagine a token contract. Its responsibilities are to manage the balances of multiple users (transfer, mint, burn etc.). There is really nothing about these responsibilities that has anything to do with _how exactly_ the user authorized the balance-modifying transaction. The users may want to use some hardware key that supports a new generation of crypto algorithms(which don't even have to exist today) or they may want to have bespoke multisig scheme and none of this really has anything to do with the token logic. Account abstraction provides a convenient extension point for every contract that uses `Address` for authorization. It doesn't solve all the issues automatically - client-side tooling may still need to be adapted to support different authentication schemes or different wallets. But the on-chain state doesn't need to be modified and modifying the on-chain state is a much harder problem. #### Types of Account Implementations Conceptually, every abstract account is a special contract that defines authentication rules and potentially some additional account-specific authorization policies. However, for the sake of optimization and integration with the existing Stellar accounts, Soroban supports 4 different kinds of the account implementations. Below are the general descriptions of these implementations. See the transaction [guide](../contract-development/contract-interactions/stellar-transaction.mdx) for the concrete information of how different accounts are represented. ##### Stellar Account Corresponds to `Address::Account`. This is a special, built-in 'account contract' that handles all the Stellar accounts. It is not a real contract and doesn't need to be deployed. This supports the Stellar multisig with medium threshold. See Stellar [documentation] for more details on multisig and thresholds. [documentation]: ../transactions/signatures-multisig.mdx ##### Transaction Invoker Corresponds to `Address::Account`. This is also a Stellar account, but its signature is inferred from the source account of the Stellar transaction (or operation, if it has one). This is purely an optimization of the [Stellar Account](#stellar-account) that can skip one signature in case the transaction source account also authorizes the contract invocation. ##### Contract Invoker Corresponds to `Address::Contract`. This is a special case of an 'account' that may appear only when a contract calls another contract. We consider that since the contract makes a call, then it must be authorizing it (otherwise, it shouldn't have made that call). Hence all the `require_auth` calls made on behalf of the **direct** invoker contract `Address` are considered to be authorized (but not any calls on behalf of the contract deeper down the stack). ##### Contract Account Corresponds to `Address::Contract`. This is the extension point of account abstraction. A contract that implements the `CustomAccountInterface` and `__check_auth` becomes a contract account. If any contract calls `require_auth` for the `Address` of this contract, the Soroban host will call `__check_auth` with the corresponding arguments. `__check_auth` gets a signature payload, a list of signatures (in any user-defined format) and a list of the contract invocations that are being authorized by these signatures. Its responsibility is to perform the authentication via verifying the signatures and also (optionally) to apply a custom authorization policy. For example, a signature weight system similar to Stellar can be implemented, but it also can have customizable rules for the weights, e.g. to allow spending more than X units of token Y only given signature weight Z. Contract accounts can also be treated as a custodial wallet. It holds the user's funds (token balances, NFTs etc.) and provides the user(s) with ways to authorize operations on these funds. Nothing prevents contract accounts from authorizing operations unrelated to balances; for example, they can perform administrative functions for tokens (contract accounts define what to do when `require_auth` is called). For the exact interface and more details, see the [Simple Account example]. [Simple Account example]: ../../../build/smart-contracts/example-contracts/simple-account.mdx ### Secp256r1, passkeys and contract accounts After a successful public validator vote to upgrade Stellar's Mainnet to Protocol 21, the secp256r1 signature scheme was enabled for smart contract transactions. This allows developers to implement passkeys to sign transactions instead of using secret keys or seed phrases. For guidance, see the [passkey wallet guide](../../../build/guides/contract-accounts/smart-wallets.mdx). ### Advanced Concepts Most of the contracts shouldn't need the concepts described in this section. Refer to this when developing complex contracts that deal with deep contract call trees and/or multiple `Address`es. #### `require_auth` implementation details When a Soroban transaction is executed on-chain, the host collects a list of `SorobanAuthorizationEntry` entries from the transaction ([XDR][soroban-auth-entry]). These entries contain signed authorizer credentials and authorized invocation trees. The host uses these entries to verify authorization during the contract execution. Every time `require_auth`/`require_auth_for_args` host function is called for non-contract-invoker account, the following steps happen: - Find an authorized invocation tree that matches the `require_auth` call. The matching process is pretty involved and is described in the section below. - If authentication hasn't happened for this tree yet, then perform it: - Verify signature expiration. Expired signatures are not valid. - Verify and consume nonce. Nonce is an arbitrary number, that has to be unique among all the non-expired signatures of the address. - Build the expected [signature payload preimage] and compute its SHA-256 hash to get the final signature payload - Call `__check_auth` of the account contract corresponding to the `Address` using the signature payload and the invocations from the authorization tree - Mark the invocation as 'exhausted' in its authorized invocation tree. 'Exhausted' invocations will be skipped when matching the future `require_auth` calls. If any of the steps above fails, then the authorization is considered unsuccessful. Notice, that authentication happens just once per tree, as the whole tree needs to be signed. [soroban-auth-entry]: https://github.com/stellar/stellar-xdr/blob/e372df9f677961aac04c5a4cc80a3667f310b29f/Stellar-transaction.x#L570 [signature payload preimage]: https://github.com/stellar/stellar-xdr/blob/e372df9f677961aac04c5a4cc80a3667f310b29f/Stellar-transaction.x#L703 #### Matching Authorized Invocation Trees In order for authorizations to succeed, all the `require_auth`/`require_auth_for_args` calls have to be covered by the corresponding `SorobanAuthorizedInvocation` trees in a transaction (defined in transaction [XDR][invocation-xdr]). Formally, this correspondence is defined as follows. Given a top-level contract invocation `I` we can build a 'contract invocation tree' `T` by tracing all the sub-contract calls (a directed edge `A->B` in the tree means 'contract function A calls contract function B). Note, that we only consider the functions that are implemented in different contracts, i.e. any function calls that don't involve a contract invocation via host `call` are considered to belong to the same node. Let's say authorization is required from addresses `A_1..A_N`. Then for every address `A_i` there are two kinds of nodes in the invocation tree `T`: `R`-nodes that had a `require_auth` call for `A_i` and `N`-nodes that didn't have such call. Then we remove all the `N`-nodes and all the edges from `T` and add the directed edges connecting the remaining `R`-nodes such that the edge goes from `R_j` to `R_k` if there was a path between `R_j` and `R_k` in `T` that doesn't contain any other `R`-nodes. As a result we get a forest of `SorobanAuthorizedInvocation` trees for `A_i`. Notice, that these trees don't have to have their root be `I` node (i.e. the top-level contract call), so it's possible to e.g. batch the authorized call together without requiring signing the batching function. In simpler terms, `SorobanAuthorizedInvocation` trees for an `Address` are subsets of the full invocation tree that are 'condensed' to only contain invocations that have `require_auth` call for that `Address`. During the matching process that happens for every `require_auth` host tries to match the current path in `T` to a `SorobanAuthorizedInvocation` tree for the corresponding `Address`. The path is considered to be matched only when there is a corresponding path of _exhausted_ `R` nodes leading to the current call. This means that if the `Address` signs a sequence of calls `A.foo->B.bar->C.baz`, then its authorization check will fail in case if `A.foo` directly calls `C.baz` because `C.baz` strictly has to be called from `B.bar`. ##### Duplicate Addresses In case if the same contract function calls `require_auth` for the same `Address` multiple times (e.g. when multiple operations from the same user are being batched), every `require_auth` call still has to have a corresponding node in the `SorobanAuthorizedInvocation` tree. Due to that, there might be multiple valid trees that make all the authorization checks pass. There is nothing wrong about that - the address still must have authorized all the invocations. The only requirement for such cases to be handled correctly is to ensure that the `require_auth` calls for an `Address` happen before the corresponding sub-contract calls. [invocation-xdr]: https://github.com/stellar/stellar-xdr/blob/e372df9f677961aac04c5a4cc80a3667f310b29f/Stellar-transaction.x#L537 --- ## Contract Interactions Learn about the methods available for developers and users to interact with deployed contracts. --- ## Cross-Contract A cross-contract invocation is a powerful (yet expensive) kind of contract interaction. A contract invocation is similar to starting a new process because the code that runs will be in a separate address space, meaning that they do not share any data other than what was passed in the invocation. While a contract invocation typically transfers control to a _different_ contract, it is possible to transfer control to the currently running contract. Regardless of whether the contract that receives control is a different contract or the currently running contract, the value returned by `get_invoking_contract` will be the previous value of `get_current_contract`. A contract invocation can only access the public methods of a contract. One public function is an exception: a contract's `__constructor` is invoked by the host at deployment, rather than by another contract. For example, the [token example contract] sets its admin and metadata in a constructor: ```rust #[contractimpl] impl Token { pub fn __constructor(e: Env, admin: Address, decimal: u32, name: String, symbol: String) { ``` So instead of being invoked like other functions, its arguments are passed to `deploy_v2`, which deploys the contract and runs its constructor in the same invocation: ```rust e.deployer() .with_current_contract(salt) .deploy_v2(token_wasm_hash, (admin, decimal, name, symbol)) ``` In tests, the same arguments are passed to `register`: ```rust e.register(Token {}, (admin, decimal, name, symbol)) ``` For calling a contract's other functions, see [Making cross-contract calls]. [token example contract]: ../../../../build/smart-contracts/example-contracts/tokens.mdx [making cross-contract calls]: ../../../../build/guides/conventions/cross-contract.mdx --- ## Overview(Contract-interactions) Interact with smart contracts. Contracts are invoked through a pair of host functions `call` and `try_call`: - `try_call(contract, function, args)` calls `function` exported from `contract`, passing `args` and returning a `Error` on any error. - `call(contract, function, args)` just calls `try_call` with its arguments and traps on `Error`, essentially propagating the error. In both cases `contract` is a `Binary` host object containing the contract ID, `function` is a `Symbol` holding the name of an exported function to call, and `args` is a `Vector` of values to pass as arguments. These host functions can be invoked in two separate ways: - From outside the host, such as when a user submits a transaction that calls a contract. - From within the host, when one contract calls another. Both cases follow the same logic: - The contract's Wasm bytecode is retrieved from a `CONTRACT_DATA` ledger entry in the host's storage system. - A Wasm VM is instantiated for the duration of the invocation. - The function is looked up and invoked, with arguments passed from caller to callee. When a call occurs from outside the host, any arguments will typically be provided in serialized XDR form accompanying the transaction, and will be deserialized and converted to host objects automatically before invoking the contract. When a call occurs from inside the host, the caller and callee contracts _share the same host_ and the caller can pass references to host objects directly to the callee without any need to serialize or deserialize them. Since host objects are immutable, there is limited risk to passing a shared reference from one contract to another: the callee cannot modify the object in a way that would surprise the caller, only create new objects. --- ## Stellar Transaction {`Invoke and deploy smart contracts with the InvokeHostFunctionOp operation.`} ## Example SDK Usage Some (but not all yet) of the Stellar SDKs have functions built-in to handle most of the process of building a Stellar transaction to interact with a Soroban smart contract. Below, we demonstrate in JavaScript and Python how to build and submit a Stellar transaction that will invoke an instance of the [increment example](../../../../build/smart-contracts/getting-started/storing-data.mdx) smart contract. :::tip The existing [JavaScript SDK](https://github.com/stellar/js-stellar-sdk) now incorporates all of the elements needed for Soroban. All you need to do is install it using your preferred package manager. ```bash npm install --save @stellar/stellar-sdk ``` ::: ```js (async () => { const { Keypair, Contract, rpc as StellarRpc, TransactionBuilder, Networks, BASE_FEE, } = require("@stellar/stellar-sdk"); // The source account will be used to sign and send the transaction. // GCWY3M4VRW4NXJRI7IVAU3CC7XOPN6PRBG6I5M7TAOQNKZXLT3KAH362 const sourceKeypair = Keypair.fromSecret( "SCQN3XGRO65BHNSWLSHYIR4B65AHLDUQ7YLHGIWQ4677AZFRS77TCZRB", ); // Configure the SDK to use the `stellar-rpc` instance of your choosing. const server = new StellarRpc.Server( "https://soroban-testnet.stellar.org:443", ); // Here we will use a deployed instance of the `increment` example contract. const contractAddress = "CBEOJUP5FU6KKOEZ7RMTSKZ7YLBS5D6LVATIGCESOGXSZEQ2UWQFKZW6"; const contract = new Contract(contractAddress); // Transactions require a valid sequence number (which varies from one // account to another). We fetch this sequence number from the RPC server. const sourceAccount = await server.getAccount(sourceKeypair.publicKey()); // The transaction begins as pretty standard. The source account, minimum // fee, and network passphrase are provided. let builtTransaction = new TransactionBuilder(sourceAccount, { fee: BASE_FEE, networkPassphrase: Networks.TESTNET, }) // The invocation of the `increment` function of our contract is added // to the transaction. Note: `increment` doesn't require any parameters, // but many contract functions do. You would need to provide those here. .addOperation(contract.call("increment")) // This transaction will be valid for the next 30 seconds .setTimeout(30) .build(); // We use the RPC server to "prepare" the transaction. This simulating the // transaction, discovering the storage footprint, and updating the // transaction to include that footprint. If you know the footprint ahead of // time, you could manually use `addFootprint` and skip this step. let preparedTransaction = await server.prepareTransaction(builtTransaction); // Sign the transaction with the source account's keypair. preparedTransaction.sign(sourceKeypair); // Let's see the base64-encoded XDR of the transaction we just built. console.log( `Signed prepared transaction XDR: ${preparedTransaction .toEnvelope() .toXDR("base64")}`, ); // Submit the transaction to the Stellar-RPC server. The RPC server will // then submit the transaction into the network for us. Then we will have to // wait, polling `getTransaction` until the transaction completes. try { let sendResponse = await server.sendTransaction(preparedTransaction); console.log(`Sent transaction: ${JSON.stringify(sendResponse)}`); if (sendResponse.status === "PENDING") { let getResponse = await server.getTransaction(sendResponse.hash); // Poll `getTransaction` until the status is not "NOT_FOUND" while (getResponse.status === "NOT_FOUND") { console.log("Waiting for transaction confirmation..."); // See if the transaction is complete getResponse = await server.getTransaction(sendResponse.hash); // Wait one second await new Promise((resolve) => setTimeout(resolve, 1000)); } console.log(`getTransaction response: ${JSON.stringify(getResponse)}`); if (getResponse.status === "SUCCESS") { // Make sure the transaction's resultMetaXDR is not empty if (!getResponse.resultMetaXdr) { throw "Empty resultMetaXDR in getTransaction response"; } // Find the return value from the contract and return it let transactionMeta = getResponse.resultMetaXdr; let returnValue = getResponse.returnValue; console.log(`Transaction result: ${returnValue.value()}`); } else { throw `Transaction failed: ${getResponse.resultXdr}`; } } else { throw sendResponse.errorResultXdr; } } catch (err) { // Catch and report any errors we've thrown console.log("Sending transaction failed"); console.log(JSON.stringify(err)); } })(); ``` :::tip The [`py-stellar-base`](https://stellar-sdk.readthedocs.io/en/soroban) Python SDK has stable support for interacting with Soroban smart contracts as of [v9.0.0](https://github.com/StellarCN/py-stellar-base/releases/tag/9.0.0). ```bash pip install stellar-sdk ``` ::: ```py from typing import Optional, Union from stellar_sdk import scval, MuxedAccount, Keypair, Network from stellar_sdk.contract import AssembledTransaction, ContractClient # `IncrementContractClient` is automatically generated by stellar-contract-bindings, and you do not need to write it manually. # See https://github.com/lightsail-network/stellar-contract-bindings # stellar-contract-bindings python --contract-id CDMARRPKAZEZMASLYONRI4LJI6X3QLDJQ647YGQANG2PDCP746BD5U73 --rpc-url https://soroban-testnet.stellar.org class IncrementContractClient(ContractClient): def increment( self, source: Union[str, MuxedAccount], signer: Optional[Keypair] = None, base_fee: int = 100, transaction_timeout: int = 300, submit_timeout: int = 30, simulate: bool = True, restore: bool = True, ) -> AssembledTransaction[int]: """Increment increments an internal counter, and returns the value.""" return self.invoke( "increment", [], parse_result_xdr_fn=lambda v: scval.from_uint32(v), source=source, signer=signer, base_fee=base_fee, transaction_timeout=transaction_timeout, submit_timeout=submit_timeout, simulate=simulate, restore=restore, ) # The source account will be used to sign and send the transaction. # GCWY3M4VRW4NXJRI7IVAU3CC7XOPN6PRBG6I5M7TAOQNKZXLT3KAH362 source_keypair = Keypair.from_secret('SCQN3XGRO65BHNSWLSHYIR4B65AHLDUQ7YLHGIWQ4677AZFRS77TCZRB') rpc_server_url = "https://soroban-testnet.stellar.org:443" network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE contract_address = 'CDMARRPKAZEZMASLYONRI4LJI6X3QLDJQ647YGQANG2PDCP746BD5U73' client = IncrementContractClient(contract_address, rpc_server_url, network_passphrase) result = client.increment(source_keypair.public_key).sign_and_submit(source_keypair) print(result) ``` :::tip [java-stellar-sdk](https://github.com/lightsail-network/java-stellar-sdk) provides support for Soroban. Please visit the project homepage for more information. ::: ```java public class SorobanExample { public static void main(String[] args) throws SorobanRpcException, IOException, InterruptedException { // The source account will be used to sign and send the transaction. KeyPair sourceKeypair = KeyPair.fromSecretSeed("SCQN3XGRO65BHNSWLSHYIR4B65AHLDUQ7YLHGIWQ4677AZFRS77TCZRB"); // Configure SorobanClient to use the `stellar-rpc` instance of your choosing. SorobanServer sorobanServer = new SorobanServer("https://soroban-testnet.stellar.org"); // Here we will use a deployed instance of the `increment` example contract. String contractAddress = "CBEOJUP5FU6KKOEZ7RMTSKZ7YLBS5D6LVATIGCESOGXSZEQ2UWQFKZW6"; // Transactions require a valid sequence number (which varies from one account to // another). We fetch this sequence number from the RPC server. TransactionBuilderAccount sourceAccount = null; try { sourceAccount = sorobanServer.getAccount(sourceKeypair.getAccountId()); } catch (AccountNotFoundException e) { throw new RuntimeException("Account not found, please activate it first"); } // The invocation of the `increment` function of our contract is added to the // transaction. Note: `increment` doesn't require any parameters, but many // contract functions do. You would need to provide those here. InvokeHostFunctionOperation operation = InvokeHostFunctionOperation.invokeContractFunctionOperationBuilder( contractAddress, "increment", null) .build(); // Create a transaction with the source account and the operation we want to invoke. Transaction transaction = new TransactionBuilder(sourceAccount, Network.TESTNET) .addOperation(operation) .setTimeout(30) // This transaction will be valid for the next 30 seconds .setBaseFee(100) // The base fee is 100 stroops (0.00001 XLM) .build(); // We use the RPC server to "prepare" the transaction. This simulating the // transaction, discovering the storage footprint, and updating the transaction // to include that footprint. If you know the footprint ahead of time, you could // manually use `addFootprint` and skip this step. try { transaction = sorobanServer.prepareTransaction(transaction); } catch (PrepareTransactionException e) { // You should handle the error here throw new RuntimeException(e); } // Sign the transaction with the source account's keypair. transaction.sign(sourceKeypair); // Let's see the base64-encoded XDR of the transaction we just built. System.out.println("Signed prepared transaction XDR: " + transaction.toEnvelopeXdrBase64()); // Submit the transaction to the Stellar-RPC server. The RPC server will then // submit the transaction into the network for us. Then we will have to wait, // polling `getTransaction` until the transaction completes. SendTransactionResponse response = sorobanServer.sendTransaction(transaction); if (!SendTransactionResponse.SendTransactionStatus.PENDING.equals(response.getStatus())) { throw new RuntimeException("Sending transaction failed"); } // Poll `getTransaction` until the status is not "NOT_FOUND" GetTransactionResponse getTransactionResponse; while (true) { System.out.println("Waiting for transaction confirmation..."); // See if the transaction is complete getTransactionResponse = sorobanServer.getTransaction(response.getHash()); if (!GetTransactionResponse.GetTransactionStatus.NOT_FOUND.equals( getTransactionResponse.getStatus())) { break; } // Wait one second Thread.sleep(1000); } System.out.println("get_transaction response: " + getTransactionResponse); if (GetTransactionResponse.GetTransactionStatus.SUCCESS.equals( getTransactionResponse.getStatus())) { // Find the return value from the contract and return it TransactionMeta transactionMeta = TransactionMeta.fromXdrBase64(getTransactionResponse.getResultMetaXdr()); long returnValue = Scv.fromUint32(transactionMeta.getV3().getSorobanMeta().getReturnValue()); System.out.println("Transaction result: " + returnValue); } else { System.out.println("Transaction failed: " + getTransactionResponse.getResultXdr()); } } } ``` ## XDR Usage Stellar supports invoking and deploying contracts with a new operation named `InvokeHostFunctionOp`. The [`stellar-cli`] abstracts these details away from the user, but not all SDKs do yet. If you're building a dapp you'll probably find yourself building the XDR transaction to submit to the network. The `InvokeHostFunctionOp` can be used to perform the following Soroban operations: - Invoke contract functions. - Upload Wasm of the new contracts. - Deploy new contracts using the uploaded Wasm or built-in implementations (this currently includes only the [token contract](../../../../tokens/stellar-asset-contract.mdx)). [`stellar-cli`]: ../../../../build/smart-contracts/getting-started/setup.mdx#install-the-stellar-cli There is only a single `InvokeHostFunctionOp` allowed per transaction. Contracts should be used to perform multiple actions atomically, for example, to deploy a new contract and initialize it atomically. Additionally, Soroban transactions containing `InvokeHostFunctionOp` have the following restrictions: - The transaction **cannot include a memo** — the memo must be set to `MEMO_NONE`. - The transaction **source account cannot be a muxed account**. - The **operation source account, if set, cannot be a muxed account**. See [Send to and receive payments from Contract Accounts](../../../../build/guides/transactions/send-and-receive-c-accounts.mdx) for information on how to provide a muxed ID when making payments in Soroban. ### InvokeHostFunctionOp The XDR of `HostFunction` and `InvokeHostFunctionOp` below can be found [here][xdr]. [xdr]: https://github.com/stellar/stellar-xdr/blob/e372df9f677961aac04c5a4cc80a3667f310b29f/Stellar-transaction.x#L581 ```cpp union HostFunction switch (HostFunctionType type) { case HOST_FUNCTION_TYPE_INVOKE_CONTRACT: InvokeContractArgs invokeContract; case HOST_FUNCTION_TYPE_CREATE_CONTRACT: CreateContractArgs createContract; case HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM: opaque wasm<>; }; struct InvokeHostFunctionOp { // Host function to invoke. HostFunction hostFunction; // Per-address authorizations for this host function. SorobanAuthorizationEntry auth<>; }; ``` #### Function The `hostFunction` in `InvokeHostFunctionOp` will be executed by the Soroban host environment. The supported functions are: 1. `HOST_FUNCTION_TYPE_INVOKE_CONTRACT` - This will invoke a function of the deployed contract with arguments specified in `invokeContract` struct. ```cpp struct InvokeContractArgs { SCAddress contractAddress; SCSymbol functionName; SCVal args<>; }; ``` `contractAddress` is the address of the contract to invoke, `functionName` is the name of the function to invoke and `args` are the arguments to pass to that function. 2. `HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM` - This will upload the contract Wasm using the provided `wasm` blob. - Uploaded Wasm can be identified by the SHA-256 hash of the uploaded Wasm. 3. `HOST_FUNCTION_TYPE_CREATE_CONTRACT` - This will deploy a contract instance to the network using the specified `executable`. The 32-byte contract identifier is based on `contractIDPreimage` value and the network identifier (so every network has a separate contract identifier namespace). ```cpp struct CreateContractArgs { ContractIDPreimage contractIDPreimage; ContractExecutable executable; }; ``` - `executable` can be either a SHA-256 hash of the previously uploaded Wasm or it can specify that a built-in contract has to be used: ```cpp enum ContractExecutableType { CONTRACT_EXECUTABLE_WASM = 0, CONTRACT_EXECUTABLE_STELLAR_ASSET = 1 }; union ContractExecutable switch (ContractExecutableType type) { case CONTRACT_EXECUTABLE_WASM: Hash wasm_hash; case CONTRACT_EXECUTABLE_STELLAR_ASSET: void; }; ``` - `contractIDPreimage` is defined as following: ```cpp union ContractIDPreimage switch (ContractIDPreimageType type) { case CONTRACT_ID_PREIMAGE_FROM_ADDRESS: struct { SCAddress address; uint256 salt; } fromAddress; case CONTRACT_ID_PREIMAGE_FROM_ASSET: Asset fromAsset; }; ``` - The final contract identifier is created by computing SHA-256 of this together with the network identifier as a part of [`HashIDPreimage`]: [`hashidpreimage`]: https://github.com/stellar/stellar-xdr/blob/v27.0/Stellar-transaction.x#L721-L762 ```cpp union HashIDPreimage switch (EnvelopeType type) { ... case ENVELOPE_TYPE_CONTRACT_ID: struct { Hash networkID; ContractIDPreimage contractIDPreimage; } contractID; ... ``` - `CONTRACT_ID_PREIMAGE_FROM_ADDRESS` specifies that the contract will be created using the provided address and salt. This operation has to be authorized by `address` (see the following section for details). - `CONTRACT_ID_PREIMAGE_FROM_ASSET` specifies that the contract will be created using the Stellar asset. This is only supported when `executable == CONTRACT_EXECUTABLE_STELLAR_ASSET`. Note, that the asset doesn't need to exist when this is applied, however the issuer of the asset will be the initial token administrator. Anyone can deploy asset contracts. ##### JavaScript Usage Each of these variations of host function invocation has convenience methods in the [JavaScript SDK](../../../../tools/sdks/client-sdks.mdx#javascript-sdk): - [`Operation.invokeHostFunction`](https://stellar.github.io/js-stellar-sdk/Operation.html#.invokeHostFunction) is the lowest-level method that corresponds directly to the XDR. - [`Operation.invokeContractFunction`](https://stellar.github.io/js-stellar-sdk/Operation.html#.invokeContractFunction) is an abstraction to invoke the method of a particular contract, similar to [`Contract.call`](https://stellar.github.io/js-stellar-sdk/Contract.html#call). - [`Operation.createStellarAssetContract`](https://stellar.github.io/js-stellar-sdk/Operation.html#.createStellarAssetContract) and [`Operation.createCustomContract`](https://stellar.github.io/js-stellar-sdk/Operation.html#.createCustomContract) are abstractions to instantiate contracts: the former is for wrapping an existing [Stellar asset](../../../../tokens/how-to-issue-an-asset.mdx) into a smart contract and the latter is for deploying your own contract. - [`Operation.uploadContractWasm`](https://stellar.github.io/js-stellar-sdk/Operation.html#.uploadContractWasm) corresponds to the above `HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM` variant, letting you upload the raw WASM buffer to the ledger. #### Authorization Data Soroban's [authorization framework](../authorization.mdx) provides a standardized way for passing authorization data to the contract invocations via `SorobanAuthorizationEntry` structures. ```cpp struct SorobanAuthorizationEntry { SorobanCredentials credentials; SorobanAuthorizedInvocation rootInvocation; }; union SorobanCredentials switch (SorobanCredentialsType type) { case SOROBAN_CREDENTIALS_SOURCE_ACCOUNT: void; case SOROBAN_CREDENTIALS_ADDRESS: SorobanAddressCredentials address; }; ``` `SorobanAuthorizationEntry` contains a tree of invocations with `rootInvocation` as a root. This tree is authorized by a user specified in `credentials`. `SorobanAddressCredentials` have two options: - `SOROBAN_CREDENTIALS_SOURCE_ACCOUNT` - this simply uses the signature of the transaction (or operation, if any) source account and hence doesn't require any additional payload. - `SOROBAN_CREDENTIALS_ADDRESS` - contains `SorobanAddressCredentials` with the following structure: ```cpp struct SorobanAddressCredentials { SCAddress address; int64 nonce; uint32 signatureExpirationLedger; SCVal signature; }; ``` The fields of this structure have the following semantics: - When `address` is the address that authorizes invocation. - `signatureExpirationLedger` the ledger sequence number on which the signature expires. Signature is still considered valid on `signatureExpirationLedger`, but it is no longer valid on `signatureExpirationLedger + 1`. It is recommended to keep this as small as viable, as it makes the transaction cheaper. - `nonce` is an arbitrary value that is unique for all the signatures performed by `address` until `signatureExpirationLedger`. A good approach to generating this is to just use a random value. - `signature` is a structure containing the signature (or multiple signatures) that signed the 32-byte, SHA-256 hash of the `ENVELOPE_TYPE_SOROBAN_AUTHORIZATION` preimage ([XDR][envelope-xdr]). The signature structure is defined by the account contract corresponding to the `Address` (see below for the Stellar account signature structure). `SorobanAuthorizedInvocation` defines a node in the authorized invocation tree: ```cpp struct SorobanAuthorizedInvocation { SorobanAuthorizedFunction function; SorobanAuthorizedInvocation subInvocations<>; }; union SorobanAuthorizedFunction switch (SorobanAuthorizedFunctionType type) { case SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN: SorobanAuthorizedContractFunction contractFn; case SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN: CreateContractArgs createContractHostFn; }; struct SorobanAuthorizedContractFunction { SCAddress contractAddress; SCSymbol functionName; SCVec args; }; ``` `SorobanAuthorizedInvocation` consists of the `function` that is being authorized (either contract function or a host function) and the authorized sub-invocations that `function` performs (if any). `SorobanAuthorizedFunction` has two variants: - `SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN` is a contract function that includes the address of the contract, name of the function being invoked, and arguments of the `require_auth`/`require_auth_for_args` call performed on behalf of the address. Note, that if `require_auth[_for_args]` wasn't called, there shouldn't be a `SorobanAuthorizedInvocation` entry in the transaction. - `SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN` is authorization for `HOST_FUNCTION_TYPE_CREATE_CONTRACT` or for `create_contract` host function called from a contract. It only contains the `CreateContractArgs` XDR structure corresponding to the created contract. Building `SorobanAuthorizedInvocation` trees may be simplified by using the recording auth mode in Soroban's `simulateTransaction` mechanism (see the [docs][simulate-transaction-doc] for more details). [envelope-xdr]: https://github.com/stellar/stellar-xdr/blob/v27.0/Stellar-transaction.x#L747-L754 [simulate-transaction-doc]: transaction-simulation.mdx#authorization ##### Stellar Account Signatures `signatureArgs` format is user-defined for the [custom accounts], but it is protocol-defined for the Stellar accounts. The signatures for the Stellar account are a vector of the following Soroban [structures] in the Soroban SDK format: ```rust #[contracttype] pub struct AccountEd25519Signature { pub public_key: BytesN<32>, pub signature: BytesN<64>, } ``` [structures]: https://github.com/stellar/rs-soroban-env/blob/99d8c92cdc7e5cd0f5311df8f88d04658ecde7d2/soroban-env-host/src/native_contract/account_contract.rs#L51 [custom accounts]: ../authorization.mdx#account-abstraction ##### JavaScript Usage There are a couple of helpful methods in the SDK to make dealing with authorization easier: - Once you've gotten the authorization entries from [`simulateTransaction`](https://soroban.stellar.org/api/methods/simulateTransaction), you can use the [`authorizeEntry`](https://stellar.github.io/js-stellar-sdk/global.html#authorizeEntry) helper to "fill out" the empty entry accordingly. You will, of course, need the appropriate signer for each of the entries if you are in a multi-party situation. ```typescript const signedEntries = simTx.auth.map(async (entry) => // In this case, you can authorize by signing the transaction with the // corresponding source account. entry.switch() === xdr.SorobanCredentialsType.sorobanCredentialsSourceAccount() ? entry : await authorizeEntry( entry, // The `signer` here will be unique for each entry, perhaps reaching out // to a separate entity. signer, currentLedger + 1000, Networks.TESTNET, ), ); ``` - If you, instead, want to _build_ an authorization entry from scratch rather than relying on simulation, you can use [`authorizeInvocation`](https://stellar.github.io/js-stellar-sdk/global.html#authorizeInvocation), which will build the structure with the appropriate fields. ### Transaction resources Every Soroban transaction has to have a `SorobanTransactionData` transaction [extension] populated. This is needed to compute the [Soroban resource fee](../../../fundamentals/fees-resource-limits-metering.mdx). [extension]: https://github.com/stellar/stellar-xdr/blob/v27.0/Stellar-transaction.x#L957-L964 The Soroban transaction data is defined as follows: ```cpp struct SorobanResources { // The ledger footprint of the transaction. LedgerFootprint footprint; // The maximum number of instructions this transaction can use uint32 instructions; // The maximum number of bytes this transaction can read from disk backed entries uint32 diskReadBytes; // The maximum number of bytes this transaction can write to ledger uint32 writeBytes; }; struct SorobanResourcesExtV0 { // Vector of indices representing what Soroban // entries in the footprint are archived, based on the // order of keys provided in the readWrite footprint. uint32 archivedSorobanEntries<>; }; struct SorobanTransactionData { union switch (int v) { case 0: void; case 1: SorobanResourcesExtV0 resourceExt; } ext; SorobanResources resources; // Amount of the transaction `fee` allocated to the Soroban resource fees. int64 resourceFee; }; ``` This data comprises the Soroban `resources` and the `resourceFee`. The `resourceFee` is the portion of the transaction fee allocated to Soroban resource fees. It has a non-refundable part (fees for instructions, ledger I/O, and transaction size) and a refundable part that is charged based on actual consumption of refundable resources: the contract events emitted by the transaction, the return value of the host function invocation, and the [ledger space rent](../storage/state-archival.mdx). The `SorobanResources` structure includes the ledger footprint and the resource values, which together determine the resource consumption limit and the resource fee. The footprint must contain the `LedgerKey`s that will be read and/or written. The simplest method to determine the values in `SorobanResources` and `resourceFee` is to use the [`simulateTransaction` mechanism](transaction-simulation.mdx). #### JavaScript Usage You can use the [`SorobanDataBuilder`](https://stellar.github.io/js-stellar-sdk/SorobanDataBuilder.html) to leverage the [builder pattern](https://en.wikipedia.org/wiki/Builder_pattern) and get/set all of the above resources accordingly. Then, you call `.build()` and pass the resulting structure to the [`setSorobanData`](https://stellar.github.io/js-stellar-sdk/TransactionBuilder.html#setSorobanData) method of the corresponding [`TransactionBuilder`](https://stellar.github.io/js-stellar-sdk/TransactionBuilder.html). --- ## Tests [Debugging contracts](../errors-and-debugging/debugging.mdx) explains that it is much more convenient to debug using native code than Wasm. Given that you are testing native code, it is tempting to interact with your contract directly using function calls. If you attempt this approach, you will find that it doesn't always work. Function call interactions do not set the environment into the correct state for contract execution, so functions involving contract data and determining the current or invoking contract will not work. When writing tests, it is important to always interact with contracts through contract invocation. In a production setting, contract invocation will execute Wasm bytecode loaded from the ledger. So how does this work if you are testing native code? You must register your contract with the environment, so it knows what functions are available and how to call them. While this sounds complex, the `contractimpl` procedural macro automatically generates almost all the code to do this. All you have to do is write a small [stub](https://github.com/stellar/soroban-token-contract/blob/42380647bb817bf01c739c19286f18be881e0e41/src/testutils.rs#L12-L15) to actually call the generated code, such as ```rust title="test.rs" pub fn register_test_contract(e: &Env, contract_id: &[u8; 32]) { let contract_id = FixedBinary::from_array(e, *contract_id); e.register(crate::contract::Token {}, &contract_id); } ``` Some contracts, such as the token contract, also provide a [friendlier interface](https://github.com/stellar/soroban-token-contract/blob/42380647bb817bf01c739c19286f18be881e0e41/src/testutils.rs#L26-L191) to facilitate testing. There are many ways these interfaces might make testing easier, but one common one is to allow automatic message signing by passing a [ed25519_dalek::Keypair](https://docs.rs/ed25519-dalek/latest/ed25519_dalek). Note that everything described in this section is only available if the `testutils` feature is enabled. ### Example This machinery can also be used to test multiple contracts together. For example, the single offer contract test case [creates a token](https://github.com/stellar/soroban-examples/blob/56fef787395b5aed7cd7b19772cca28e21b3feb5/single_offer/src/test.rs#L22). --- ## Transaction Simulation ## Footprint As mentioned in the [persisting data](../storage/persisting-data.mdx) section, a contract can only load or store `CONTRACT_DATA` entries that are declared in a _footprint_ associated with its invocation. A footprint is a set of ledger keys, each marked as either read-only or read-write. Read-only keys are available to the transaction for reading; read-write keys are available for reading, writing, or both. Any Soroban transaction submitted by a user has to be accompanied by this footprint. A single footprint encompasses _all_ the data read and written by _all_ contracts transitively invoked by the transaction: not just the initial contract that the transaction calls, but also all contracts it calls, and so on. Since it can be difficult for a user to know which ledger entries a given contract call will attempt to read or write (especially entries that are caused by other contracts deep within a transaction), the host provides an auxiliary `simulateTransaction` mechanism that executes a transaction against a temporary, possibly out-of-date _snapshot_ of the ledger. The `simulateTransaction` mechanism is _not_ constrained to only read or write the contents of a footprint; rather it _records_ a footprint describing the transaction's execution, discards the execution's effects, and then returns the recorded footprint to its caller. This simulation-provided footprint can then be used to accompany a "real" submission of the same transaction to the network for real execution. If the state of the ledger has changed too much between the time of the simulated and the real submission, the footprint may be too stale and no longer accurately identify the _keys_ the transaction needs to read and/or write, at which point the simulation must be retried to refresh the footprint. In any event (whether successful or failing), the real transaction will execute atomically, deterministically, and with serializable consistency semantics. An inaccurate footprint simply causes deterministic transaction failure, not a stale-read anomaly. All effects of such a failed transaction are discarded, as they would be in the presence of any other error. ## Authorization Please refer to the [authorization overview](../authorization.mdx) and transaction authorization [section][auth-data] for general information on Soroban authorization: this section pertains specifically to how simulation works alongside authorization requirements. Soroban's transaction [simulation mechanism][sim-tx] can be used to precompute the [`SorobanAuthorizedInvocation`][auth-invoke] trees that must be authorized by the `Address`es for all the `require_auth` checks to pass. It can be invoked in two different ways: ### Recording Mode The Soroban host environment provides a simulation mode that records the entire context (address, contract ID, function, arguments, etc.) involved in calls to `require_auth`. These records are added to a [`SorobanAuthorizedInvocation`][auth-invoke] tree and marked as successful. Then, after the invocation has finished, transaction simulation returns all of the recorded trees, as well as randomly-generated nonce values for the expected signatures. Given this information from simulation, the client only needs to provide these trees and nonces to the `Address`es involved the invocation for signing, then build the final transaction by combining simulation output with the corresponding signatures. Note that the "recording" auth mode _never_ emulates authorization failures. This is because failing authorization is always an "exceptional" situation (i.e., the `Address`es for which you don't anticipate successful authorization shouldn't be used in the first place). It is similar to how, for example, the [`simulateTransaction`][sim-tx] mechanism doesn't emulate failures caused by the incorrect footprint. If you'd like to validate signatures, you should use [`simulateTransaction`][sim-tx] in authorization ["enforcement" mode](#enforcing-mode), which will verify the signatures before executing the transaction on-chain. ### Enforcing Mode The recording auth mode is one option for [`simulateTransaction`][sim-tx]. However, when dealing with the custom account contracts, for example, it may be necessary to simulate the custom account's `__check_auth` code (which is simply _omitted_ in the recording auth mode), to get its ledger footprint. This is called running simulation with "enforcing" auth mode. This is basically equivalent to running the transaction on-chain (with possibly a slightly stale ledger state); hence, it requires all the signatures to be valid. From a developer's perspective, the difference between these is whether or not authorization entries are present in the [`InvokeHostFunction` operation](../../../../learn/fundamentals/transactions/list-of-operations.mdx#invoke-host-function) submitted to [`simulateTransaction`][sim-tx]. The [examples below](#typescript-utilities) highlight this distinction in detail, but the short story is that passing `auth` to [`Operation.invokeContractFunction`](https://stellar.github.io/js-stellar-sdk/Operation.html#.invokeContractFunction) (which is a convenience wrapper on [`invokeHostFunction`](https://stellar.github.io/js-stellar-sdk/Operation.html#.invokeHostFunction)) will imply enforcement mode. ### SDK Usage Below, we'll demonstrate the various ways in which you can invoke transaction simulation as well as highlight some utilities available in the [TypeScript SDK](https://stellar.github.io/js-stellar-sdk) for authorization. We'll cover three types of invocations: - A simple invocation in which the source account of the transaction is the only signer for the invocation tree. - An invocation in which two accounts need to sign the invocation tree. - An invocation run in enforcement mode to confirm that signatures are correct. #### Example 1: source account authorization. In this variant, we will leverage the "source account authorization" variant: this is when the source account on the transaction is the only one that needs to sign for the invocation (see the "source account" variant of [`SorobanCredentials`](https://github.com/stellar/stellar-xdr/blob/v22.0/Stellar-transaction.x#L586-L595)). In this scenario, the signature on the transaction itself directly implies signing the invocation. ```javascript Asset, Keypair, Networks, Operation, authorizeEntry, TransactionBuilder, xdr, } from "@stellar/stellar-sdk"; const s = Server("https://soroban-testnet.stellar.org"); // Pretend is is a real, funded account. const signer = Keypair.random(); const xlmContract = Asset.native().contractId(Networks.TESTNET); async function main() { const tx = new TransactionBuilder(await s.loadAccount(signer.publicKey()), { networkPassphrase: Networks.TESTNET, fee: BASE_FEE, }) .addOperation( Operation.invokeContractFunction( xlmContract, [ ["balance", "symbol"], [signer.publicKey(), "address"], ].map((val, type) => nativeToScVal(val, { type })), ), ) .build(); const preppedTx = s.prepareTransaction(tx); preppedTx.sign(signer); const sendTx = await s.sendTransaction(preppedTx); return s.pollTransaction(sendTx.hash); } main().catch((e) => console.error(e)); ``` Notice that, in contrast to the following example, we didn't need to do simulation separately. This is because we can sign the transaction as-is rather than needing to inspect its authorization entries. #### Example 2: multi-party authentication. In this variant, we'll extend the required signatures to more than one party, so the source account is no longer enough. We'll leverage the [`authorizeEntry` helper](https://stellar.github.io/js-stellar-sdk/global.html#authorizeEntry), which is designed specifically for making it easy to sign the entries returned by transaction simulation. ```typescript Asset, Keypair, Networks, Operation, authorizeEntry, TransactionBuilder, xdr, } from "@stellar/stellar-sdk"; const s = Server("https://soroban-testnet.stellar.org"); // Pretend these are real, funded accounts. const signers = [Keypair.random(), Keypair.random()]; const xlmContract = Asset.native().contractId(Networks.TESTNET); async function main() { // Notice that the source account is the first keypair, but the transfer // occurs *from* the second keypair, which means the second keypair will // need to sign for an authorization entry to approve the transfer. const tx = new TransactionBuilder( await s.loadAccount(signers[0].publicKey()), { networkPassphrase: Networks.TESTNET, fee: BASE_FEE, }, ) .addOperation( Operation.invokeContractFunction( xlmContract, [ ["transfer", "symbol"], [signers[1].publicKey(), "address"], // from [signers[0].publicKey(), "address"], // to [1000, "i128"], // amount ].map((val, type) => nativeToScVal(val, { type })), ), ) .build(); const simResult = s.simulateTransaction(tx); // For every auth entry that needs signing, sign it with the correct keypair. // // Inject the auths back into the simulation result so they // get assembled into our transaction. simResult.result.auth = simResult.result.auth.map((entry) => authorizeEntry( entry, // Ignore source account entries, which is handled as a no-op. entry.credentials().switch() !== xdr.SorobanCredentialsType.sorobanCredentialsSourceAccount() ? signers.find( // Find the keypair that matches the entry's address. (signer) => Address.fromScAddress( entry.credentials().address().address(), ).toString() === signer.publicKey(), ) : null, response.latestLedger + 12, // signature is valid for ~1m Networks.TESTNET, ), ); const preppedTx = assembleTransaction(tx, simResult); preppedTx.sign(signers[0]); const sendTx = await s.sendTransaction(preppedTx); return s.pollTransaction(sendTx.hash); } main().catch((e) => console.error(e)); ``` Alternatively, we could go a step lower in the stack and build the authorization entries ourselves using [`authorizeInvocation`](https://stellar.github.io/js-stellar-sdk/global.html#authorizeInvocation), giving us full control over the actual "call stack" that is being invoked. This can be useful if you want to authorize specific invocations, build the invocations yourself, or lower the network bandwidth you use when sharing entries for other parties to sign. #### Example 3: enforcement mode. In this example, we'll leverage transaction [simulation][sim-tx]'s auth ["enforcement" mode](#enforcing-mode), which, when given signed authorization entries, will ensure that they are the necessary and sufficient signatures for the transaction's execution. To keep things really simple, we won't do much coding. Instead, we'll just show the difference from the previous example: all we need to do is run simulation once more. ```diff - preppedTx.sign(signers[0]); - - const sendTx = await s.sendTransaction(preppedTx); + const resimTx = await s.prepareTransaction(preppedTx); + resimTx.sign(signers[0]); + const sendTx = await s.sendTransaction(resimTx); ``` [auth-data]: stellar-transaction.mdx#authorization-data [auth-invoke]: https://github.com/stellar/stellar-xdr/blob/v22.0/Stellar-transaction.x#L558 [sim-tx]: ../../../../data/apis/rpc/api-reference/methods/simulateTransaction.mdx --- ## Contract Lifecycle {`The process of developing, deploying, and maintaining smart contracts.`} ## Development Contract development can be done on a local computer with as little as 3 necessary components: an IDE, a copy of the Rust toolchain, and a copy of the Soroban SDK. The SDK contains a full working copy of the host environment, as well as a "mock" version of the ledger for persistent storage of contract data. It is therefore possible (and encouraged) to edit, compile, test and debug contracts directly against this "local" copy of the host, entirely offline and without even accessing a test network. To make the local development process even more convenient and fast, the contract being developed can (and should) be compiled as native code and linked directly to the local host, rather than compiled to Wasm and run in a local VM sandbox. Both configurations are possible, but the native configuration is fastest and provides the richest testing and debugging experience. The SDK-provided local contract host also contains a local web server that serves the necessary HTTP API endpoint used for client applications to interact with a contract. This can be used for local development of applications, again without needing to deploy contracts to any test or live network. ## Deployment Once a contract has been tested and debugged locally, it can be deployed. To do this it must be compiled to Wasm code, and then included by value in a transaction sent to the intended deployment network. The SDK provides a command-line utility that invokes the Rust compiler with the correct settings for producing a Wasm bundle for deployment, but developers can also build this themselves. Before submitting to the network, developers should inspect the resulting Wasm binary emitted by the Rust compiler to ensure that it contains only the intended code and data, and is as small as possible. The SDK command-line utility contains diagnostic commands to assist with this process. The SDK command-line utility can also build and submit the transaction deploying a Wasm contract to the network. Deployment requires sufficient network credentials to sign a transaction performing the deployment and pay its fees. Contracts should be deployed to test networks and thoroughly tested there before being deployed to the live network. :::note [Verify your contract on StellarExpert](https://stellar.expert/explorer/public/contract/validation). This GitHub Actions process compiles and optimizes your Stellar contract, publishes a GitHub release with the build artifacts and SHA256 hashes, and sends the hash, repo name, and commit to StellarExpert. Once validated, StellarExpert displays a link to the exact GitHub commit so anyone can review the original source code and confirm it matches the deployed contract. ::: ## Execution Deployed contracts live on chain in a CONTRACT_DATA ledger entry. They are executed within a VM sandbox managed by a host environment inside stellar-core. Each transaction that leads to a contract execution is run in a separate host environment, and each contract called by such a transaction (either directly or indirectly from another contract) is executed in a separate guest Wasm VM contained within the transaction’s host environment. Execution is initiated by a host function called "call". The "call" host function can itself be invoked two different ways: either by someone submitting a transaction to the network that invokes "call" directly, or indirectly by some other contract invoking "call". In either case, the "call" host function is provided with the ID of a contract to invoke, the name of a function in the contract, and a vector of argument values to pass. The "call" host function then sets up a VM sandbox for the called contract, loads and instantiates its Wasm bytecode, and invokes the named function, passing the provided arguments. Each contract execution continues until the contract either completes successfully or traps with an error condition. If execution completes successfully, all ledger entries modified during execution will be written back to the ledger atomically. If execution traps, all modified ledger entries will be discarded and the contract will have no effect on the ledger. A variety of conditions in either the guest or host environments can cause a contract to trap. If a host function is called with invalid arguments, for example, the host will trap. Similarly if the contract performs an erroneous Wasm bytecode such as a division by zero or access to memory out of bounds, the Wasm VM will trap. Also if the contract uses more resources than its enclosing transaction has paid for, the contract will trap. ## Monitoring Contracts can be monitored in two main ways: by observing events emitted during their execution, and by examining the ledger entries written by them. ## Upgrading contracts See the [Upgrading Contracts page](../../../build/guides/conventions/upgrading-contracts.mdx) for details on this. --- ## Environment Concepts {`The interface that defines objects, functions, and data available to smart contracts.`} The contract environment is an _interface_ that defines the facilities -- objects, functions, data sources, etc. -- available to contracts. ## Host and Guest As an interface, the environment has two sides, which we refer to as the **host environment** and the **guest environment**. Code in the host environment _implements_ the environment interface; code in the guest environment _uses_ the environment interface. The **host environment** is provided by a known set of Rust crates, compiled once into stellar-core (or the SDK). Multiple contracts interact with the same host environment, and the host environment has access to any facilities of its enclosing operating system: files, networking, memory, etc. In contrast, a new **guest environment** is established for each invocation of each smart contract. Each contract sees a single environment interface, and can only call functions provided by the environment interface. In other words, the guest environment is a sandbox for executing arbitrary code within safe parameters. ## WebAssembly (Wasm) The on-chain guest environment is isolated inside a WebAssembly (Wasm) virtual machine ("VM"). This means that deployed contract code is compiled to Wasm bytecode rather than native machine code. The host environment includes an interpreter for the VM, and a new short-lived VM is instantiated for each call to a contract, running the bytecode for the contract and then exiting. The use of a VM helps provide security against any potential guest-code misbehavior, to both host and other guest environments, as well as ensuring portability of guest code between hosts running on different types of hardware. When developing and testing contract code off-chain, it is possible to compile contract code to native machine code rather than Wasm bytecode, and to [run tests and debug contracts](./errors-and-debugging/debugging.mdx) against a local copy of the host environment by linking directly to it, rather than executing within a VM. This configuration runs much faster and provides much better debugging information, but is only possible locally, off-chain. On-chain deployed contracts are always Wasm. WebAssembly is a relatively low-level VM, which means that it does not provide a very rich set of standard or "built-in" operations. In contrast to VMs like the JVM, it has no garbage collector (not even a memory allocator), no IO facilities, no standard data structures like lists, arrays, maps or strings, no concepts of objects or types at all besides basic machine types like 32 and 64-bit integers. As a result, programs compiled to Wasm bytecode often face a dilemma: if they want rich standard functionality, they must often include a copy of all the "support code" for that functionality within themselves. But if they do, they dramatically increase their code size, which incurs costs and limits performance. Moreover, including such support code limits their ability to interoperate with other programs that may include different, incompatible support code. The way out of this dilemma is for the environment itself to provide support code for rich standard functionality, in the form of host objects and functions that guest code can use by reference. Each contract refers to the same functionality implemented in the host, ensuring much smaller code size, higher performance, and greater interoperability between contracts. This is what Soroban does. ## Host objects and functions Shared, standard functionality available to all contract guest code is provided through the environment interface in terms of host objects and host functions. The environment supports a small number of types of host objects covering data structures like vectors, maps, binary blobs, addresses, strings and big integers. Host objects are all immutable, are allocated and reside within the host environment, and are only available in the guest environment by reference. Guest code refers to host objects by integer-valued handles. There is also a slightly larger set of host functions that act on host objects: creating, modifying, inspecting and manipulating them. Some host functions allow copying blocks of binary data into and out of the VM memory of the guest, and some host functions perform cryptographic operations on host objects. There are also host functions for interacting with select components of the host environment beyond the host object repertoire, such as reading and writing ledger entries, emitting events, calling other contracts, and accessing information about the transaction context in which guest code is executing. ### Serialization Host objects can be passed (by handle) directly to storage routines or between collaborating contracts. **No serialization or deserialization code needs to exist in the guest**: the host knows how to serialize and deserialize all of its object types and does so transparently whenever necessary. ## Values and types All host functions can accept as arguments and return values from, at most, the limited Wasm VM repertoire of machine-level types. To simplify matters, Soroban further limits all host functions to passing and returning values from within a single specialized form of 64-bit integers called "value" or "the value type". Through careful bit-packing, the value type can encode any of several separate types more meaningful to users than just "integers". Specifically, the value type can directly encode small integers (up to 56 bits), but also boolean true and false, signed or unsigned 32-bit integers, typed host object handles, typed error codes, small symbols (up to 9 latin-alphanumeric characters), or a unique void value. Individual bits in a value are allocated to tagging and switching between these cases dynamically, and host functions or objects that require specific cases may reject values of other cases. Since the value type can contain a handle to a host object, any container object that can contain the value type can in fact hold any object. Therefore the host map and vector types -- container data structures -- are defined merely as containers for the value type, where the specific case of each value may vary from container to container or even among the elements of a container. In this way, the host container types are more like the containers of dynamic-typed languages like JavaScript or Python. The SDK also provides static, uniformly-typed wrappers when this is desired, prohibiting values outside the designated case from being added to the container. ![](/assets/diagrams/environment-concepts.png) --- ## Errors and Debugging In-depth explanations and articles concerning specific topics of interest. --- ## Debugging Contract Errors To understand how to debug Soroban errors, first we must understand how the errors are associated with each step of the transaction flow, and the likely and common errors in each step of the transaction flow. ## General Transaction Flow The typical transaction submission process can be broken down into the following sequential steps, excluding external interactions like wallet interactions. Each step has its own set of potential errors: 1. **Transaction Simulation (optional):** - **What happens:** This step involves executing the RPC endpoint [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction.mdx). The endpoint executes a transaction in 'simulation' mode of host and records the necessary ledger entries, CPU instructions, required authorizations. The end result is akin to running transactions in Core with maxed-out resources, unrestricted ledger access, and no fees. - **Common failures:** Errors typically involve exceeding the network limit or encountering contract logic issues. 2. **Core Accepts the Transaction:** - **What happens:** Core evaluates each transaction to decide whether to accept or reject it. Rejected transactions are usually invalid or have insufficient fees, especially during high traffic periods. - **Common failures:** Invalid transactions, such as those with incorrect resource fees or overly high resource values, and invalid footprint are rejected. Other common errors include bad transaction signatures or insufficient funds in the source account. Valid transactions may only be rejected due to a low inclusion fee. 3. **Core Includes the Transaction in the Ledger:** - **What happens:** Accepted transactions remain in Core’s memory until they are either included in the ledger or evicted. - **Common failures:** Transactions may fail to be included in the ledger if they have a low inclusion fee and need to be evicted during traffic surges to accommodate more transactions. 4. **Core Applies the Transaction to the Ledger:** - **What happens:** Core executes the included transaction. - **Common failures:** This step has the widest range of potential errors. Failures could include accessing archived entries, resource depletion, accessing entries outside the specified footprint, or encountering logic failures within the contract. _For more information about fees, please visit [Fees, Resource Limits, and Metering](../../fees-resource-limits-metering.mdx#inclusion-fee)._ ## Detailed Soroban Errors ### 1. Transaction Simulation Errors here are returned by the host and propagated through RPC. This doesn’t cover other possible errors (e.g. network errors, errors in RPC itself etc.) | Error | Explanation | Fix | | --- | --- | --- | | `HostError(Budget, LimitExceeded)` | A network-defined resource limit has been exceeded (either instructions, or memory). Refer to diagnostic events to check which limit has been exceeded (99% of the time this will be instructions). | Optimize the contract to consume fewer resources. | | `HostError(Storage, MissingValue)` | Trying to access a ledger entry that does not exist. 99.9% of the time this means that either contract, or Wasm does not exist in the ledger (the remaining cases can only appear when the developer doesn’t use the Soroban SDK for contracts). Diagnostic events should indicate which entry is missing. | Deploy the respective contract or Wasm. | | `HostError(WasmVm, InvalidAction)` | There was a failure in some Wasm contract, typically a `panic!()`. Diagnostic events might provide more detailed information, but not always, as the `panic!()` messages are not included in the Wasm builds. | Fix the contract logic or invocation arguments. Since the most typical reason for encountering this is `panic!()`, it might be a good idea to use `panic_with_error!()` instead of `panic!()` everywhere. Writing more unit tests is recommended. | | `HostError()` | An arbitrary execution error, like accessing a value out of container bounds, overflow in i128 arithmetics, incorrect invocation argument type etc. The error code should provide a general idea of what is failing, but refer to diagnostic events for details. | Fix the contract logic or invocation arguments. Additional debugging can be done via unit tests. | ### 2. Core Accepts the Transaction The error is returned by the core immediately as a response to the transaction being sent and surfaced to the user through RPC. There are a few error-related fields in the Core’s response: - `status` contains one of the few coarse codes: it’s either “ERROR” for any transaction validation error, or one of the few special non-validation-related statuses. - `result` contains the encoded TransactionResult XDR that, in case of “ERROR” status, will contain the transaction-level error code starting with “tx”, such as `txMALFORMED`. - `txFAILED` transaction errors at this stage correspond to the operation-specific validation errors, so the operation result code should be examined. - `diagnostics` will contain additional error information in the Soroban diagnostic event format. This typically will be returned for `txSOROBAN_INVALID` transaction errors but may be used more extensively in the future. | Error from Core | Explanation | Fix | | --- | --- | --- | | status: `TRY_AGAIN_LATER` | There is already a transaction from the same source account in memory. The transaction has been rejected due to too low inclusion fee and has been resubmitted too soon. | Wait for the previous transaction to be applied and resubmit, or switch to channel accounts if higher throughput is needed. Wait more time before re-submitting the transaction. Build a transaction within the network limits. | | status: `ERROR`, error: `txMALFORMED` | Transaction is fundamentally wrong: Soroban operation is not the only operation in the transaction; Soroban extension is missing; Fee or resource fee is negative; Resource fee is greater than tx fee; Declared resources are higher than network-wide limits (e.g. it wants to use 200M instructions, while the ledger-wide limit is just 100M). | Make sure the transaction is well-formed and the total transaction fee is high enough. | | status: `ERROR`, error: `txINSUFFICIENT_FEE` | Most likely: The inclusion fee is too low during the traffic surge. Unlikely: The inclusion fee is lower than the network minimum (i.e., lower than 100 stroops). | Bump the transaction fee or wait until there is less traffic. | | status: `ERROR`, error: `txSOROBAN_INVALID`, diagnostics: “transaction $RESOURCE_NAME resources exceed network config limit” or similar | Some resource value specified in the transaction exceeds the network limit. For example, this would trigger if the transaction specifies 200M instructions, while the network limit is just 100M. Note, that this has nothing to do with what the transaction actually does. Only resource declarations are examined at this point. | Optimize the contract to fit into the resource limit and specify the respective value in transaction (typically by running [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction)). In the case if there is just too big a ‘safety margin’ for the resources (e.g. +20% given that the transaction already needs 90% of the network limit), reduce the resource declaration to fit the limit. | | status: `ERROR`, error: `txSOROBAN_INVALID`, diagnostics: footprint-related message | There are a number of footprint requirements, such as it shouldn’t contain duplicate keys, shouldn’t contain entries unsupported by Soroban etc. Diagnostic message will specify the details of which requirement has been violated. | Fix the footprint. This should only occur if the footprint has been built or modified manually; footprints included in the [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) response should always pass all checks. | | status: `ERROR`, error: `txSOROBAN_INVALID`, diagnostics: "transaction sorobanData.resourceFee is lower than the actual Soroban resource fee" | The resource fee specified in the transaction is not sufficient to cover the resources specifiedin the transaction. Note, that this has nothing to do with what the transaction actually does. Only resource declarations are examined at this point. | Increase the resource fee. This should normally only occur when resources are computed or modified manually. [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) always computes the sufficient resource fee. | | status: `ERROR`, error: `txFAILED`, operation error: `EXTEND_FOOTPRINT_TTL_MALFORMED` | One of the footprint requirements for ExtendFootprintTTL operation has not been fulfilled: - Only Soroban ledger entries can be extended. - Only readOnly footprint should be populated. - The TTL extension should be not larger than the maximum allowed TTL extension. | Make sure that footprints conform to the requirements. Ideally, client-side libraries should ensure that the TTL extension transactions are well-formed. | | status: `ERROR`, error: `txFAILED`, operation error: `RESTORE_FOOTPRINT_MALFORMED` | One of the footprint requirements for RestoreFootprint operation has not been fulfilled: Only persistent Soroban ledger entries can be restored; Only readWrite footprint should be populated. | Make sure that footprints conform to the requirements. Ideally, client-side libraries should ensure that the restoration transactions are well-formed. | | status: `ERROR`, error: `tx$CODE` | The remaining error codes have the same semantics for Soroban as they do for Stellar; these include errors like insufficient account balance, bad seq num, etc. These errors are usually straightforward to interpret according to the name. | Fix the issue corresponding to the code. There is nothing Soroban specific here. | ### 3. Core Includes the Transaction in the Ledger There are instances when the Core does not appear to include the transaction in the ledger, The following best practice is recommended: | Error | Explanation | Fix | | --- | --- | --- | | Transaction appears ‘stuck’ and is never applied | There is no direct error reporting here because the network can’t reasonably communicate which transaction that it drops (E.g. Node A has dropped the transaction doesn’t necessarily mean that some other Node B has dropped it). When querying against a data endpoint, the transaction might be reported as ‘pending’. | Introduce transaction time bounds on the client side. If a transaction hasn’t been applied within 1-2 minutes it’s highly unlikely that the network still remembers it. _It is strongly recommended thus that all transactions should include a time bound or ledger bound._ Having a bound allows the developer to re-submit the transaction after the time bound is exceeded and optionally bump the fee if the error persists. | :::info It is strongly recommended that all transactions should include a time bound or ledger bound. ::: ### 4. Core Applies the Transaction to the Ledger The errors and diagnostic events are recorded in the transaction meta stream emitted by the Core instance when the ledger is being closed. Then, the RPC ingests the transaction metadata (tx meta) and allows developers to query the tx meta. When an error occurs, the Core stores a few error-related fields in the transaction result meta: - The transaction result (error) will be `txFAILED` for Soroban-related failures (or any operation failures in general). It is sometimes possible to get other `tx$ERROR` errors (such as `txBAD_AUTH`), but these are not related to Soroban and are more of an edge case. - The operation error will be one of `INVOKE_HOST_FUNCTION_$ERROR`, `RESTORE_FOOTPRINT_$ERROR`, and `EXTEND_FOOTPRINT_TTL_$ERROR` (corresponding to InvokeHostFunction, RestoreFootprint, and ExtendFootprintTTL operations). The operation errors are not very granular, and diagnostic events should typically be used to understand the exact error. - If the Core instance producing meta has Soroban diagnostics enabled (which it usually should), the meta will also contain diagnostic events with more detailed error information. - Note that there might be a few gaps in diagnostic event coverage. Since this is not a protocol change, Core may add more diagnostic events in future releases. :::note `$OPERATION` here refers to any one of the Soroban operations: `INVOKE_HOST_FUNCTION`, `RESTORE_FOOTPRINT`, and `EXTEND_FOOTPRINT_TTL`. ::: | Error and Diagnostics | Explanation | Fix | | --- | --- | --- | | `$OPERATION_RESOURCE_LIMIT_EXCEEDED`, diagnostics: message specifying which resource limit has been exceeded | The transaction has exceeded the resource limit during execution. For most resources (instructions, read bytes, write bytes), this has nothing to do with the network limit; it's the transaction-specified limit. For example, a transaction has 10M instructions specified but consumes at least 10M + 1 instruction and immediately fails. Diagnostic events specify which limit has been exceeded and contain both the limit itself and the value transaction tried to consume. | If exceeding the transaction-specified limit, increase the respective resource declared in the transaction. If [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) determined the resource limits, likely reason for exceeding is logic dependent on volatile ledger state (e.g., RNG or ledger sequence). Ensure logic is estimated sufficiently by [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction). If exceeding network limits (entry size, memory), modify/ optimize the contract. | | `$OPERATION_INSUFFICIENT_REFUNDABLE_FEE` | The refundable resource fee was not sufficient to cover the consumed refundable resources, i.e., not enough to pay for emitted events or TTL extensions. | The simplest fix is to increase the resource fee unconditionally; unspent fees are refunded. [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) estimates refundable fees but can't predict TTL extensions' actual cost due to ledger state volatility. Thus [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) can’t always predict the fee that will actually be charged. | | `INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED` | Transaction tries to access an archived ledger entry, e.g. when the footprint contains a ledger key for an archived persistent Soroban ledger entry. Note, this failure happens before the Soroban host is even created, so this is unrelated to contract logic. | Restore the archived entry using RestoreFootprintOp operation. [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) usually reports archived entries, but an entry may be archived after the transaction simulation happened. | | `INVOKE_HOST_FUNCTION_TRAPPED`, diagnostics: HostError(Storage, LimitExceeded). | Host function tried to access a ledger entry outside of the footprint. Diagnostic events specify the exact missing entry. [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) should usually return the valid footprint, so in the case when [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) is being used, it’s likely that: a) Contract generates non-deterministic keys (e.g. derives them from RNG or ledger sequence), b) Contract has non-deterministic logic that results in access to a different entry set (e.g. a ‘lottery’ contract that non-deterministically performs transfer to either address A or address B). | If [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) is not used, fix client code to build the correct footprint. If using [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction), fix contract or footprint: avoid non-deterministic keys , and add all keys that can be accessed in any scenario, (e.g. balance entries for both A and B in the ‘lottery’ contract example). | | `INVOKE_HOST_FUNCTION_TRAPPED`, diagnostics: HostError(WasmVm, InvalidAction) | Failure in a Wasm contract, typically a `panic!()`. Diagnostic events might provide more detailed information, but not always, as the `panic!()` messages are not included in the Wasm builds. | Fix contract logic. Consider using `panic_with_error!()` and increase [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) runs and unit testing. | | `INVOKE_HOST_FUNCTION_TRAPPED`, diagnostics: HostError(Auth, InvalidAction) "Unauthorized function call for address \<'ADDRESS'>" | The transaction didn’t have an authorization payload necessary to satisfy the `require_auth` host function call for a given \<’ADDRESS’>. This is unlikely to occur when [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) is being used, but still possible if the `require_auth` arguments depend on volatile ledger state (e.g. if `require_auth` contains the ledger sequence number) | Attach proper authorization payload to the transaction, usually via [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction). Ensure auth payloads are deterministic and depend only on invocation arguments. | | `INVOKE_HOST_FUNCTION_TRAPPED`, diagnostics: HostError(Auth, \<'error code'>) | Authentication error, like missing/expired/invalid signature or reused nonce. These errors won’t always appear in the [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) response unless the whole signed auth payload is used in the [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) request. The diagnostic events will also have a more detailed explanation of what the error was. | Fix authentication payload according to error (e.g. use proper signature, new nonce etc.). Consider running [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) with all signatures for faster debugging. | | `INVOKE_HOST_FUNCTION_TRAPPED`, HostError(\<'some other code'>) | An arbitrary execution error, like accessing a value out of container bounds, overflow in i128 arithmetics, incorrect invocation argument type etc. The error code should provide a general idea of what is failing, but refer to diagnostic events for more details. | Fix contract logic or invocation arguments. Additional debugging can be done via more [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction) runs and unit tests. | --- ## Debugging Modes Debug smart contracts natively and as WASM. Soroban contracts are as much as possible regular Rust programs and can be debugged using the same tools you'd usually use. The debugging facilities available differ significantly depending on whether a contract is compiled natively for local testing, or compiled into Wasm for deployment. Deciding between these two modes and making the most of them while debugging requires a **careful understanding** of which code is compiled-in to deployed contracts and which code is only available for local testing. ## Local-testing mode It is possible (and encouraged during development) to compile Soroban contracts **natively** (eg. as x86-64 or AArch64 code) and link against the host environment **directly**, such that the contract is not a guest running in a Wasm virtual machine at all: it is simply one native library calling another -- its host -- and both host and contract are linked together as a single program, together with a test harness. This configuration is referred to as **"local-testing mode"** and since it eliminates the Wasm virtual machine from the debugging experience it has many advantages: - Tests run much faster since there is no VM interpreting them, and will execute in parallel by default on multiple threads. - Tests can use numerous standard testing and debugging techniques: - [The standard Rust `#[test]` harness](https://doc.rust-lang.org/reference/attributes/testing.html), including push-button IDE support for running and re-running single tests. - [Standard IDE-supported debuggers](https://code.visualstudio.com/docs/languages/rust#_debugging), including IDE support for setting breakpoints and inspecting values in both contract and host. - Lightweight debugging via [standard logging](https://docs.rs/log/latest/log) or [tracing](https://docs.rs/tracing/latest/tracing). - Systematic testing such as [fuzzing](https://crates.io/crates/cargo-fuzz), [property-testing](https://crates.io/crates/proptest), or even [model checking](https://crates.io/crates/kani-verifier) or [formal verification](https://github.com/xldenis/creusot). - The simplest of all debugging approaches, [printing to standard error](https://doc.rust-lang.org/std/macro.eprintln.html). Local-testing mode is the **default** configuration when compiling code targeting your local computer's CPU and operating system, which is what cargo will do if you set up a new Rust project and don't specify a target. ## Wasm mode If on the other hand you wish to compile for deployment, you must tell cargo to build for the Wasm target. Building for Wasm will _disable_ many of the debugging facilities described above, typically for one of three reasons: - The Wasm VM simply can't (or the VM we've chosen doesn't) provide them. - The Wasm VM _could_ provide them but doing so would violate constraints of the [contract Rust dialect](../rust-dialect.mdx). - The Wasm VM _could_ provide them but doing so would make the resulting Wasm code impractically large. While we encourage most testing to happen in local-testing mode, some problems will obviously only arise in deployment and some debugging facilities thus remain available even there: - A "sandbox" host with a mock-ledger that can read and write `CONTRACT_DATA` ledger entries to the local filesystem. - A general logging system that allows contracts to log values of the [shared host/guest "value" type](../environment-concepts.mdx), even in production. - User-extensible `Error` codes that can be returned from any contract call to indicate problems. --- ## Generate Contract Errors Generating errors from smart contracts. There are a number of ways to generate errors in contracts. One way is error enum types, that are defined by contracts and that map errors to unsigned 32-bit integer values. They are usable as error values in the return types of contract functions. :::info The [errors example] demonstrates how to define your own error types. ::: [errors example]: ../../../../build/smart-contracts/example-contracts/errors.mdx ## Error Enums Errors are a special type of enum integer type that are stored on ledger as `Error` values containing a `u32` code. ```rust #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum Error { AnError = 1, } ``` When converted to XDR, the value becomes an `ScVal`, containing a `ScError`, containing the integer value of the error as contract error. ```json { "error": { "contractError": 1 } } ``` --- ## Overview(Contract-development) This section provides an overview of Stellar contracts, outlining their key concepts of smart contracts on the Stellar network. Learn to write your first Stellar smart contract in the [Getting Started Guide](../../../build/smart-contracts/getting-started/setup.mdx). ## Smart Contract in WebAssembly (Wasm) A smart contract on Stellar is a programmable set of executable binary in WebAssembly (Wasm). WebAssembly (Wasm) is a lightweight, portable binary instruction format designed for high-performance execution across various environments, including blockchains, web browsers, and cloud services. In Stellar, Wasm serves as the foundation for smart contracts by enabling a secure and efficient execution environment. - **Compilation**: Once smart contract is written in a supported language, such as Rust, the contract is compiled into a Wasm executable file. This file contains low-level bytecode that can be efficiently executed within a sandboxed environment. - **Upload and stored**: After compilation, the Wasm executable is first uploaded on the Stellar network, where it is stored in a `CONTRACT_DATA` ledger entry. The `CONTRACT_DATA` ledger entry is created to store the Wasm binary data. - **Unique hash is created**: An unique identifier for this `CONTRACT_DATA` entry is the hash of the executable file, called `Wasm hash`. To note, this binary executable is stored independently from its deployed contract(s). - **Multiple contract instances**: Multiple contract instances can be deployed that references the same Wasm bytecode, with each instances maintaining its own contract storage, state, and configurations. ## Contract Instances After the executable bytecode is uploaded on-chain, contract instances can be deployed that reference the same bytecode. **A contract Wasm executable can have a one-to-many relationship with its contract instances which function independently.** This means the same executable code can be used by multiple contract instances that all behave identically, because of the shared executable code, while maintaining separate and distinct state data, because the data is tied to the contract instance. A contract instance is stored as its own ledger entry, and any of the contract's instance storage is stored in that same ledger entry alongside the contract instance. Therefore, data stored in contract's instance storage has a TTL as the contract instance itself. If a contract is live and unexpired, the instance storage is guaranteed to be available as well. ```mermaid flowchart LR A[my instance] & B[your instance]--> C[contract Wasm] ``` :::info[Important Takeway] The Wasm bytecode itself is stored separately from the deployed contract instances. This separation allows multiple contract instances to reference the same Wasm executable while maintaining their own contract storage, state, and configurations. ::: ## Contract Storage In addition to the ledger entries that are created during the contract upload/deploy process, each contract can create and access its own set of ledger entries. These ledger entries (as well as the contract code and the contract instance ledger entries) are subject to [state archival](./storage/state-archival.mdx) lifetimes, or TTL (time-to-live) behavior. Each storage type has distinct fee structures, TTL (time-to-live) behavior, and is designed to store specific types of data. Using the incorrect storage can lead to logical errors in your application, in addition to impacting costs. | **Attribute** | **Temporary Storage** | **Persistent Storage** | **Instance Storage** | | --- | --- | --- | --- | | **Fees** | Cheapest | Most expensive (same as Instance) | Most expensive (same as Persistent) | | **Persistence** | Permanently deleted when TTL ledger is reached | Can be archived even if contract instance is active. Lifetime is independent from contract instance. | Shares the lifetime of the contract instance. Data accessible if instance TTL not reached. | | **Recovery** | Cannot be restored | `RestoreFootprintOp` | `extendFootprintTTLOp` | | **Capacity** | Unlimited | Unlimited | Limited | | **Use Cases** | Time-bounded or easily re-creatable data (e.g. price oracles, signatures). Not suitable for correctness checks | Long-term user data that must persist beyond TTL (e.g. balances) | Shared contract state tied to contract instance (e.g. admin accounts, contract metadata) | ## Invoke Contracts Now that you have learned the basics of a Stellar contract, let's understand some concepts about contract invocation. When a transaction attempts invokes a function on the contract, the Wasm bytecode is first retrieved from the ledger, and a secure, isolated runtime virtual machine ("VM") is instantiated so it can run the bytecode for the contract and then exit. This approach offers several benefits, including security as the virtual machine (VM) operates in a sandboxed environment and provides deterministic execution. To test out invoking contracts on Testnet, please see [Stellar Lab's contract explorer](https://lab.stellar.org/smart-contracts/contract-explorer?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&smartContracts$explorer$contractId=CB2RXGQVNGOMHW3XYCPKREXHD45M4DMJ2PTBETV6P3EMV22QLJFUQHWB;;). ## Contract Info A Stellar contract's Wasm contains dedicated custom sections. They provide useful meta data that implementations can make use of to improve user experience. - Environment Meta - Contract Meta - Contract Spec ![Contract Info Meta](/assets/contract/contract-info-meta.png) ![Contract Info Spec](/assets/contract/contract-info-spec.png) _Explore contract info at [Stellar Lab's Contract Explorer](https://lab.stellar.org/smart-contracts/contract-explorer)._ ### Environment Meta Contracts must contain a Wasm custom section with name `contractenvmetav0` and containing a serialized `SCEnvMetaEntry`. The interface version stored within should match the version of the host functions supported. For reference, please see [CAP-46-1 Env Meta](https://github.com/stellar/stellar-protocol/blob/603a55b018a8ce12ac16aa8621d189d5b78d0d02/core/cap-0046-01.md?plain=1#L508). To view the Environment Meta, please consider using Stellar Lab's Contract Explorer, or Stellar CLI's command `stellar contract info env-meta --contract-id `. ### Contract Meta Contracts can optionally include a custom Wasm section named `contractmetav0`, which contains a serialized `SCMetaEntry`. This section is not used by the network itself, but allows contracts to embed arbitrary metadata, including contract name, version, author, supported interfaces, source repo, or home domain. Applications and tooling can read this metadata to provide richer developer experiences, better indexing, or enhanced contract discovery. For reference, please see [SEP-46 Contract Meta](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0046.md). To add metadata to Contract Meta, please use the Stellar CLI command `stellar contract build --meta `, or use the Rust SDK (e.g. `contractmeta!(key="name", val="Defi Swap Contract")`). To view the Contract Meta, please use the Stellar Lab's Contract Explorer, or the Stellar CLI command `stellar contract info meta --contract-id `. ### Contract Spec Contracts should contain a Wasm custom section with name `contractspecv0` and containing a serialized stream of [`SCSpecEntry`]. There should be a [`SCSpecEntry`] for every function, struct, and union exported by the contract. For reference, please see [SEP-48 Contract Interface Specification](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0048.md). A contract spec is just like an ABI (Application Binary Interface) in Ethereum. It is a standardized description of a smart contract's interface, typically in JSON format or XDR format. It defines the contract's functions, data structures, events, and errors in a way that external applications can understand and use. When you compile a smart contract using the Rust SDK, the resulting Wasm file includes a special section containing a complete description of your contract's interface types. This is often referred to as the contract's `spec` or `contract spec`. Stellar smart contract specification, known as the `contract spec`, is a foundational element for interacting with contracts and for building dApps on the Stellar network. The contract Spec provides a robust and fully typed definitions for interacting with smart contracts, offering functionality equivalent to Ethereum's ABI while addressing its limitations. The contract spec serves as a standardized interface for interacting with Stellar smart contracts. Similar to Ethereum's ABIs, but with key advantages: - On-chain Availability: Every contract spec is stored on-chain - Developer Comments: Comments from the contract author are preserved - Seamless communication between contracts and external applications - Ecosystem-wide compatibility with tools like wallets, explorers, and SDKs This standardization simplifies integrations and accelerates the development process. #### Fully Typed Contract Definitions The Contract Spec enforces fully typed definitions for all contract functions, inputs, and outputs. This ensures that: - Developers can define contract behavior explicitly, reducing ambiguity. - Type mismatches and runtime errors are minimized, leading to more reliable smart contracts. - Tools can provide intelligent suggestions and validations during development. By embedding type safety at the protocol level, Stellar’s Contract Spec creates a more predictable and robust development environment. #### Comparison to Ethereum ABI Stellar’s Contract Spec shares many similarities with Ethereum’s ABI but also introduces enhancements: | Feature | Ethereum ABI | Stellar Contract Spec | | ----------------------- | ------------------ | --------------------------- | | Fully typed contracts | Partial | Yes | | Decoding and validation | Manual or external | Built-in | | Security focus | Moderate | High (type safety enforced) | #### Generating contract specs [The Stellar CLI](https://github.com/stellar/stellar-cli) provides a command to generate a contract spec from a contract's source code. This process is easy but requires you to have the Wasm binary of the contract. Sometimes, you may not have access to the contract's source code or the ability to compile it. In such cases, you must use the [`stellar contract fetch`](../../../tools/cli/stellar-cli.mdx#stellar-contract-fetch) command to download the contract's Wasm binary and generate the spec. Finally, we use the [`stellar bindings`](../../../tools/cli/stellar-cli.mdx#stellar-contract-bindings-json) command to generate the contract spec from the Wasm binary. The Stellar Lab has a [Contract Explorer](https://lab.stellar.org/smart-contracts/contract-explorer) also provides the ability to view and to download a contract's contract spec. :::note Having read the introduction of a Stellar contract, let's try to write a smart contract by following the [Getting Started guide](../../../build/smart-contracts/getting-started/README.mdx). ::: [`scspecentry`]: https://github.com/stellar/stellar-xdr/blob/next/Stellar-contract-spec.x --- ## Contract Rust Dialect Contract development occurs in the Rust programming language, but several features of the Rust language are either unavailable in the deployment guest environment, or not recommended because their use would incur unacceptable costs at runtime. For this reason it makes sense to consider code written for contracts to be a _dialect_ or special variant of the Rust programming language, with certain unusual constraints and priorities, such as determinism and code size. These constraints and priorities are _similar_ to those encountered when writing Rust code for "embedded systems", and the tools, libraries and techniques used in the "contract dialect" are frequently borrowed from the [Rust embedded systems community](https://docs.rust-embedded.org/book/index.html), and by default contracts are recommended to be built with the [`#[no_std]` mode](https://docs.rust-embedded.org/book/intro/no-std.html) that excludes the Rust standard library entirely, relying on the smaller underlying `core` library instead. :::note These constraints and priorities are **not enforced when building in local-testing mode**, and in fact local contract tests will _frequently_ use facilities -- to generate test input, inspect test output, and guide testing -- that are not supported in the deployment guest environment. Developers **must understand** the difference between code that is compiled-in to Wasm modules for deployment and code that is conditionally compiled for testing. See [debugging contracts](./errors-and-debugging/debugging.mdx) for more details. ::: The "contract dialect" has the following characteristics: ## No floating point Floating-point arithmetic in the guest is completely prohibited. Floating-point operations in Wasm have a few nondeterministic or platform-specific aspects: mainly NaN bit patterns, as well as floating-point environment settings such as rounding mode. While it is theoretically possible to force all floating-point code into deterministic behavior across Wasm implementations, doing so on some Wasm implementations may be difficult, costly, or error-prone. To avoid the complexity, all floating-point code is rejected at instantiation time. This restriction may be revisited in a future version. ## Limited (ideally zero) dynamic memory allocation Dynamic memory allocation within the guest is **strongly** discouraged, but not completely prohibited. The host object and host function repertoire has been designed to relieve the guest from having to perform dynamic allocation within its own linear memory; instead, the guest is expected and intended to allocate dynamic structures _within host objects_ and interact with them using lightweight handles. Using host objects instead of data structures in guest memory carries numerous benefits: much higher performance, much smaller code size, interoperability between contracts, shared host support for serialization, debugging and data structure introspection. The guest does, however, have a small linear memory available to it in cases where dynamic memory allocation is necessary. Using this memory carries costs: the guest must include in its code a full copy of a memory allocator, and must pay the runtime cost of executing the allocator's code inside the VM. This restriction is due to the limited ability of Wasm to support code-sharing: there is no standard way for the Wasm sandbox to provide shared "standard library" code within a guest, such as a memory allocator, nor does the host have adequate insight into the contents of the guest's memory to provide an allocator itself. Every contract that wishes to use dynamic allocation must therefore carry its own copy of an allocator. Many instances where dynamic memory allocation might _seem_ to be required can also be addressed just as well with a library such as [heapless](https://docs.rs/heapless/latest/heapless). This library (and others of its kind) provide data structures with familiar APIs that _appear_ dynamic, but are actually implemented in terms of a single stack or static allocation, with a fixed maximum size established at construction: attempts to grow the dynamic size beyond the maximum size simply fail. In the context of a contract, this can sometimes be preferable behavior, and avoids the question of dynamic allocation entirely. ## Non-standard I/O All standard I/O facilities and access to the operating system that a typical Rust program would expect to perform using the Rust standard library is prohibited; programs that try to import such functions from the host through (for example) the WASI interface will fail to instantiate, since they refer to functions not provided by the host. No operating system, nor any simulation thereof, is present in the contract sandbox. Again, the repertoire of host objects and host functions is intended to replace and largely obviate the need for such facilities from the standard library. This restriction arises from the fact that contracts need to run with _stronger_ guarantees than those made by typical operating-system APIs. Specifically contracts must perform I/O with all-or-nothing, transactional semantics (relative to their successful execution or failure) as well as serializable consistency. This eliminates most APIs that would relate to typical file I/O. Furthermore contracts must be isolated from all sources of nondeterminism such as networking or process control, which eliminates most of the remaining APIs. Once files, networking and process control are gone, there simply isn't enough left in the standard operating system I/O facilities to bother trying to provide them. ## No multithreading Multithreading is not available. As with I/O functions, attempting to import any APIs from the host related to multithreading will fail at instantiation time. This restriction is similarly based on the need for contracts to run in an environment with strong determinism and serializable consistency guarantees. ## Immediate panic The Rust `panic!()` facility for unrecoverable errors will trap the Wasm virtual machine immediately, halting execution at the instruction that traps rather than unwinding. This means that `Drop` code in Rust types will not run during a panic. This behavior is similar to the `panic = "abort"` profile that Rust code can (and often is) compiled with. This is not a hard restriction enforced by the host, but a soft configuration made through a mixture of SDK functions and flags used when compiling, in the interest of minimizing code size and limiting execution costs. It can be bypassed with some effort if unwinding and `Drop` code is desired, at the cost of greatly increased code size. ## Pure-functional collections Host objects have significantly different semantics than typical Rust data structures, especially those implementing _collections_ such as maps and vectors. In particular: host objects are **immutable**, and any "modification" to a host object returns a **full new copy** of the object, leaving the initial one unchanged. For the most part this distinction is hidden through wrappers in the SDK, such that objects like `Map` or `Vec` appear to the contract programmer to be uniquely owned mutable values similar to Rust's standard library types, but the underlying host objects are immutable, so have different performance characteristics. Specifically: cloning such an object is O(1), whereas any modification is O(N). Since most host objects are typically very small, the O(N) cost of modification is typically cheaper than any alternative implementation involving shared substructures. :::note These container types `Vec` and `Map` should _not_ be used for managing large or unbounded collections of data. For such cases, contracts should store data in multiple separate ledger entries, each with its own unique contract-defined key. Doing so also limits the IO cost of a contract to only the entries it accesses, and furthermore allows concurrent modification of entries with separate keys from separate transactions. ::: ## Limited WebAssembly features The WebAssembly specification has grown significantly since its initial introduction and now supports many _features_ that may or may not be available on a given implementation of WebAssembly. Soroban intentionally limits which WebAssembly features it supports, to minimize the security-critical surface area and retain flexibility in choice of WebAssembly implementations. As of Rust `v1.84.0`, a new target `wasm32v1-none` was added to Rust that intentionally restricts itself to the "WebAssembly 1.0" subset of features, all of which Soroban supports. New Soroban contracts should be built with Rust `v1.84.0` or later, and use the `wasm32v1-none` target. The easiest way to do this is with `stellar contract build` from [stellar-cli], which targets `wasm32v1-none` and applies all required build settings automatically. :::warning The `wasm32-unknown-unknown` target is **not supported** for building Soroban contracts when using Rust 1.82 or newer. On those versions, `wasm32-unknown-unknown` enables WebAssembly features (reference-types, multi-value) that the Soroban runtime does not support. Attempting to build a contract for `wasm32-unknown-unknown` with Rust 1.82+ will produce a build error from the SDK's build script. Use `wasm32v1-none` (available with Rust 1.84+) as the build target, or use `stellar contract build` which selects the correct target automatically. ::: :::note The `wasm32v1-none` target does not, by default, ship with a version of the `std` library at all. When adding the `wasm32v1-none` target, it was decided that these sorts of stubs were not of any benefit to users. The `wasm32v1-none` target _does_ include a copy of the `alloc` crate, which contains most of the _containers_ (such as vectors and maps) that are re-exported by `std`. For example, rather than using `std::vec::Vec` one can use `alloc::vec::Vec`, which is the same code under a different name. But as mentioned above in the section on dynamic memory allocation, generally Soroban contracts should avoid `alloc` as well: a crate like `heapless` will usually perform better. ::: [stellar-cli]: https://github.com/stellar/stellar-cli --- ## Storage(Storage) --- ## Persisting Data Store and access smart contract data. ## Ledger entries Contracts can access ledger entries of type `CONTRACT_DATA`. Host functions are provided to probe, read, write, and delete `CONTRACT_DATA` ledger entries. Each `CONTRACT_DATA` ledger entry is keyed in the ledger by the contract ID that owns it, its storage type (`Persistent`, `Temporary`, `Instance`) as well as a single user-chosen value, of the standard value type. This means that the user-chosen key may be a simple value such as a symbol, number or binary blob, or it may be a more complex structured value like a vector or map with multiple sub-values. Each `CONTRACT_DATA` ledger entry also holds (in addition to its key) a single value associated with the key. Again, this value may be simple like a symbol or number, or may be complex like a vector or map with many sub-values. No serialization or deserialization is required in contract code when accessing `CONTRACT_DATA` ledger entries: the host automatically serializes and deserializes any ledger entries accessed, exchanging them with the contract as deserialized values. If a contract wishes to use a custom serialization format, it can store a binary-valued `CONTRACT_DATA` ledger entry and provide its own code to serialize and deserialize, but Soroban has been designed with the intent to minimize the need for contracts to ever do this. ## Access Control Contracts are only allowed to read and write `CONTRACT_DATA` ledger entries owned by the contract: those keyed by the same contract ID as the contract performing the read or write. Attempting to access other `CONTRACT_DATA` ledger entries will cause a transaction to fail. There is no access control for TTL extension operations. Any user may invoke `ExtendFootprintTTLOp` on any LedgerEntry. ## Granularity A `CONTRACT_DATA` ledger entry is read or written from the ledger in its entirety; there is no way to read or write "only a part" of a `CONTRACT_DATA` ledger entry. There is also a fixed overhead cost to accessing any `CONTRACT_DATA` ledger entry. Contracts are therefore responsible for dividing logically "large" data structures into "pieces" with an appropriate size granularity, to use for reading and writing. If pieces are too large there may be unnecessary costs paid for reading and writing unused data, as well as unnecessary contention in parallel execution; but if pieces are too small there may be unnecessary costs paid for the fixed overhead of each entry. ## Footprints and parallel contention Contracts are only allowed to access ledger entries specified in the footprint of their transaction. Transactions with overlapping footprints are said to contend, and will only execute sequentially with respect to one another, on a single thread. Transactions with non-overlapping footprints may execute in parallel. This means that a finer granularity of `CONTRACT_DATA` ledger entries may reduce artificial contention among transactions using a contract, and thereby increase parallelism. ## Contract Data Best Practices ### Account State vs. Shared State While there is no distinction between "account" state and "shared" state at the protocol level, it can be helpful to think of data in these terms when deciding what storage type and TTL Extension strategy to use for a given use case. As a guideline, account and shared state can be described as follows: - Most contract state can either be associated with a specific account or shared between multiple stakeholders (“public good”) - Account specific state - Balances, positions, allowances, etc. - Shared state - Admin entry, pool values, etc. - There are two subcategories of shared state - "Global" state shared by all contract users - Contract instance, contract wasm, or a global admin - State shared by only a specific subset of users - AMM pool values In an AMM monolith contract (Uniswap V4 style) - Note: Sometimes account and shared state can merge when the contract scope is small - I.e. a contract account or a single NFT, where a new contract instance is generated for each account ### Owned Contracts vs. Autonomous Contracts In addition to the types of state, it is also helpful to consider the type of contract instance being used. Again, these types are not enforced at the protocol level, but can be helpful. - Owned contracts - Contract instances that have a clear owner - A contract account or a single NFT, where a new contract instance is generated for each account - Custom reserve backed assets (USDC, etc) - Autonomous contracts - Contract instance that have not clear owner, or have a decentralized group of owners - Most DeFi protocols, especially non-upgradable ones ### Best Practices - Prefer `Temporary` over `Persistent` and `Instance` storage - Anything that can have a timeout should be `Temporary` with TTL set to the timeout. See the [resource limits table](https://lab.stellar.org/network-limits) on the Stellar Lab for the current maximum TTL/timeout. - Ideally, `Temporary` entries should be associated with an absolute ledger boundary and thus never need a TTL extension - Example: Soroban Auth signatures have an absolute expiration ledger, so nonces can be stored in `Temporary` entries without security risks - Example: SAC allowance that lives only until a given ledger (so that some old allowance signature can not be used in the future if not exhausted) - All global state that cannot be `Temporary` should be in `Instance` storage - This guarantees that the TTL of the contract instance and all relevant globals are tied together - Since global state is used often, this ensures that global state will never need to be restored, leading to cheaper and more efficient contract invocations - Autonomous contracts should extend the TTL of any shared state touched by an invocation via the `extend_ttl()` host function - Given that these contracts have no owners, leaving TTL extensions to benevolent clients might lead to a “tragedy of the commons” situation - Most users do not extend the TTL because it is not required, but still benefit from the benevolent client who does extend the TTL - Owners of owned contracts should subsidize shared state TTL extension fees by manually submitting extend operations - Owners should keep track of TTLs for all shared state - This could be implemented via something like a cron job where a `ExtendFootprintTTLOp` for all relevant shared state is submitted periodically (i.e. once a month) - Alternatively, this could also be implemented via an admin smart contract function routinely called by the owner - Clients (Wallets/Dapps) should identify the state that relates to their respective account, present TTL info to the users, and suggest TTL extensions when necessary - TTL extensions should never be relied on for functionality or safety - Unsafe example: An entry must be permanent, but instead of using `Persistent` storage, a contract uses `Temporary` storage but will continually extend the entry so it is always live - Because there is no automatic TTL extension interface, and every TTL extension must come from either a smart contract invocation or `ExtendFootprintTTLOp`, it must be assumed that an entry's TTL can go to 0 and the entry be deleted - TTL extension fees are variable, and it may become too expensive to extend the TTL of pseudo “persistent” `Temporary` entries if fees increase unexpectedly - If the TTL extension process is automated (i.e. a cron job) the account automatically sending `ExtendFootprintTTLOps` may run out of funds unexpectedly should TTL extension fees increase, causing the `Temporary` entry to exhaust its TTL and be permanently deleted - Entry TTL exhaustion should never be relied on for functionality or safety - Unsafe example: a nonce should expire in 7 days, so a contract creates a `Temporary` entry and extends the TTL to 7 days with no other lifetime enforcement in place. - Anyone can submit a TTL extension operation on any entry without authorization, meaning that the nonce can have its TTL extended indefinitely by any user - If an entry needs to be invalidated after a certain time period, this must be implemented manually by the contract --- ## State Archival(Storage) Smart contract state archival. Contract data is made up of three different types: `Persistent`, `Temporary`, and `Instance`. In a contract, these are accessed with `env.storage().persistent()`, `env.storage().temporary()`, and `env.storage().instance()` respectively; see the [`storage()` docs](https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Storage.html). Learn about choosing the right storage for your use case in this [How-To Guide](../../../../build/guides/storage/choosing-the-right-storage.mdx) and other state archival related guides [here](../../../../build/guides/archival). All contract data has a Time To Live (TTL) that must be periodically extended. If an entry's TTL is not periodically extended, the entry's TTL will eventually go to 0 and either become "archived" or permanently deleted depending on the storage type. Each type of storage functions similarly, but has different fees and archival behavior: - When a `Temporary` entry's TTL is 0, it is deleted from the ledger and is permanently inaccessible - When a `Persistent` or `Instance` entry TTL is 0, it is "archived" and can't be accessed until it is "restored". There are two ways to restore entries: - (usually) Automatically when an archived entry is accessed by `InvokeHostFunction` operation - (rarely) By manually building the `RestoreFootprintOp` operation ## Contract Data Type Descriptions The general usage and interface are identical for all storage types. They differ only in fees and archival behavior as follows: ### `Temporary` - Cheapest fees. - Permanently deleted when TTL goes to 0, cannot be restored. - Suitable for time-bounded data (i.e. price oracles, signatures, etc.) and easily recreateable data. - Unlimited amount of storage. ### `Instance` - Most expensive fees (same price as `Persistent` storage). - Archived when TTL goes to 0, is automatically restored via the `InvokeHostFunction` operation (or restored manually by using the `RestoreFootprintOp` operation in rare cases). - Shares the same TTL as the contract instance. If the contract instance has not been archived, instance data is guaranteed to be accessible and not archived. - Limited amount of storage available. - Suitable for "shared" contract state that cannot be `Temporary` (i.e. admin accounts, contract metadata, etc.). ### `Persistent` - Most expensive fees (same price as `Instance` storage). - Archived when TTL goes to 0, is automatically restored via the `InvokeHostFunction` operation (or restored manually by using the `RestoreFootprintOp` operation in rare cases). - Does not share the same TTL as the contract instance. If the contract instance is not archived, `Persistent` data may be archived and need to be restored before invoking the contract. - Unlimited amount of storage. - Suitable for user data that cannot be `Temporary` (i.e. balances). ## Contract Data Automatic Restoration Starting in Protocol 23 ([CAP-66: Soroban In-Memory Read Resource](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0066.md#automatic-entry-restoration)), archived `Persistent` or `Instance` contract entries can be automatically restored before a host function runs, but only if they're included in the transaction's restore list. In practice, this list is usually populated by the contract invocation simulation (via the Stellar RPC): if the simulation detects an access to an archived entry, it adds that entry to the restore list. When the resulting `InvokeHostFunction` operation executes, those entries are restored first, and then the host function runs; normal rent and fees still apply. :::important If you build a transaction manually without running simulation (and therefore don’t include all needed archived entries in the restore list), automatic restoration will not occur and the transaction will fail when it tries to access those archived entries. ::: ## Contract Data Best Practices As a general rule, `Temporary` storage should only be used for data that can be easily recreated or is only valid for a period of time, whereas `Persistent` or `Instance` storage should be used for data that cannot be recreated and should be kept permanently, such as a user's token balance. Each storage type is in a separate key space. To demonstrate this, see the code snippet below: ```rust const EXAMPLE_KEY: Symbol = symbol_short!("KEY"); env.storage().persistent().set(&EXAMPLE_KEY, 1); env.storage().temporary().set(&EXAMPLE_KEY, 2); env.storage().persistent().get(&EXAMPLE_KEY); // Returns Ok(1) env.storage().temporary().get(&EXAMPLE_KEY); // Returns Ok(2) ``` All `Instance` storage is stored in a single contract instance `LedgerEntry` and shares a single TTL. This means that one call to `env.storage().instance().extend_ttl()` will extend the TTL of all `Instance` entries, as well as the contract instance itself. It will also extend the contract code entry as well, which we'll expand on in the next section. For `Temporary` and `Persistent` storage, each entry has its own TTL and must be extended individually. The interface is slightly different and takes the key of the entry being extended as well as the TTL extension value. A call to `extend_ttl(N)` ensures that the current TTL of the contract instance entry is _at least_ N ledgers. For example, if `extend_ttl(100)` is called and the contract instance entry has a current TTL of 50 ledgers, the TTL will be extended up to 100 ledgers. If `extend_ttl(100)` is called and the contract instance entry has a current TTL of 150 ledgers, the TTL will not be extended and the `extend_ttl()` call is a no-op. In addition to contract-defined TTL extensions using the `extend_ttl()` function, a contract data entry's TTL can be extended via the [`ExtendFootprintTTLOp`](#extendfootprintttlop) operation. ## Contract Code and Contract Instance lifetimes Contract code entries and contract instances have lifetimes, and there are a couple ways to extend them. Methods like `env.storage().instance().extend_ttl()` and `env.deployer().extend_ttl()` extend both the contract code and contract instance. The threshold check and extensions are done independently for both the contract code and contract instance, so it’s possible that one is bumped but not the other depending on what the current TTL’s are. If you would like to extend the contract code or contract instance separately, you can use `env.deployer().extend_ttl_for_code()` and `env.deployer().extend_ttl_for_contract_instance()` respectively. Calling both with the same parameters is equivalent to calling only `env.deployer().extend_ttl()`. ## Behavior of transactions that try to access an archived Persistent entry Here are some important points to keep in mind when it comes to archived entries - 1. A Soroban transaction that has a key to an archived Persistent entry in the footprint, but not in the transaction's restore list, will fail immediately during the apply stage prior to contract execution. It does not matter if the contract itself was going to access the entry. (If the entry _is_ included in the restore list, it's [automatically restored](#contract-data-automatic-restoration) before the host function runs instead.) 2. Due to the previous point that archived entries can never make it into smart contract logic, there is no reason to write code in your contract to handle archived entries. The same applies to contract test cases - while you may want to write tests that check if your extension logic is correct, you don't need to write archival tests because the transaction will fail before getting to the contract. It is possible though to access an archived entry in a Soroban test case, in which case the host will panic. You can read more about this in [Test TTL Extensions](../../../../build/guides/archival/test-ttl-extension.mdx). 3. Archived persistent entries can never be re-created. They must instead be restored. Once they are restored, then they can be modified or deleted. ## Terms and Semantics ### Live Until Ledger Each `ContractData` and `ContractCode` entry has a `liveUntilLedger` field stored in its `LedgerEntry`. The entry is no longer live (i.e. either archived or deleted depending on storage type) when `current_ledger > liveUntilLedger`. ### TTL An entry's Time To Live (TTL) is defined as how many ledgers remain until the entry is no longer live. For example, if the current ledger is 5 and an entry's live until ledger is 15, then the entry's TTL is 10 ledgers. ### Minimum TTL For each entry type, there is a minimum TTL that the entry will have when being created or restored. This TTL minimum is enforced automatically at the protocol level. Minimum TTL is a network parameter. Refer to the [resource reference](https://lab.stellar.org/network-limits) on the Stellar Lab to find the current values. ### Maximum TTL On any given ledger, an entry's TTL can be extended up to the maximum TTL. This is a network parameter (see the [resource limits table](https://lab.stellar.org/network-limits) on the Stellar Lab for the current maximum TTL). Maximum TTL is not enforced based on when an entry was created, but based on the current ledger. For example, if an entry is created on January 1st, 2024, its TTL could initially be extended up to January 1st, 2025. After this initial TTL extension, if the entry received another TTL extension later on January 10th, 2024, the TTL could be extended up to January 10th, 2025. The `max_ttl()` function can be used to determine the current maximum allowed TTL. ## Operations ### ExtendFootprintTTLOp #### Semantics XDR: ``` /* Threshold: low Result: ExtendFootprintTTLResult */ struct ExtendFootprintTTLOp { ExtensionPoint ext; uint32 extendTo; }; ``` `ExtendFootprintTTLOp` is a Soroban operation that will extend the live until ledger of the entries specified in the _read-only set of the footprint_. The read-write set must be empty. The extension will make sure that the entries' TTL will be at least extendTo ledgers from now. Let's look at this example below. ``` Ex. Last closed ledger (LCL) = 5, Current Ledger = 6, liveUntilLedger = 8 entry1.liveUntilLedger = 10 entry2.liveUntilLedger = 14 entry3.liveUntilLedger = 10000 entry1.liveUntilLedger will be updated to 14 so it will live for 8 more ledgers, including the current ledger, and the entry can be accessed in ledgers [6, 13]. entry2 and entry3 will not be updated because they already have an liveUntilLedger that is large enough. ``` #### Transaction resources `ExtendFootprintTTLOp` is a Soroban operation, and therefore must be the only operation in a transaction. The transaction also needs to populate `SorobanTransactionData` transaction extension explained [here](../contract-interactions/stellar-transaction.mdx#transaction-resources). To fill out `SorobanResources`, use the transaction simulation mentioned in the provided link, or make sure `diskReadBytes` covers the serialized size of every entry in the `readOnly` set. ### RestoreFootprintOp `RestoreFootprintOp` is, for the most part, no longer needed starting in Protocol 23, as archived entries included in a transaction's [restore list](#contract-data-automatic-restoration) (usually populated during transaction simulation) are automatically restored when the `InvokeHostFunctionOp` executes. `RestoreFootprintOp` can still be used in rare use cases, such as: - If auto-restoration makes a transaction too large to fit into the network limits, the restoration can be performed separately via `RestoreFootprintOp` - If contract developers want to make sure that they pay the restoration fees (as opposed to their users), they can restore their entries on-demand with `RestoreFootprintOp` XDR: ``` /* Threshold: low Result: RestoreFootprintOp */ struct RestoreFootprintOp { ExtensionPoint ext; }; ``` `RestoreFootprintOp` is a Soroban operation that will restore archived entries specified in the _read-write set of the footprint_ and make them accessible again. The read-only set of the footprint must be empty. An archived entry is one where its liveUntilLedger is less than the current ledger number. **Only persistent and instance entries can be restored.** The restored entry will have its live until ledger extended to the [minimum] the network allows for newly created entries, which is `current_ledger_number + 4095` for persistent entries. The minimum TTL value is a network configuration parameter and is subject to be updated (likely increased) via network upgrades. [minimum]: https://github.com/stellar/stellar-core/blob/2109a168a895349f87b502ae3d182380b378fa47/src/ledger/NetworkConfig.h#L77-L78 #### Transaction resources `RestoreFootprintOp` is a Soroban operation, and therefore must be the only operation in a transaction. The transaction also needs to populate `SorobanTransactionData` transaction extension explained [here](../contract-interactions/stellar-transaction.mdx#transaction-resources). To fill out `SorobanResources`, use the transaction simulation mentioned in the provided link, or make sure `writeBytes` includes the key and entry size of every entry in the `readWrite` set and make sure `extendedMetaDataSizeBytes` is at least double of `writeBytes`. --- ## Examples We've done our best to build tooling around state archival in both the Stellar RPC server as well as the JavaScript SDK to make it easier to deal with, and this set of examples demonstrates how to leverage it. ### Overview :::info The manual operation (`RestoreFootprintOp`) is, for the most part, no longer needed starting in Protocol 23, as archived entries included in a transaction's [restore list](#contract-data-automatic-restoration) (usually populated during transaction simulation) are automatically restored when the `InvokeHostFunctionOp` executes. `RestoreFootprintOp` can be used in rare use cases [described above](#restorefootprintop). ::: Both restoring and extending the TTL of ledger entries follows a three-step process regardless of their nature (contract data, instances, etc.): 1. **Identify the ledger entries**. This usually means acquiring them from a Stellar RPC server as part of your initial transaction simulation (see the [transaction simulation docs](../contract-interactions/transaction-simulation.mdx) and the [`simulateTransaction`](../../../../data/apis/rpc/api-reference/methods/simulateTransaction.mdx) RPC method). 2. **Prepare your operation**. This means describing the ledger entries within the corresponding operation (i.e., `ExtendFootprintTTLOp` or `RestoreFootprintOp`) and its ledger footprint (the `SorobanTransactionData` field), then simulating it to fill out fee and resource usage information (when restoring, you usually have simulation results already). 3. **Submit the transaction** and start again with what you were trying to do in the first place. Each of the examples below will follow a structure like this. We'll work our way through two different scenarios: 1. [a piece of persistent data in my contract is archived](#example-my-data-is-archived) 2. [my contract instance or the WASM is archived](#example-my-contract-is-archived) Remember, though, that any combination of these scenarios can occur in reality. ### Preparation In order to help the scaffolding of the code, we'll reuse the rudimentary, retry-enabled transaction polling function `submitTx` which we outlined in [another guide](../../../../build/guides/transactions/submit-transaction-wait-js.mdx). In the following code, we will also leverage [`Server.prepareTransaction`](https://stellar.github.io/js-stellar-sdk/module-rpc.Server.html#prepareTransaction). This is a helpful method that, given a transaction, will simulate it, then amend the transaction with the simulation results (fees, etc.) and return that. Then, it can just be signed and submitted. We will also use [`SorobanDataBuilder`](https://stellar.github.io/js-stellar-sdk/SorobanDataBuilder.html), a convenient abstraction that lets us use a [builder pattern](https://en.wikipedia.org/wiki/Builder_pattern) to set the appropriate storage footprints for a transaction. ### Example: My data is archived! We'll start with the likeliest occurrence: my piece of persistent data is archived because I haven't interacted with my contract in a while. How do I make it accessible again? In this example, we will assume two things: the contract itself is still live (i.e. others have been extending its TTL while you've been away) and you don't know how your archived data is represented on the ledger. If you did, you could skip the steps below where we figure that out and just set up the restoration footprint directly. The process involves three discrete steps: 1. Simulate our transaction as we normally would. 2. If the simulation indicated it, we perform restoration via [`Operation.restoreFootprint`](https://stellar.github.io/js-stellar-sdk/Operation.html#.restoreFootprint) using its hints. 3. We retry running our initial transaction. Let's see that in code: ```typescript BASE_FEE, Networks, Keypair, TransactionBuilder, SorobanDataBuilder, rpc as StellarRpc, xdr, } from "@stellar/stellar-sdk"; // add'l imports to preamble const { Api, assembleTransaction } = StellarRpc; // assume that `server` is the Server() instance from the preamble async function submitOrRestoreAndRetry( signer: Keypair, tx: Transaction, ): Promise { // We can't use `prepareTransaction` here because we want to do // restoration if necessary, basically assembling the simulation ourselves. const sim = await server.simulateTransaction(tx); // Other failures are out of scope of this tutorial. if (!Api.isSimulationSuccess(sim)) { throw sim; } // If simulation didn't fail, we don't need to restore anything! Just send it. if (!Api.isSimulationRestore(sim)) { const prepTx = assembleTransaction(tx, sim); prepTx.sign(signer); return submitTx(prepTx); } // // Build the restoration operation using the RPC server's hints. // const account = await server.getAccount(signer.publicKey()); let fee = parseInt(BASE_FEE); fee += parseInt(sim.restorePreamble.minResourceFee); const restoreTx = new TransactionBuilder(account, { fee: fee.toString() }) .setNetworkPassphrase(Networks.TESTNET) .setSorobanData(sim.restorePreamble.transactionData.build()) .addOperation(Operation.restoreFootprint({})) .build(); restoreTx.sign(signer); const resp = await submitTx(restoreTx); if (resp.status !== Api.GetTransactionStatus.SUCCESS) { throw resp; } // // now that we've restored the necessary data, we can retry our tx using // the initial data from the simulation (which, hopefully, is still // up-to-date) // const retryTxBuilder = TransactionBuilder.cloneFrom(tx, { fee: (parseInt(tx.fee) + parseInt(sim.minResourceFee)).toString(), sorobanData: sim.transactionData.build(), }); // because we consumed a sequence number when restoring, we need to make sure // we set the correct value on this copy retryTxBuilder.source.incrementSequenceNumber(); const retryTx = retryTxBuilder.build(); retryTx.sign(signer); return submitTx(retryTx); } ``` Notice that when restoration is required, **simulation still succeeds**. The way that we know that something needs to be restored is the presence of a `restorePreamble` structure in the RPC's response. This contains both the footprint and fee needed for restoration, while the rest of the response contains the invocation simulation **as if** that restoration was done first. This is great, as it means fewer round-trips to get going again! ### Example: My contract is archived! As you can imagine, if your deployed contract instance or the code that backs it is archived, it can't be loaded to execute your invocations. Remember, there's a distinct, one-to-many relationship on the chain between a contract's code and deployed instances of that contract: ```mermaid flowchart LR A[my instance] & B[your instance]--> C[contract WASM] ``` We need **both** to be live for our contract calls to work. Let's work through how these can be recovered. The recovery process is slightly different: while we don't need simulation to figure out the footprints, we do need to do an additional ledger entry fetch. We can leverage [`Contract.getFootprint()`](https://stellar.github.io/js-stellar-sdk/Contract.html#getFootprint) to get the ledger key used by a given contract instance, but that won't give us its backing WASM code. For that, we'll recreate [this example](../../../../data/apis/rpc/api-reference/methods/getLedgerEntries.mdx#requesting-a-contracts-wasm-code) here. We also need simulation to figure out the fees for our restoration. This, however, can be easily covered by the SDK's [`Server.prepareTransaction`](https://stellar.github.io/js-stellar-sdk/module-rpc.Server.html#prepareTransaction) helper, which will do simulation and assembly for us: ```typescript BASE_FEE, Contract, Keypair, Networks, TransactionBuilder, SorobanDataBuilder, Operation, rpc as StellarRpc, } from "@stellar/stellar-sdk"; async function restoreContract( signer: Keypair, c: Contract, ): Promise { const instance = c.getFootprint(); const account = await server.getAccount(signer.publicKey()); const wasmEntry = await server.getLedgerEntries( getWasmLedgerKey(instance) ); const restoreTx = new TransactionBuilder(account, { fee: BASE_FEE }) .setNetworkPassphrase(Networks.TESTNET) .setSorobanData( // Set the restoration footprint (remember, it should be in the // read-write part!) new SorobanDataBuilder().setReadWrite([ instance, wasmEntry ]).build(), ) .addOperation(Operation.restoreFootprint({})) .build(); const preppedTx = await server.prepareTransaction(restoreTx); preppedTx.sign(signer); return submitTx(preppedTx); } function getWasmLedgerKey(entry: xdr.ContractDataEntry): { return xdr.LedgerKey.contractCode( new xdr.LedgerKeyContractCode({ hash: entry.val().instance().wasmHash() }) ); } ``` The nice part about this approach is that it will restore both the instance and the backing WASM code if necessary, skipping either if they're already in the ledger state. [`restorefootprintop`]: #RestoreFootprintOp [`extendfootprintttlop`]: #ExtendFootprintTTLOp --- ## Types Learn about the types and data structures available to developers in Soroban smart contracts. --- ## Built-In Types Built-in types used as smart contract inputs and outputs. Built-in types are available to all contracts for use as contract function inputs and outputs, and are defined by the [environment] and the [Rust SDK]. [environment]: ../environment-concepts.mdx [rust sdk]: ../../../../tools/sdks/contract-sdks.mdx#soroban-rust-sdk :::tip Custom types like structs, enums, and unions are also supported. See Custom Types. ::: ## Primitive Types The following primitive types are supported: ### Unsigned 32-bit Integer (`u32`) ### Signed 32-bit Integer (`i32`) ### Unsigned 64-bit Integer (`u64`) ### Signed 64-bit Integer (`i64`) ### Unsigned 128-bit Integer (`u128`) ### Signed 128-bit Integer (`i128`) ### Bool (`bool`) ## Symbol (`Symbol`) Symbols are small efficient strings up to 32 characters in length and limited to `a-z` `A-Z` `0-9` `_` that are encoded into 64-bit integers. Symbols are primarily used for function names and other identifiers that are exported in the public API of a contract. They can also be used wherever short strings are needed to keep resource costs down. ## Bytes, Strings (`Bytes`, `BytesN`, `String`) Byte arrays and strings can be passed to contracts and stores using the `Bytes` type. For byte arrays of fixed length, `BytesN` can be used. For example, contract IDs are fixed 32-byte byte arrays, and are represented as `BytesN<32>`. Note that the bytes contained in `String`s do not necessarily conform to any standard text encoding such as ASCII or Unicode UTF-8. They are plain uninterpreted bytes, and users expecting a particular encoding need to enforce that encoding manually. ## Vec (`Vec`) Vec is a sequential and indexable growable collection type. Values are stored in the environment and are available to contract through the functions defined on Vec. Values stored in the Vec are transmitted to the environment as RawVals, and when retrieved from the Vec are transmitted back and converted from RawVal back into their type. The values in a Vec are not guaranteed to be of any specific type and conversion will fail if they are not of the expected type. Most functions on Vec return a Result due to this. ## Map (`Map`) Map is a ordered key-value dictionary. The map is ordered by its keys. Iterating a map is stable and always returns the keys and values in order of the keys. The map is stored in the Host and available to the Guest through the functions defined on Map. Values stored in the Map are transmitted to the Host as RawVals, and when retrieved from the Map are transmitted back and converted from RawVal back into their type. The keys and values in a Map are not guaranteed to be of type K/V and conversion will fail if they are not. Most functions on Map return a Result due to this. Maps have at most one entry per key. Setting a value for a key in the map that already has a value for that key replaces the value. ## Address (`Address`) Address is a universal opaque identifier to use in contracts. It may represent a 'classic' Stellar account, a custom account implemented in Soroban or just an arbitrary contract. Address can be used as a contract function input argument (for example, to identify the payment recipient), as a data key (for example, to store the balance), as the authentication & authorization source (for example, to authorize a token transfer) etc. See [authorization documentation](../authorization.mdx) for more details on how to use the `Address` type. ## Option (`Option`) Option represents an optional value. The Option type is used for values that may, or may not, be present. The Option type is an enum and can either be `Some` (some value exists) or `None` (no value exits). While Option acts like Rust's `Option`, the Option type is not explicitly represented in XDR. The Option type is represented in the XDR by the `ScVal` `ScVoid` value when no value exists (None), any by any other `ScVal` value when the value exists (Some). --- ## Custom Types(Types) Struct, union, and enum types defined by contracts. Custom types are struct, union, and enum types defined by contracts. They are usable everywhere primitives types can be used: as contract inputs, outputs, or for storage. :::info The [custom types example] demonstrates how to define your own types. [custom types example]: ../../../../build/smart-contracts/example-contracts/custom-types.mdx ::: :::info Error enum types are another type contracts can define that have some unique behaviors. See [Errors](../errors-and-debugging/errors.mdx) for more information. ::: ## Structs (with Named Fields) Structs with named fields are stored on ledger as a map of key-value pairs, where the key is a 32 character string representing the field name, and the value is the value encoded. Field names must be no more than 32 characters. Fields with names no longer than 9 characters are slightly more efficient at runtime, though the difference should be marginal most of the time. ```rust #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct State { pub count: u32, pub last_incr: u32, } ``` When converted to XDR, the value becomes an `ScVal`, containing an `ScMap`, containing an array of key-value pairs. ```json { "map": [ { "key": { "symbol": "count" }, "val": { "u32": 0 } }, { "key": { "symbol": "last_incr" }, "val": { "u32": 0 } } ] } ``` ## Structs (with Unnamed Fields) Structs with unnamed fields are stored on ledger as a vector of values, and are interchangeable with tuples and vectors. The elements are placed in the vector in order that they appear in the field list. ```rust #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct State(pub u32, pub u32); ``` When converted to XDR, the value becomes an `ScVal`, containing an `ScVec`, containing an array of values. ```json { "vec": [{ "u32": 0 }, { "u32": 0 }] } ``` ## Enum (Unit and Tuple Variants) Enums containing unit and tuple variants are are stored on ledger as a two element vector, where the first element is the name of the enum variant as a string up to 32 characters in length, and the value is the value if the variant has one. Only unit variants and tuple variants, like `A` and `B` below, are supported. ```rust #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub enum Enum { A, B(u32), } ``` When a unit variant, such as `Enum::A`, is converted to XDR, the value becomes an `ScVal`, containing an `ScVec`, containing an array with a single value, the symbol containing the variant name. ```json { "vec": [{ "symbol": "A" }] } ``` When a tuple variant, such as `Enum::B`, is converted to XDR, the value becomes an `ScVal`, containing an `ScVec`, containing an array with two values, the symbol containing the variant name and the tuple value. ```json { "vec": [{ "symbol": "B" }, { "u32": 0 }] } ``` When tuple variants containing multiple values are implemented, the values will be included into the vector. ## Enum (Integer Variants) Enums containing integer values are stored on ledger as the `u32` value. ```rust #[contracttype] #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(u32)] pub enum Enum { A = 1, B = 2, } ``` When converted to XDR, the value becomes an `ScVal`, containing a `U32`. ```json { "u32": 1 } ``` --- ## Fully-Typed Contracts {`Smart contract WASM files contain a machine-readable description of the interface type.`} When you compile a contract created with [soroban-sdk](../../../../tools/sdks/contract-sdks.mdx#soroban-rust-sdk), the Wasm file ends up with a [custom section](https://webassembly.github.io/spec/core/appendix/custom.html) containing a machine-readable description of your contract's interface types, sometimes called its [spec](https://github.com/stellar/rs-soroban-sdk/tree/main/soroban-spec) or its [API](https://github.com/stellar/soroban-docs/pull/381#issuecomment-1507283476). This is similar to [ABIs](https://www.quicknode.com/guides/ethereum-development/smart-contracts/what-is-an-abi) in Ethereum, except that Soroban will store every single one of them on-chain from day one, and they include comments from the contract's author. These interface types are formatted using [XDR](../../../fundamentals/data-format/xdr.mdx), a data format used widely throughout Stellar. It can be tricky to create or consume XDR manually, but tooling can fetch these interface types to make your life easier. [Stellar CLI](../../../../tools/cli/README.mdx#cli) and [Stellar SDK](../../../../tools/sdks/client-sdks.mdx#javascript-sdk) are two such tools to do so. Let's look at each. ## Stellar CLI: `stellar contract invoke` Really, every smart contract is its own program, and deserves its own CLI. So that's what Stellar CLI gives you. A unique CLI for each smart contract. Constructed on-the-fly, right from the on-chain interface types. Including the author's comments. An _implicit CLI_. For example, calling the native asset contract on the Test network: ```bash $ stellar contract invoke --network testnet --id CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC -- --help Usage: CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC [COMMAND] Commands: balance Returns the balance of `id`. # Arguments * `id` - The address for which a balance is being queried. If the address has no existing balance, returns 0. ... transfer Transfer `amount` from `from` to `to`. # Arguments * `from` - The address holding the balance of tokens which will be withdrawn from. * `to` - The address which will receive the transferred tokens. * `amount` - The amount of tokens to be transferred. # Events Emits an event with topics `["transfer", from: Address, to: Address], data = amount: i128` ... Options: -h, --help Print help ``` Like any other CLI, you can also get help for any of these subcommands. Omitting everything before the `--` in the previous command, this would look like: `… -- balance --help`. Stellar CLI again fetches the on-chain interface types, this time using it to generate a full list of all arguments to the function, and even generates examples. :::tip If you're unfamiliar with the `--` double dash separator, this is a pattern used by other CLIs. Everything after the double dash, sometimes called the [slop](https://github.com/clap-rs/clap/issues/971), gets passed to the child process. An example of other CLIs that make use of this are `npm run` and `cargo run`. ::: ## Stellar JS SDK: `contract.Client` To create a contract client for the same contract as shown for `contract invoke` above, you would use this JavaScript: ```js const xlm = contract.Client.from({ contractId: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", networkPassphrase: "Test SDF Network ; September 2015", rpcUrl: "https://soroban-testnet.stellar.org", }); ``` Like the CLI, this will fetch the contract from the live network and parse the contract's types/spec. It will auto-generate a class that allows you to ergonomically call the contract's methods: ```js xlm.balance({ id: "G123…" }); ``` Note that everything shown above works dynamically from a browser. However, sometimes it's nice to also have this behavior available as a library, while building apps. And in this case, it's really nice to have TypeScript, which enables type-ahead for all methods in the contract, and which can include the comments from the contract's original author. That's what the CLI's `contract bindings typescript` command is for: ```bash stellar contract bindings typescript \ --network testnet \ --output-dir xlm --overwrite \ --id CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC ``` This creates a fully-typed NPM module for the given contract. ## Already the best; just getting started We love that Soroban had all contract interface types available on-chain right from day one. No secondary API calls to external services, no secondary API token management, no signing in or creating an account anywhere else, and near-perfect reliability. It's a game-changer within the blockchain space. Stellar CLI and JS SDK already show how this can be built into foundational tooling to give developers delightful experiences. And this is only the beginning. At every level of the stack, you can expect—and [build](https://stellar.org/foundation/grants-and-funding)—tooling that makes interacting with any contract predictable and seamless. Soon we'll have GUIs that adapt to any given contract on-the-fly, functioning as interactive documentation. If you're writing contracts that make cross-contract calls, most of the code you need can also be auto-generated. And that's still all just the beginning. With those as the foundations, what else can we build? What else can you imagine? What is the future of components, now that one small program, aka smart contract, can be easily interacted with at all layers of the software stack? When considered in combination with Wasm—on-chain, in-browser, and [elsewhere](https://en.wikipedia.org/wiki/WebAssembly#Implementations)—what possibilities for interop can we imagine and co-create? We can't wait to see what you build. --- ## Data Format Learn how Stellar represents and exchanges data. --- ## XDR-JSON The XDR-JSON schema is defined by the [stellar-xdr crate](https://docs.rs/stellar-xdr) and provides a round-trippable means for converting Stellar [XDR] values to JSON and converting that JSON back to the identical XDR. Converting with the schema is also exposed by the following tools and libraries. The JSON-Schema for the XDR-JSON format can be found using the [`stellar xdr types schema`] command. ## Tools - [Stellar CLI > `stellar xdr`](../../../tools/cli/stellar-cli.mdx#stellar-xdr) - [Lab > View XDR](../../../tools/lab/README.mdx) ## Libraries - [stellar-xdr](https://docs.rs/stellar-xdr) rust crate - [@stellar/stellar-xdr-json](https://www.npmjs.com/package/@stellar/stellar-xdr-json) npm package - [github.com/stellar/go-stellar-xdr-json](https://github.com/stellar/go-stellar-xdr-json) go package ## Key Characteristics of the JSON and XDR Conversion Schema - **Round-Trippable:** The JSON format allows for converting from XDR to JSON and back to XDR without loss of information. - **Self-Describing:** The JSON format describes the internals of the type but does not identify the type that is encoded. This is similar to XDR, which also does not identify the encoded type. - **64-bit Integers:** The JSON format includes integers up to 64-bit in size. JavaScript runtimes do not support 64-bit integers, so a custom decoder must be used, such as [lossless-json](https://www.npmjs.com/package/lossless-json). - **Escaped ASCII Strings:** The JSON format includes strings that are UTF-8 safe, escaped ASCII strings. This is because XDR strings are not UTF-8 encoded, but are instead byte streams that can contain any values. Non-ASCII characters are escaped. :::info[backwards-incompatible] The defined schema (linked above) is **not** backwards compatible between a given protocol version and prior versions. The best practice is to store required ledger data in XDR format, not in JSON format. ::: [XDR]: xdr [`stellar xdr types schema`]: ../../../tools/cli/stellar-cli.mdx#stellar-xdr-types-schema --- ## XDR(Data-format) Stellar stores and communicates ledger data, transactions, results, history, and messages in a binary format called External Data Representation (XDR). XDR is defined in [RFC4506]. XDR is optimized for network performance but is not human-readable. The Stellar SDKs convert XDRs into friendlier formats. ## .X files Data structures in XDR are specified in `.x` files. These files _only_ contain data structure definitions, no operations or executable code. The `.x` files for the XDR structures used on the Stellar Network are available on [GitHub](https://github.com/stellar/stellar-xdr). :::tip Stellar XDR is encodable into JSON using the [XDR-JSON] schema. ::: ## More About XDR XDR is similar to tools like Protocol Buffers or Thrift. XDR provides a few important features: - It is very compact, so it can be transmitted quickly and stored with minimal disk space. - Data encoded in XDR is reliably and predictably stored. Fields are always in the same order, which makes cryptographically signing and verifying XDR messages simple. - XDR definitions include rich descriptions of data types and structures, which is not possible in simpler formats like JSON, TOML, or YAML. ## Parsing XDR Since XDR is a binary format and not as widely known as simpler formats like JSON, the Stellar SDKs all include tools for parsing XDR and will do so automatically when retrieving data. The XDR data is still included (encoded as a base64 string) inside the JSON in case you need direct access to it. ## Digging into XDR structures Since the XDR format is a fundamental underpinning of the Stellar Network, we often find it necessary to dig into these raw binary data structures for clients to interact with things like transactions and authorization entries. It can be hard to understand because of its structure, but this section will aim to enlighten you on the ways to interact with it in a handful of popular languages. The Protocol's schema is defined in the [`stellar/stellar-xdr`](https://github.com/stellar/stellar-xdr) repository, though that contains far more than what we'll need for our purposes. ### XDR's Common Forms In the Stellar Protocol, XDR takes a handful of different forms that are important to understand and distinguish: #### Basic Primitives These are the easiest to understand for anyone familiar with a programming language and includes things like integers, strings, and arrays of bytes. These are fairly straightforward to interact with, though things can get more complicated when you have aliases. For example, you have the `Uint64` alias in the Stellar Protocol (defined [Stellar-types.x](https://github.com/stellar/stellar-xdr/blob/70180d5e8d9caee9e8645ed8a38c36a8cf403cd9/Stellar-types.x#L14)), defined for readability instead of XDR's native "unsigned hyper" value. You can treat it exactly as you'd expect: ```typescript const u64 = new xdr.Uint64(12345678); ``` Some language variations will let you instantiate it in a number of ways. For example, since 64-bit integers are a little... [wonky](https://stackoverflow.com/a/9643650) in JavaScript, you can also initialize it with a [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) or even a string type: ```typescript let u64 = new xdr.Uint64("1_000_000_000_000_000"); u64 = new xdr.Uint64(1_000_000_000_000_000n); ``` You can see the full set of options to construct a `Uint64` in the definition for `UnsignedHyper` in the TypeScript definition here: [curr.d.ts](https://github.com/stellar/js-stellar-base/blob/master/types/curr.d.ts#L127-L136). In fact, this file should be your primary guide to navigating the XDR if you're using JavaScript. Other languages, such as Python (see `stellar-sdk`'s [uint64.html](https://stellar-sdk.readthedocs.io/en/stable/_modules/stellar_sdk/xdr/uint64.html) documentation), support arbitrarily-sized integers by default, so there are no such variations: ```python from stellar_sdk import xdr u64 = xdr.Uint64(1_000_000_000_000_000) ``` As a result, many of the basic primitives can be initialized in the "intuitive" way for your respective language. #### Unions This is where things get a little more complicated. A _union type_ is a generic type that switches between different "arms", each of which is a different internal type. When parsing an XDR union type, it's important to determine which arm is being used before you try parsing what's inside. Let's take an [`SCAddress`](https://github.com/stellar/stellar-xdr/blob/70180d5e8d9caee9e8645ed8a38c36a8cf403cd9/Stellar-contract.x#L185-L191), for example. From the type definition, we can see that it's either an account (i.e. `SC_ADDRESS_TYPE_ACCOUNT`) or the hash of a contract (`SC_ADDRESS_TYPE_CONTRACT`). Right above that definition, you can see that these are constants of the `SCAddressType` enum defined as 0 or 1, respectively. In general, unions follow a particular pattern: switch on the type and match the appropriate enum value. So when you have one of these unions, you need to figure out which one it is: ```typescript // suppose `scAddr` is an instance of `xdr.ScAddress` we're parsing switch (scAddr.switch()) { case xdr.ScAddressType.scAddressTypeAccount().value: console.log("The account is:", scAddr.accountId()); break; case xdr.ScAddressType.scAddressTypeContract().value: console.log("The contract is:", scAddr.contractId()); break; default: throw new Error(`Unexpected address type: ${scAddr.switch()}`); } ``` ```python if scAddr.type == SCAddressType.SC_ADDRESS_TYPE_ACCOUNT: print("The account is:", scAddr.account_id); break elif scAddr.type == SCAddressType.SC_ADDRESS_TYPE_CONTRACT: print("The contract is:", scAddr.contract_id); break else: raise Exception(f"Unexpected address type: {scAddr.type}") ``` You can see the references for these in the documentation ([Python](https://stellar-sdk.readthedocs.io/en/stable/api.html#scaddress) and [JavaScript](https://github.com/stellar/js-stellar-base/blob/master/types/curr.d.ts#L13881-L13886), respectively). It's worth noting that you _could_ just use the defined enum constants directly, e.g. ```typescript // suppose `scAddr` is an instance of `xdr.ScAddress` we're parsing switch (scAddr.switch().value) { case 0: console.log("The account is:", scAddr.accountId()); break; case 1: console.log("The contract is:", scAddr.contractId()); break; default: throw new Error(`Unexpected address type: ${scAddr.switch()}`); } ``` However, this is generally more error-prone and harder to read. You can also refer to them by their _name_ (`.name` in JS and `str(...)` in Python): this is marginally better for readability but worse for performance since you're doing string comparisons. It's very useful for logs, user-facing rendering, or other debugging, though. Unions appear in _many_ places throughout the XDR and they can be just one arm or a whole _bunch_ of arms. For example, the **s**mart **c**ontract **val**ue `ScVal` has 22 possible values since it's the universal representation for a value inside of a smart contract. You can see all of them being inspected in the JavaScript utility [`scValToNative`](https://stellar.github.io/js-stellar-sdk/js-stellar-base_src_scval.js.html#line279). Remember, XDR is the lowest level of communication on the Stellar Network. Generally speaking, there are high-level abstractions that should help with these structures. For example, the above parsing routine is exactly the purpose of the [`Address.fromScAddress`](https://stellar.github.io/js-stellar-sdk/Address.html#.fromScAddress) and [`Address.from_xdr_sc_address`](https://stellar-sdk.readthedocs.io/en/stable/api.html#stellar_sdk.address.Address) abstractions in the JavaScript and Python SDKs, respectively. This greatly simplifies your code: ```typescript // suppose `scAddr` is an instance of `xdr.ScAddress` we're parsing const address = Address.fromScAddress(scAddr); ``` ```python # suppose `scAddr` is an instance of `xdr.ScAddress` we're parsing addr = Address.from_xdr_sc_address(scAddr) ``` Always try to find a higher-level abstraction before digging into the raw structure. #### Structures Like in any C-like language, a structure is a packed object containing a bunch of fields of arbitrary types. Structures can themselves contain structures, so be sure you traverse the entire tree of fields when you're constructing one. Let's take, for example, the [`ContractEvent`](https://github.com/stellar/stellar-xdr/blob/c1a58e2f81a4d1b02adc6db9914e6f51960e1ce3/Stellar-ledger.x#L339-L358) structure. Its definition is a little complicated, so let's break it down: - An `ExtensionPoint` is a way for the protocol to be extended while maintaining backwards binary compatibility. It's basically an empty slot into which you can add variations in the future. The [`SorobanTransactionMeta` structure](https://github.com/stellar/stellar-xdr/blob/c1a58e2f81a4d1b02adc6db9914e6f51960e1ce3/Stellar-ledger.x#L410) is a great example of this in action: When the structure was initially added to the protocol, it just contained the details of an invocation (events and a return value). Later, we needed more details about the invocation in the resulting metadata, so `SorobanTransactionMetaExt` was injected into the `ExtensionPoint` with a [`V1` variant](https://github.com/stellar/stellar-xdr/blob/c1a58e2f81a4d1b02adc6db9914e6f51960e1ce3/Stellar-ledger.x#L368C8-L368C35) containing metadata about fees charged as a result of the invocation. - The `contractID` should be self-explanatory: it's the hash of the contract ID that caused this event. It's a pointer, though, because not all events are related to a specific contract. Some may come from the system, diagnostics, etc. It should not be surprising that the field is stored as a `Hash`: even though at the ecosystem layer we refer to contracts by their "strkey" or string representation (in the form `C...`), these are actually "friendlier" representations of a raw SHA-256 hash. This is common at the protocol layer: the simplest, most compact representation will always be used. There are generally SDK tools to make translating these into more user-friendly types (as demonstrated in the example below). - The `ContractType` is essentially a way to differentiate which of the fields in the structure you should expect to be present and isn't worth elaborating on. - Finally, we have the curious `v` union field. You will often see unions in structures and vice-versa. If you have a handle on how each of these is traversed individually, you can handle them interleaved amongst each other. As a demonstration of how this is done with an SDK, even catered specifically to this structure, we can take a look at the source of the [`humanizeEvents` helper method](https://stellar.github.io/js-stellar-sdk/global.html#humanizeEvents) in the JavaScript SDK. Specifically, we'll look at the details of the [`extractEvent` subroutine](https://stellar.github.io/js-stellar-sdk/js-stellar-base_src_events.js.html), replicated here for convenience: ```js function extractEvent(event) { return { ...(typeof event.contractId === "function" && event.contractId() != null && { contractId: StrKey.encodeContract(event.contractId()), }), type: event.type().name, topics: event .body() .value() .topics() .map((t) => scValToNative(t)), data: scValToNative(event.body().value().data()), }; } ``` In this short and sweet example we can see tons of the nuances we've discussed in this guide at work: - By differentiating on whether or not the `contractId` returns a value or not (it's a pointer, remember?), we decide whether or not we want to turn the raw hash into an ecosystem-compatible contract ID. - We access the `.name` field of the event type, which turns it into a human-readable string rather than an opaque structure, just like we discussed in [Unions](#unions), earlier. - We inspect _deeply_ into the `body` member to extract the `topics` and `data` fields separately, converting each of those opaque `ScVal`ues into JavaScript-friendly equivalents. In the last part, we "abuse" the fact that there is only one variation of the `body` union we discussed: there is only the initial version (an arm with `v == 0`), so we can use the `.value()` accessor to give us the underlying value directly rather than going through the rigamarole of differentiating the different cases. Calling `.v0()` would have been equivalent, but the benefit here is that if a `v == 1` variation is introduced that _also_ has a `topics` array and a `data` field (i.e. it adds additional fields rather than changing the v0 structure), this code will continue to work! There are many cases in which the different union arms share structure, and `.value()` lets you take advantage of that. ## Record Marking / Frames in Streams In some applications like stellar-core, when multiple XDR objects are stored sequentially in a file or stream, each object is framed using the Record Marking Standard defined in [RFC 5531 Section 11](https://www.rfc-editor.org/rfc/rfc5531#section-11). Each record is composed of one or more fragments. Each fragment begins with a 4-byte header followed by the fragment data: - **Bit 31 (high bit)**: Set to `1` if this is the last fragment of the record, `0` if more fragments follow. - **Bits 0-30**: The length in bytes of the fragment data that follows the header. The header is encoded as a 4-byte big-endian unsigned integer. Note that this header is _not_ itself in XDR standard form. In Stellar's usage today each record contains exactly one XDR object, encoded as a single fragment with the last-fragment bit always set. This framing is used in: - **History archives**: Bucket files and checkpoint ledger files written and read by stellar-core. - **Streaming LedgerCloseMeta**: The LedgerCloseMeta stream output by stellar-core. When decoding XDR with the [stellar-cli](../../../tools/cli/stellar-cli.mdx), streams framed with the record marking can be decoded by specifying the `--input stream-framed`. --- This overview should give you a strong baseline on understanding how to inspect XDR in your respective SDK to dig into the fields that you're interested in. Specifics will, of course, be language dependent, but if you start at the foundation--that being the raw [.x files](#.x-files) themselves--you will get an understanding of the structure itself and should be able to access that same structure directly in your language of choice, as we've outlined here. {/* TODO: Room to expand much more: Tools -> CLI Visualization, JSON Visualization, Stellar Lab */} [RFC4506]: http://tools.ietf.org/html/rfc4506.html [XDR-JSON]: ./xdr-json.mdx --- ## Understanding Fees, Resource Limits, and Metering for Transactions # Fees, Resource Limits, and Metering ## Fees overview Stellar requires a fee for all transactions to make it to the ledger. This helps prevent spam and prioritizes transactions during traffic surges. All fees are paid using the native Stellar token, the [lumen (or XLM)](./lumens.mdx). There are two types of fees on Stellar: **Resource fee: _only applies to smart contract transactions_**. The amount the submitter must pay for their transaction to execute. This amount is based on a transaction’s resource consumption and the state of network storage (described in the [Storage Dynamic Pricing section](#dynamic-pricing-for-storage)). Read about resource fees [below](#resource-fee). **Inclusion fee:** the maximum amount the submitter is willing to pay for the transaction to be included in the ledger. Read more [below](#inclusion-fee). When competing for space on the ledger, smart contract transactions are _only_ competing with other smart contract transactions, and transactions that do not execute a smart contract are _only_ competing with other transactions that do not execute a smart contract. The lumens collected from transaction fees go into a locked account and are not given to or used by anyone. ## Resource fee All smart contract transactions require a resource fee in addition to an inclusion fee: `Transaction Fee (Tx.fee) = Resource Fee (sorobanData.resourceFee) + Inclusion Fee` Transactions that do not execute a smart contract can be thought of as having a resource fee of zero (`resourceFee == 0`). Conversely, for smart contract transactions, you can subtract the resource fee from the total transaction fee to derive the equivalent classic transaction fee component, which is the inclusion fee. ![Soroban Fees](/assets/diagrams/soroban_fees.png) _\* Diagram: Solid line boxes are what is actually present in the transaction, while dotted lines are derivable._ Smart contracts on Stellar use a multidimensional resource fee model that charges fees for several resource types using [network-defined rates](https://lab.stellar.org/network-limits) on the Stellar Lab. The resource fee is calculated based on the resource consumption declared in the transaction and can fluctuate based on a mutable storage write fee (more on that in the [Storage Dynamic Pricing section](#dynamic-pricing-for-storage) below). If the transaction attempts to exceed the declared resource limits, it will fail. If the transaction uses fewer resources than declared, there will be no refunds [(with a couple of exceptions)](#refundable-and-non-refundable-resource-fees). The resource fee depends on the following resources: - **Instructions:** the number of CPU instructions the transaction uses, metered by the host environment; - **Ledger entry accesses:** reading or writing any single ledger entry (any storage key in the contract context); - **Ledger I/O:** the number of bytes read from or written to the ledger; - **Transaction size:** the size of the transaction submitted to the network in bytes; - **Events & return value size:** the size of the events produced by the contract and the return value of the top-level contract function — both events and return value are included in transaction metadata; - **Ledger space rent:** the payment for the ledger entry TTL extensions (i.e., rent payments) and rent payments for increasing ledger entry size. Refer to the [state archival](../fundamentals/contract-development/storage/state-archival.mdx) section for more information about smart contract rent. :::note Some parameters may contribute to multiple fee components. For example, the transaction size is charged for network propagation (as network bandwidth is limited) and for historical storage (as storing ledger history is not free). ::: The implementation details for fee computation are provided by the following [library](https://github.com/stellar/rs-soroban-env/blob/main/soroban-env-host/src/fees.rs). This library is used by the protocol to compute the fees and thus can be considered canonical. The resource fee rates may be updated based on consensus from the network validators. Find current resource fees in the [Resource Limits & Fees](https://lab.stellar.org/network-limits) on the Stellar Lab. For help in analyzing smart contract cost and efficiency, see this [How-To Guide](../../build/guides/fees/analyzing-smart-contract-cost.mdx). ### Refundable and non-refundable resource fees The resource fee is calculated with a non-refundable fees portion and a refundable fees portion: `ResourceFee(sorobanData.resourceFee) = Non-refundable resource fee + Refundable resource fees`. **Non-refundable fees:** calculated from CPU instructions, read bytes, write bytes, and bandwidth (transaction size, including its signatures). **Refundable fees:** calculated from rent, events, and return value. These fees are enforced in two distinct phases. Before execution, the declared `sorobanData.resourceFee` must be at least the fee computed from the transaction's _declared_ resources, or the transaction is rejected as invalid (`txSOROBAN_INVALID`). The refundable portion is then charged from the source account and, during execution, reconciled against actual usage (rent, events, and return value); if the refundable budget is not enough for the resources actually consumed, the transaction fails at apply time and any unused portion is refunded. ### Find a transaction’s resource fee The best way to find the required resource fee for any smart contract transaction is to use the [`simulateTransaction` endpoint](../fundamentals/contract-development/contract-interactions/transaction-simulation.mdx) from the RPC, which enables you to dry run the execution of a transaction to compute the necessary resource values and fees. ### Resource limitations :::note Only smart contract transactions are subject to resource limitations. ::: Stellar’s ledger close time is constrained to a few seconds, preventing the execution of arbitrarily large transactions, regardless of the resource fees involved. All resources mentioned in the prior section are subject to a per-transaction limit. A transaction’s memory (RAM) is also capped, though not subject to any charge. Resource limits are determined by a validator vote and can be adjusted based on network usage and ecosystem needs with a validator consensus. Find current resource limits in the [Resource Limits & Fees](https://lab.stellar.org/network-limits) on the Stellar Lab. ## Inclusion fee The inclusion fee is the maximum bid (a bid denotes a dynamic fee, meaning it varies based on certain network conditions) the submitter is willing to pay for the transaction to be included in the ledger. The inclusion fee equals the number of operations in the transaction multiplied by the effective base fee for the given ledger: `inclusion fee = # of operations * effective base fee` **Effective base fee:** the fee required per operation for a transaction to make it to the ledger. This cannot be lower than 100 stroops per operation (the network minimum). **Stroop:** the smallest unit of a lumen, one ten-millionth of a lumen (.0000001 XLM). :::note Transactions can have up to 100 operations per transaction except for transactions that execute a smart contract. Smart contract transactions are only allowed one operation per transaction (unless the transaction is getting [fee-bumped](../../build/guides/transactions/fee-bump-transactions.mdx); this would add another operation), and the limits are instead specified in CPU instructions and other resource limits. ::: When you set a base fee for a transaction, you are specifying the maximum amount you are willing to pay per operation in that transaction. This doesn’t necessarily mean you’ll pay that amount. You’ll only be charged the lowest amount needed for your transaction to make it to the ledger. If network traffic is light and the number of submitted operations or transactions is below the network ledger limit (configured by validators: on Mainnet, as of July 2026, 1,000 non-smart-contract operations and 2,000 smart contract transactions — other networks differ; see current values on Stellar Lab's [Network Limits](https://lab.stellar.org/network-limits) page), you will only pay the network minimum (configured by validators, currently 100 stroops). Alternatively, your transaction may not make it to the ledger if the effective base fee is higher than your base fee bid. When network traffic exceeds the ledger limit, the network enters into [surge pricing mode](#surge-pricing), and your effective base fee becomes your maximum bid. Fees are deducted from the source account unless there is a fee-bump transaction that states otherwise. Learn about fee-bump transactions in the [Fee-Bump Transaction section](../../build/guides/transactions/fee-bump-transactions.mdx). ## Surge and dynamic pricing ### Surge pricing The network can enter surge pricing mode under two circumstances: 1. when the number of operations submitted to a ledger exceeds the network capacity (1,000 operations for transactions that do not execute smart contracts), or 2. if there is competition between smart contract transactions for a particular resource (instructions, ledger entry accesses (reads and writes), ledger IO (bytes read and bytes written), and the total size of transactions to be applied). During this time, the network uses market dynamics to decide which transactions to include in the ledger. Transactions that offer a higher maximum base fee bid make it to the ledger first. During surge pricing mode, transactions are sorted based on their inclusion fee amount, and the user pays the minimum inclusion fee in their transaction set. For example, if there are five transactions with respective inclusion fees of 2, 3, 4, 4, and 5 XLM, and only four of them can make it to the ledger, then all included transactions pay the inclusion fee of 3 XLM. If all five transactions can make it to the ledger (which would mean the network is not in surge pricing mode), each would pay the minimum inclusion fee of 100 stroops (.00001 XLM). If there are multiple transactions offering the same inclusion fee, but they cannot all fit into the ledger, transactions are picked randomly so that the total operations for the entire set don’t exceed 1,000. The rest of the transactions are pushed to the next ledger or discarded if they’ve been waiting for too long. If your transaction is discarded, it will never appear in a ledger as retrieved from RPC. :::note It is recommended to apply [ledger bounds](./transactions/operations-and-transactions.mdx#ledger-bounds) or [time bounds](./transactions/operations-and-transactions.mdx#time-bounds) to transactions — either your transaction makes it to the ledger or fails, depending on your time and/or ledger parameters. ::: **You are more likely to pay a higher inclusion fee when submitting smart contract transactions.** Smart contract transactions have tighter ledger limits than transactions that don’t interact with smart contracts and will therefore experience surge pricing more often. You are more likely to pay your maximum inclusion fee bid or, at least, the minimum inclusion fee bid in your transaction set. So, you must plan your fee bidding strategy accordingly. ### Dynamic pricing for storage Stellar’s storage database size is determined by two forces: the rate of additions (writes) and the rate of deletions (evictions). Stellar has set a ledger growth threshold to a constant value (the `BucketListTargetSizeBytes` network parameter, implemented to prevent explosive state growth and subject to change based on validator vote). Because there is a fixed capacity, write fees are based on the ledger size and can alter dynamically based on that size. When the ledger size is large, there is a higher demand for storage space, which causes a higher write fee. Over time, entries are archived, reducing the overall ledger size and, thereby, reducing storage pricing. This fee model is designed as if the database size represents the current demand for storage at any given instant. Write fees will grow gradually over time when the database size is below the ledger growth threshold and will grow linearly, but with a 1,000x factor after exceeding that threshold. This is a safeguard against spam and is not anticipated under normal circumstances. ## Metering Metering is a mechanism in the host environment that accounts for the resource costs incurred during the execution of a smart contract. The outcomes of metering act as the canonical truth of a smart contract’s execution cost and serve as an input for fee computations. Stellar’s smart contract execution environment comprises a host and a guest. The host encapsulates shared functionalities for all contracts, including host objects, functions, and a Wasm interpreter (VM). The guest environment is where the compiled Wasm contract is interpreted and executed. A detailed discussion of these environments can be found in [Environment Concepts](../fundamentals/contract-development/environment-concepts.mdx). The division between the host and guest environments and their shared functionalities necessitates a unique approach to resource accounting. In particular, the resources required for executing Wasm instructions and running host functions must be accounted for uniformly, with costs in terms of CPU instructions and memory bytes. Consider two contracts: A and B, both comprising the same number of Wasm instructions. If Contract A repeatedly calls host functions for complex computations while Contract B executes pure arithmetic operations within the VM, Contract A should be more costly, and this difference should be accurately represented in the metering process. Metering ensures fairness, thwarts resource manipulation and attacks, and generates a deterministic and reproducible measure of runtime resource costs. ### Methodology To maintain equivalence in metering between the host and guest, computation costs on both sides are expressed in terms of CPU instructions and memory bytes (representing CPU and RAM usage). Metering and limit-checking occur within the host environment, and pre-calibrated numerical models ensure results are deterministic. ### Cost types Metering is segmented into host components, referred to as **cost types**. Each cost type can be viewed as a “meta instruction” symbolizing a specific host operation with a known complexity that depends on a runtime input. For instance, cost type `ComputeSha256Hash` represents the cost of computing the SHA256 hash of a byte array. :::info Execution of Wasm instructions is accounted for as a host cost type `WasmInsnExec`, which has a constant CPU cost per Wasm instruction. This methodology treats guest instructions and host executions equivalently. ::: Find a complete list of host cost types and their definitions here: [`ContractCostType`](https://github.com/stellar/stellar-xdr/blob/e372df9f677961aac04c5a4cc80a3667f310b29f/Stellar-contract-config-setting.x#L92-L155). ### Cost parameters Cost types are carefully selected to: 1. Serve as comprehensive building blocks for all significant contract execution costs; 2. Ensure each component cost increases at most linearly (i.e., constant or linear) with respect to its input. That is, `y = a + bx`, where `y` is the cost output, `x` is the input, and `a` & `b` are the constant and linear model parameters, respectively. Each cost type has a separate model for both resource types (CPU and memory). The parameters for each model, `a` and `b`, are calibrated and fitted offline against inputs of various sizes. The collection of all model cost parameters from the network configurable entries (see [`ConfigSettingsEntry`](https://github.com/stellar/stellar-xdr/blob/e372df9f677961aac04c5a4cc80a3667f310b29f/Stellar-contract-config-setting.x#L223-L226) can be updated through network consensus. ### Metering process Before contract execution, the host environment is prepared with the cost parameters and a budget defining the resource limits. Metering is then implemented to measure the cumulative resource consumption during host execution. During execution, whenever a component (a code block defining a cost type) is encountered, the corresponding model computes the resource output from the runtime input and increments the meter accordingly. The meter checks the cumulative consumption against the budget limit. If the limit is exceeded, an error is produced, and execution is terminated. If the contract execution concludes within the specified resource limits, the metered total of CPU instructions is recorded and utilized as the input for fee calculation. While memory usage is not included in the fee computation, it is nevertheless subject to the resource limits. ## Inclusion fee pricing strategies There are two primary methods to deal with inclusion fee fluctuations and surge pricing: - [**Method 1:**](#set-the-highest-fee-youre-comfortable-paying) set the highest fee you’re comfortable paying. This does not mean that you’ll pay that amount on every transaction — you will only pay what’s necessary to get into the ledger. Under normal (non-surge) circumstances, you will only pay the standard fee even with a higher maximum fee set. This method is simple, convenient, and efficient but can still potentially fail. - [**Method 2:**](#fee-bumps-on-past-transactions) resubmit a transaction with a higher fee using a fee-bump transaction ### Set the highest fee you’re comfortable paying​ In general, it’s a good idea to choose the highest fee you’re willing to pay per operation for your transaction to make it to the ledger. Wallet developers may want to offer users a chance to specify their own base fee, though it may make more sense to set a persistent global base fee that’s above the market rate since the average user probably doesn’t care if they’re paying 0.8 cents or 0.00008 cents. Remember that you’re more likely to pay your maximum fee bid with smart contract transactions. ### Fee-bumps on past transactions​ Even with a liberal fee-paying policy, your transaction may fail to make it into the ledger due to insufficient funds or untimely surges. Fee-bump transactions can solve this problem. The following snippet shows you how to resubmit a transaction with a higher fee (as long as you have the original transaction envelope): ```js // Let `lastTx` be some transaction that fails submission due to high fees, and // `lastFee` be the maximum fee (expressed as an int) willing to be paid by // `account` for `lastTx`. server.submitTransaction(lastTx).catch(function (error) { if (isFeeError(error)) { let bump = sdk.TransactionBuilder.buildFeeBumpTransaction( account, // account that will PAY the new fee lastFee * 10, // new fee lastTx, // the (entire) failing transaction server.networkPassphrase ); bump.sign(someAccount); return server.submitTransaction(bump); } // ...other error conditions... }).then(...); ``` Suppose you submit two distinct transactions with the same source account and sequence number; the second transaction is a fee-bump transaction. In that case, the second transaction will be included in the transaction queue, replacing the first transaction if and only if the fee bid of the second transaction is at least 10x the fee bid of the first transaction. This value can typically be found in the `fee_charged` field of the transaction response under the `tx_insufficient_fee` error case. --- ## Liquidity Pools on the Stellar Decentralized Exchange: Provide Liquidity and Enable Asset Swaps # Liquidity on Stellar: Stellar Decentralized Exchange & Liquidity Pools :::note This section is scoped specifically to liquidity regarding the AMM and SDEX built into the Stellar protocol and does not include information regarding smart contracts. ::: Users can trade and convert assets on the Stellar network with the use of path payments through Stellar’s decentralized exchange and liquidity pools. In this section, we will talk about the SDEX and liquidity pools. To learn about how these work together to execute transactions, see our [Path Payments Guide](../../build/guides/transactions/path-payments.mdx). ## SDEX The Stellar network acts as a decentralized distributed exchange that allows users to trade and convert assets with the [Manage Buy Offer](./transactions/list-of-operations#manage-buy-offer) and [Manage Sell Offer](./transactions/list-of-operations#manage-sell-offer) operations. The Stellar ledger stores both the balances held by user accounts and orders that user accounts make to buy or sell assets. ### Order books Stellar uses order books to operate its decentralized exchange. An order book is a record of outstanding orders on a network, and each record sits between two assets (wheat and sheep, for example). The order book for this asset pair records every account wanting to sell wheat for sheep and every account wanting to sell sheep for wheat. In traditional finance, buying is expressed as a “bid” order, and selling is expressed as an “ask” order (ask orders are also called offers). A couple of notes on order books on Stellar: - The term “offers” usually refers specifically to ask orders. In Stellar, however, all orders are stored as selling- i.e., the system automatically converts bids to asks. Because of this, the terms “offer” and “order” are used interchangeably in the Stellar ecosystem. - Order books contain all orders that are acceptable to parties on either side to make a trade. - Some assets will have a small or nonexistent order book between them. In these cases, Stellar facilitates path payments, which we’ll discuss later. To view an order book chart, see the [Order Book Wikipedia Page](https://en.wikipedia.org/wiki/Order_book). In addition, there are also plenty of video tutorials and articles out there that can help you understand how order books work in greater detail. ### Orders An account can create orders to buy or sell assets using the Manage Buy Offer, Manage Sell Offer, or Passive Order operations. The account must hold the asset it wants to exchange, and it must trust the issuer of the asset it is trying to buy. Orders in Stellar behave like limit orders in traditional markets. When an account initiates an order, it is checked against the existing orderbook for that asset pair. If the submitted order is a marketable order (for a marketable buy limit order, the limit price is at or above the ask price; for a marketable sell limit order, the limit price is at or below the bid price), it is filled at the existing order price for the available quantity at that price. If the order is not marketable (i.e., does not cross an existing order), the order is saved on the orderbook until it is either consumed by another order, consumed by a path payment, or canceled by the account that created the order. Each order constitutes a selling obligation for the selling asset and buying obligation for the buying asset. These obligations are stored in the account (for lumens) or trustline (for other assets) owned by the account creating the order. Any operation that would cause an account to be unable to satisfy its obligations — such as sending away too much balance — will fail. This guarantees that any order in the orderbook can be executed entirely. Orders are executed on a price-time priority, meaning orders will be executed based first on price; for orders placed at the same price, the order that was entered earlier is given priority and is executed before the newer one. ### Price and operations Each order in Stellar is quoted with an associated price and is represented as a ratio of the two assets in the order, one being the “quote asset” and the other being the “base asset”. This is to ensure there is no loss of precision when representing the price of the order (as opposed to storing the fraction as a floating-point number). Prices are specified as a {`numerator`, `denominator`} pair with both components of the fraction represented as 32-bit signed integers. The numerator is considered the base asset, and the denominator is considered the quote asset. When expressing a price of “Asset A in terms of Asset B”, the amount of B is the denominator (and therefore the quote asset), and A is the numerator (and therefore the base asset). As a good rule of thumb, it’s generally correct to be thinking about the base asset that is being bought/sold (in terms of the quote asset). #### Manage Buy Offer When creating a buy order in Stellar via the Manage Buy Offer operation, the price is specified as 1 unit of the base currency (the asset being bought), in terms of the quote asset (the asset that is being sold). For example, if you’re buying 100 XLM in exchange for 20 USD, you would specify the price as {20, 100}, which would be the equivalent of 5 XLM for 1 USD (or \$.20 per XLM). #### Manage Sell Offer When creating a sell order in Stellar via the Manage Sell Offer operation, the price is specified as 1 unit of base currency (the asset being sold), in terms of the quote asset (the asset that is being bought). For example, if you’re selling 100 XLM in exchange for 40 USD, you would specify the price as {40, 100}, which would be the equivalent of 2.5 XLM for 1 USD (or \$.40 per XLM). #### Passive Order Passive orders allow markets to have zero spread. If you want to exchange USD from anchor A for USD from anchor B at a 1:1 price, you can create two passive orders so the two orders don’t fill each other. A passive order is an order that does not execute against a marketable counter order with the same price. It will only fill if the prices are not equal. For example, if the best order to buy BTC for XLM has a price of 100XLM/BTC, and you make a passive offer to sell BTC at 100XLM/BTC, your passive offer does not take that existing offer. If you instead make a passive offer to sell BTC at 99XLM/BTC it would cross the existing offer and fill at 100XLM/BTC. An account can place a passive sell order via the Create Passive Sell Offer operation. ### Fees The order price you set is independent of the fee you pay for submitting that order in a transaction. Fees are always paid in XLM, and you specify them as a separate parameter when submitting the order to the network. To learn more about transaction fees, see our section on [Fees section](./fees-resource-limits-metering.mdx). ## Liquidity pools Liquidity pools enable automated market making on the Stellar network. Liquidity refers to how easily and cost-effectively one asset can be converted to another. ### Automated Market Makers (AMMs) Instead of relying on the buy and sell orders of decentralized exchanges, AMMs keep assets in an ecosystem liquid 24/7 using liquidity pools. Automated market makers provide liquidity using a mathematical equation. AMMs hold two different assets in a liquidity pool, and the quantities of those assets (or reserves) are inputs for that equation (Asset A \* Asset B = k). If an AMM holds more of the reserve assets, the asset prices move less in response to a trade. #### AMM pricing AMMs are willing to make some trades and unwilling to make others. For example, if 1 EUR = 1.17 USD, then the AMM might be willing to sell 1 EUR for 1.18 USD and unwilling to sell 1 EUR for 1.16 USD. To determine what trades are acceptable, the AMM enforces an invariant. There are many possible invariants, and Stellar enforces a constant product invariant and so is known as a constant product market maker. This means that AMMs on Stellar must never allow the product of the reserves to decrease. For example, suppose the current reserves in the liquidity pool are 1000 EUR and 1170 USD which implies a product of 1,170,000. Selling 1 EUR for 1.18 USD would be acceptable because that would leave reserves of 999 EUR and 1171.18 USD, which implies a product of 1,170,008.82. But selling 1 EUR for 1.16 USD would not be acceptable because that would leave reserves of 999 EUR and 1171.16 USD, which implies a product of 1,169,988.84. AMMs decide exchange rates based on the ratio of reserves in the liquidity pool. If this ratio is different than the true exchange rate, arbitrageurs will come in and trade with the AMM at a favorable price. This arbitrage trade moves the ratio of the reserves back toward the true exchange rate. AMMs charge fees on every trade, which is a fixed percentage of the amount bought by the AMM. For example, if an automated market maker sells 100 EUR for 118 USD then the fee is charged on the USD. The fee is 30 bps, which is equal to 0.30%. If you actually wanted to make this trade, you would need to pay about 118.355 USD for 100 EUR. The automated market maker factors the fees into the constant product invariant, so in reality, the product of the reserves grows after every trade. ### Liquidity pool participation Any eligible participant can deposit assets into a liquidity pool, and in return, receive pool shares representing their ownership of that asset. If there are 150 total pool shares and one user owns 30, they are entitled to withdraw 20% of the liquidity pool asset at any time. Pool shares are similar to other assets on Stellar but they cannot be transferred. You can only increase the number of pool shares you hold by depositing into a liquidity pool with the `LiquidityPoolDespositOp` and decrease the number of pool shares you hold by withdrawing from a liquidity pool with `LiquidityPoolWithdrawOp`. A pool share has two representations. The full representation is used with `ChangeTrustOp`, and the hashed representation is used in all other cases. When constructing the asset representation of a pool share, the assets must be in lexicographical order. For example, A-B is in the correct order but B-A is not. This results in a canonical representation of a pool share. AMMs charge a fee on all trades and the participants in the liquidity pool receive a share of the fee proportional to their share of the assets in the liquidity pool. Participants collect these fees when they withdraw their assets from the pool. The fee rate on Stellar is 30 bps, which is equal to 0.30%. These fees are completely separate from the network fees. ### Trustlines Users need to establish trustlines to three different assets to participate in a liquidity pool: both the reserve assets (unless one of them is XLM) and the pool share itself. An account needs a trustline for every pool share it wants to own. It is not possible to deposit into a liquidity pool without a trustline for the corresponding pool share. Pool share trustlines differ from trustlines for other assets in a few ways: 1. A pool share trustline cannot be created unless the account already has trustlines that are authorized or authorized to maintain liabilities for the assets in the liquidity pool. See below for more information about how authorization impacts pool share trustlines. 2. A pool share trustline requires 2 base reserves instead of 1. For example, an account (2 base reserves) with a trustline for asset A (1 base reserve), a trustline for asset B (1 base reserve), and a trustline for the A-B pool share (2 base reserves) would have a reserve requirement of 6 base reserves. ### Authorization Pool share trustlines cannot be authorized or de-authorized independently. Instead, the authorization of a pool share trustline is derived from the trustlines for the assets in the liquidity pool. This design is necessary because a liquidity pool may contain assets from two different issuers, and both issuers should have a say in whether the pool share trustline is authorized. There are a few possibilities with regard to authorization. The behavior of the A-B pool share trustline is determined according to the following table: | SCENARIO | BEHAVIOR | | --- | --- | | Trustlines for A and B are fully authorized | No restrictions on deposit and withdrawal | | Trustline for A is fully authorized but trustline for B is authorized to maintain liabilities | Trustlines for A and B are authorized to maintain liabilities | | Trustline for B is fully authorized but trustline for A is authorized to maintain liabilities | Trustlines for A and B are authorized to maintain liabilities | | Trustlines for A and B are authorized to maintain liabilities | Trustlines for A and B are authorized to maintain liabilities | | Trustline for A is not authorized or doesn’t exist | Pool share trustline does not exist | | Trustline for B is not authorized or doesn’t exist | Pool share trustline does not exist | If the issuer of A or B revokes authorization, then the account will automatically withdraw from every liquidity pool containing that asset and those pool share trustlines will be deleted. We say that these pool shares have been redeemed. For example, if the account participates in the A-B, A-C, and B-C liquidity pools and the issuer of A revokes authorization then the account will redeem from A-B and A-C but not B-C. For each redeemed pool share trustline, a Claimable Balance will be created for each asset contained in the pool if there is a balance being withdrawn and the redeemer is not the issuer of that asset. The claimant of the Claimable Balance will be the owner of the deleted pool share trustline, and the sponsor of the Claimable Balance will be the sponsor of the deleted pool share trustline. The BalanceID of each Claimable Balance is the SHA-256 hash of the `revokeID`. ### Operations There are two operations that facilitate participation in a liquidity pool: `LiquidityPoolDeposit` and `LiquidityPoolWithdraw`. Use `LiquidityPoolDeposit` to start providing liquidity to the market. Use `LiquidityPoolWithdraw` to stop providing liquidity to the market. However, users don’t need to participate in the pool to take advantage of what it’s offering: an easy way to exchange two assets. For that, just use `PathPaymentStrictReceive` or `PathPaymentStrictSend`. If your application is already using path payments, then you don’t need to change anything for users to take advantage of the prices available in liquidity pools. ### Examples Here we will cover basic liquidity pool participation and querying. #### Preamble For all of the following examples, we’ll be working with three funded Testnet accounts. If you’d like to follow along, generate some keypairs and fund them via the friendbot. The following code sets up the accounts and defines some helper functions. These should be familiar if you’ve played around with other examples like clawbacks. ```js const sdk = require("@stellar/stellar-sdk"); const BigNumber = require("bignumber.js"); let server = new sdk.Horizon.Server("https://horizon-testnet.stellar.org"); /// Helps simplify creating & signing a transaction. function buildTx(source, signer, ...ops) { let tx = new sdk.TransactionBuilder(source, { fee: sdk.BASE_FEE, networkPassphrase: sdk.Networks.TESTNET, }); ops.forEach((op) => tx.addOperation(op)); tx = tx.setTimeout(30).build(); tx.sign(signer); return tx; } /// Returns the given asset pair in "protocol order." function orderAssets(A, B) { return sdk.Asset.compare(A, B) <= 0 ? [A, B] : [B, A]; } /// Returns all of the accounts we'll be using. function getAccounts() { return Promise.all(kps.map((kp) => server.loadAccount(kp.publicKey()))); } const kps = [ "SBGCD73TK2PTW2DQNWUYZSTCTHHVJPL4GZF3GVZMCDL6GYETYNAYOADN", "SAAQFHI2FMSIC6OFPWZ3PDIIX3OF64RS3EB52VLYYZBX6GYB54TW3Q4U", "SCJWYFTBDMDPAABHVJZE3DRMBRTEH4AIC5YUM54QGW57NUBM2XX6433P", ].map((s) => sdk.Keypair.fromSecret(s)); // kp0 issues the assets const kp0 = kps[0]; const [A, B] = orderAssets( ...[new sdk.Asset("A", kp0.publicKey()), new sdk.Asset("B", kp0.publicKey())], ); /// Establishes trustlines and funds `recipientKps` for all `assets`. function distributeAssets(issuerKp, recipientKps, ...assets) { return server.loadAccount(issuerKp.publicKey()).then((issuer) => { const ops = recipientKps .map((recipientKp) => assets.map((asset) => [ sdk.Operation.changeTrust({ source: recipientKp.publicKey(), limit: "100000", asset: asset, }), sdk.Operation.payment({ source: issuerKp.publicKey(), destination: recipientKp.publicKey(), amount: "100000", asset: asset, }), ]), ) .flat(2); let tx = buildTx(issuer, issuerKp, ...ops); tx.sign(...recipientKps); return server.submitTransaction(tx); }); } function preamble() { return distributeAssets(kp0, [kps[1], kps[2]], A, B); } ``` ```python from decimal import Decimal from typing import List, Any, Dict from stellar_sdk import * server = Server("https://horizon-testnet.stellar.org") # Preamble def new_tx_builder(source: str) -> TransactionBuilder: network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE base_fee = 100 source_account = server.load_account(source) builder = TransactionBuilder( source_account=source_account, network_passphrase=network_passphrase, base_fee=base_fee ).set_timeout(30) return builder # Returns the given asset pair in "protocol order." def order_asset(a: Asset, b: Asset) -> List[Asset]: return [a, b] if LiquidityPoolAsset.is_valid_lexicographic_order(a, b) else [b, a] secrets = [ "SBGCD73TK2PTW2DQNWUYZSTCTHHVJPL4GZF3GVZMCDL6GYETYNAYOADN", "SAAQFHI2FMSIC6OFPWZ3PDIIX3OF64RS3EB52VLYYZBX6GYB54TW3Q4U", "SCJWYFTBDMDPAABHVJZE3DRMBRTEH4AIC5YUM54QGW57NUBM2XX6433P", ] kps = [Keypair.from_secret(secret=secret) for secret in secrets] # kp0 issues the assets kp0 = kps[0] asset_a, asset_b = order_asset(Asset("A", kp0.public_key), Asset("B", kp0.public_key)) def distribute_assets( issuer_kp: Keypair, recipient_kp: Keypair, assets: List[Asset] ) -> Dict[str, Any]: builder = new_tx_builder(issuer_kp.public_key) for asset in assets: builder.append_change_trust_op( asset=asset, limit="100000", source=recipient_kp.public_key ).append_payment_op( destination=recipient_kp.public_key, asset=asset, amount="100000", source=issuer_kp.public_key, ) tx = builder.build() tx.sign(issuer_kp) tx.sign(recipient_kp) resp = server.submit_transaction(tx) return resp def preamble() -> None: resp1 = distribute_assets(kp0, kps[1], [asset_a, asset_b]) resp2 = distribute_assets(kp0, kps[2], [asset_a, asset_b]) # ... ``` Here, we use `distributeAssets()` to establish trustlines and set up initial balances of two custom assets (`A` and `B`, issued by `kp0`) for two accounts (`kp2` and `kp3`). For someone to participate in the pool, they must establish trustlines to each of the asset issuers and to the pool share asset (explained below). Note the `orderAssets()` helper here. Operations related to liquidity pools refer to the asset pair arbitrarily as `A` and `B`; however, they must be “ordered” such that `A` < `B`. This ordering is defined by the protocol, but its details should not be relevant (if you’re curious, it’s essentially lexicographically ordered by asset type, code, then issuer). We can use the comparison methods built into the SDKs (like `Asset.compare`) to ensure we pass them in the right order and avoid errors. #### Participation: Creation First, let's create a liquidity pool for the asset pair defined in the preamble. This involves establishing a trustline to the pool itself: ```js const poolShareAsset = new sdk.LiquidityPoolAsset( A, B, sdk.LiquidityPoolFeeV18, ); function establishPoolTrustline(account, keypair, poolAsset) { return server.submitTransaction( buildTx( account, keypair, sdk.Operation.changeTrust({ asset: poolAsset, limit: "100000", }), ), ); } ``` ```python pool_share_asset = LiquidityPoolAsset(asset_a=asset_a, asset_b=asset_b) def establish_pool_trustline(source: Keypair, pool_asset: LiquidityPoolAsset) -> Dict[str, Any]: tx = ( new_tx_builder(source.public_key) .append_change_trust_op(asset=pool_asset, limit="100000") .build() ) tx.sign(source) return server.submit_transaction(tx) ``` This lets the participants hold pool shares, which means now they can perform deposits and withdrawals. #### Participation: Deposits To work with a liquidity pool, you need to know its ID beforehand. It’s a deterministic value, and only a single liquidity pool can exist for a particular asset pair, so you can calculate it locally from the pool parameters. ```js const poolId = sdk .getLiquidityPoolId( "constant_product", poolShareAsset.getLiquidityPoolParameters(), ) .toString("hex"); function addLiquidity(source, signer, poolId, maxReserveA, maxReserveB) { const exactPrice = maxReserveA / maxReserveB; const minPrice = exactPrice - exactPrice * 0.1; const maxPrice = exactPrice + exactPrice * 0.1; return server.submitTransaction( buildTx( source, signer, sdk.Operation.liquidityPoolDeposit({ liquidityPoolId: poolId, maxAmountA: maxReserveA, maxAmountB: maxReserveB, minPrice: minPrice.toFixed(7), maxPrice: maxPrice.toFixed(7), }), ), ); } ``` ```python pool_id = pool_share_asset.liquidity_pool_id def add_liquidity( source: Keypair, pool_id: str, max_reserve_a: Decimal, max_reserve_b: Decimal, ) -> dict[str, Any]: exact_price = max_reserve_a / max_reserve_b min_price = exact_price - exact_price * Decimal("0.1") max_price = exact_price + exact_price * Decimal("0.1") tx = ( new_tx_builder(source.public_key) .append_liquidity_pool_deposit_op( liquidity_pool_id=pool_id, max_amount_a=f"{max_reserve_a:.7f}", max_amount_b=f"{max_reserve_b:.7f}", min_price=min_price, max_price=max_price, ) .build() ) tx.sign(source) return server.submit_transaction(tx) ``` When depositing assets into a liquidity pool, you need to define your acceptable price bounds. In the above function, we allow for a +/-10% margin of error from the “spot price”. This margin is by no means a recommendation and is chosen just for demonstration. Notice that we also specify the maximum amount of each reserve we’re willing to deposit. This, alongside the minimum and maximum prices, helps define boundaries for the deposit, since there can always be a change in the exchange rate between submitting the operation and it getting accepted by the network. #### Participation: Withdrawals If you own shares of a particular pool, you can withdraw reserves from it. The operation structure mirrors the deposit closely: ```js function removeLiquidity(source, signer, poolId, sharesAmount) { return server .liquidityPools() .liquidityPoolId(poolId) .call() .then((poolInfo) => { let totalShares = poolInfo.total_shares; let minReserveA = (sharesAmount / totalShares) * poolInfo.reserves[0].amount * 0.95; let minReserveB = (sharesAmount / totalShares) * poolInfo.reserves[1].amount * 0.95; return server.submitTransaction( buildTx( source, signer, sdk.Operation.liquidityPoolWithdraw({ liquidityPoolId: poolId, amount: sharesAmount, minAmountA: minReserveA.toFixed(7), minAmountB: minReserveB.toFixed(7), }), ), ); }); } ``` ```python def remove_liquidity( source: Keypair, pool_id: str, shares_amount: Decimal ) -> dict[str, Any]: pool_info = server.liquidity_pools().liquidity_pool(pool_id).call() total_shares = Decimal(pool_info["total_shares"]) min_reserve_a = ( shares_amount / total_shares * Decimal(pool_info["reserves"][0]["amount"]) * Decimal("0.95") ) # min_reserve_b = ( shares_amount / total_shares * Decimal(pool_info["reserves"][1]["amount"]) * Decimal("0.95") ) tx = ( new_tx_builder(source.public_key) .append_liquidity_pool_withdraw_op( liquidity_pool_id=pool_id, amount=f"{shares_amount:.7f}", min_amount_a=f"{min_reserve_a:.7f}", min_amount_b=f"{min_reserve_b:.7f}", ) .build() ) tx.sign(source) return server.submit_transaction(tx) ``` Notice here that we specify the minimum amount. Much like with a strict-receive path payment, we’re specifying that we’re not willing to receive less than this amount of each asset from the pool. This effectively defines a minimum withdrawal price. #### Putting it all together Finally, we can combine these pieces together to simulate some participation in a liquidity pool. We’ll have everyone deposit increasing amounts into the pool, then one participant withdraws their shares. Between each step, we’ll retrieve the spot price. ```js function main() { return getAccounts() .then((accounts) => { return Promise.all( kps.map((kp, i) => { const acc = accounts[i]; const depositA = ((i + 1) * 1000).toString(); const depositB = ((i + 1) * 3000).toString(); // maintain a 1:3 ratio return establishPoolTrustline(acc, kp, poolShareAsset) .then(() => addLiquidity(acc, kp, poolId, depositA, depositB)) .then(() => getSpotPrice()); }), ).then(() => accounts); }) .then((accounts) => { // kp1 takes all his/her shares out return server .accounts() .accountId(kps[1].publicKey()) .call() .then(({ balances }) => { let balance = 0; balances.every((bal) => { if ( bal.asset_type === "liquidity_pool_shares" && bal.liquidity_pool_id === poolId ) { balance = bal.balance; return false; } return true; }); return balance; }) .then((balance) => removeLiquidity(accounts[1], kps[1], poolId, balance), ); }) .then(() => getSpotPrice()); } function getSpotPrice() { return server .liquidityPools() .liquidityPoolId(poolId) .call() .then((pool) => { const [a, b] = pool.reserves.map((r) => r.amount); const spotPrice = new BigNumber(a).div(b); console.log(`Price: ${a}/${b} = ${spotPrice.toFormat(2)}`); }); } preamble().then(main); ``` ```python def main(): deposit_a = Decimal(1000) deposit_b = Decimal(3000) # maintain a 1:3 ratio establish_pool_trustline(kps[1], pool_share_asset) add_liquidity(kps[1], pool_id, deposit_a, deposit_b) get_spot_price() deposit_a = Decimal(2000) deposit_b = Decimal(6000) # maintain a 1:3 ratio establish_pool_trustline(kps[2], pool_share_asset) add_liquidity(kps[2], pool_id, deposit_a, deposit_b) get_spot_price() # kp1 takes all his/her shares out balance = 0 for b in server.accounts().account_id(kps[1].public_key).call()["balances"]: if ( b["asset_type"] == "liquidity_pool_shares" and b["liquidity_pool_id"] == pool_id ): balance = Decimal(b["balance"]) break if not balance: raise remove_liquidity(kps[1], pool_id, balance) get_spot_price() def get_spot_price(): resp = server.liquidity_pools().liquidity_pool(pool_id).call() amount_a = resp["reserves"][0]["amount"] amount_b = resp["reserves"][1]["amount"] spot_price = Decimal(amount_a) / Decimal(amount_b) print(f"Price: {amount_a}/{amount_b} = {spot_price:.7f}") if __name__ == '__main__': preamble() main() ``` #### Watching Liquidity Pool Activity You can access the transactions, operations, and effects related to a liquidity pool if you want to track its activity. Let’s see how we can track the latest deposits in a pool (suppose `poolId` is defined as before): ```js server .operations() .forLiquidityPool(poolId) .call() .then((ops) => { ops.records .filter((op) => op.type == "liquidity_pool_deposit") .forEach((op) => { console.log("Reserves deposited:"); op.reserves_deposited.forEach((r) => console.log(` ${r.amount} of ${r.asset}`), ); console.log(" for pool shares: ", op.shares_received); }); }); ``` ```python def watch_liquidity_pool_activity(): for op in ( server.operations() .for_liquidity_pool(liquidity_pool_id=pool_id) .cursor("now") .stream() ): if op["type"] == "liquidity_pool_deposit": print("Reserves deposited:") for r in op["reserves_deposited"]: print(f" {r['amount']} of {r['asset']}") print(f" for pool shares: {op['shares_received']}") # ... ``` --- ## Understanding Lumens, The Native Currency of the Network # Lumens (XLM) Lumens (XLM) are the native currency of the Stellar network. The lumen is the only token that doesn’t require an issuer or trustline. They are used to pay all transaction [fees](#transaction-fees), fund [rent](./fees-resource-limits-metering.mdx#resource-fee), and to cover [minimum balance requirements](stellar-data-structures/accounts.mdx#base-reserves-and-subentries) on the network. To read up on the basics of lumens, head over to our Stellar Learn site: [Stellar Learn: Lumens](https://stellar.org/lumens) ## Transaction fees Stellar requires a small fee for all transactions to prevent ledger spam and prioritize transactions during surge pricing. Transaction fees are paid in lumens. To learn about fees on Stellar, see our [Fees section](./fees-resource-limits-metering.mdx). Smart contract transactions on Stellar employ a different fee structure based on an inclusion fee and resource consumption (which includes [rent](#rent)). Read more in the [Fees and Metering section](./fees-resource-limits-metering.mdx). ## Base reserves A unit of measurement used to calculate an account’s minimum balance. One base reserve is currently 0.5 XLM. Validators can vote to change the base reserve, but that’s uncommon and should only happen every few years. ## Minimum balance Stellar accounts must maintain a minimum balance to exist, which is calculated using the base reserve. An account must always maintain a minimum balance of two base reserves (currently 1 XLM). Every subentry after that requires an additional base reserve (currently 0.5 XLM) and increases the account’s minimum balance. Subentries include trustlines (for both traditional assets and pool shares), offers, signers, and data entries. An account cannot have more than 1,000 subentries. Data also lives on the ledger as ledger entries. Ledger entries include claimable balances (which require a base reserve per claimant) and liquidity pool deposits and withdrawals. For example, an account with one trustline, two offers, and a claimable balance with one claimant has a minimum balance of: 2 base reserves (1 XLM) + 3 subentries/base reserves (1.5 XLM) + 1 ledger entry/base reserve (1 XLM) = 3.5 XLM When you close a subentry, the associated base reserve will be added to your available balance. An account must always pay its own minimum balance unless it is being sponsored by another account. Sponsorship can cover both an account's subentries and its own two base reserves — so a sponsored account can even be created with a starting balance of `0`, with the sponsor carrying its reserves. For information about this, see our [Sponsored Reserves guide](../../build/guides/transactions/sponsored-reserves.mdx). ## Rent Smart contract data does not require any base reserves in order to live on the ledger, so every smart contract entry must pay rent instead. The rent charged for an entry to exist on the ledger is based on how big the entry is and how long it should be live on the ledger before being archived. There are different rent requirements for each storage type `Persistent`, `Temporary`, and `Instance`, which you can read about in the [State Archival section](../fundamentals/contract-development/storage/state-archival.mdx). ## Lumen Supply Metrics This section explains how lumen supply metrics are calculated and made available via API. This information can be useful for products and services that track the distribution of XLM, including market cap aggregators and some exchanges, or to anyone who wants to investigate the distribution of XLM defined by the SDF mandate. Unlike many other blockchains, the native network currency is not created through mining- all XLM that has ever existed and will ever exist was created when the Stellar network went live. [SDF’s Dashboard API endpoint](https://dashboard.stellar.org/api/v3/lumens) will always have the live totals for the essential numbers around lumens. This guide explains important supply metrics like Original Supply, Total Supply, and Circulating Supply entailed in that data. ### Dashboard API As of July 21st, 2026, the Dashboard API shows: ```json { "updatedAt": "2026-07-21T16:30:53.463Z", "originalSupply": "100000000000", "inflationLumens": "5443902087.3472865", "burnedLumens": "55442115247.4348098", "totalSupply": "50001786839.9124767", "upgradeReserve": "258885847.5135959", "feePool": "10206059.1580437", "sdfMandate": "15563517195.789479", "circulatingSupply": "34169177737.4513581", "_details": "https://www.stellar.org/developers/guides/lumen-supply-metrics.html" } ``` ### Definitions **originalSupply** One hundred billion lumens [were created](https://stellar.expert/explorer/public/ledger/2) when the Stellar network went live. That’s the Original Supply for the network. **inflationLumens** For the first five or so years of Stellar’s existence, the supply of lumens increased by 1% annually. This “network inflation” was ended by validator vote on October 28, 2019. The total number of lumens generated by inflation was 5,443,902,087.3472865. Adding this number to the Original Supply, you get the total lumens that have ever existed: 105,443,902,087.3472865. This number is visible on the [List All Ledgers](../../data/apis/horizon/api-reference/list-all-ledgers.api.mdx) Horizon API endpoint as `_embedded.records.total_coins`. See all Stellar Mainnet Horizon data providers [here](../../data/apis/horizon/providers.mdx). **burnedLumens** These are all the lumens sent to accounts with no signers, meaning the funds are inaccessible and have been removed forever from Stellar’s lumen supply. While any address with no signers is counted here, the vast majority of the lumens in this sum are in a single locked address. On November 4, 2019, SDF [reduced](https://stellar.org/blog/sdfs-next-steps) its lumen holdings to better reflect its mission and the growth of the Stellar ecosystem. To do so, the Foundation sent 55,442,095,285.7418 lumens to [GALA…LUTO](https://stellar.expert/explorer/public/account/GALAXYVOIDAOPZTDLHILAJQKCVVFMD4IKLXLSZV5YHO7VY74IWZILUTO). **totalSupply** The Total Supply is the number of lumens now in existence: 50,001,786,839.9124767. The Total Supply includes four major categories of lumens, which the API treats in detail. **upgradeReserve** The Upgrade Reserve is a special address that’s neither circulating nor a part of SDF’s mandate. When Stellar [changed its consensus algorithm](https://stellar.org/blog/upgraded-network-is-here) in 2015 and relaunched the network these lumens were set aside, to be claimed, one-for-one, by holders of the old network tokens. The [Upgrade Reserve account](https://stellar.expert/explorer/public/account/GBEZOC5U4TVH7ZY5N3FLYHTCZSI6VFGTULG7PBITLF5ZEBPJXFT46YZM) is essentially an escrow, and we don’t expect many claimants to come and pull those lumens into the circulating supply at this point. **feePool** The Fee Pool is where network fees collect. The lumens do not belong to any particular account. No one has access to the fee pool, so these lumens are non-circulating. Network validators could theoretically vote for a protocol change that would affect the fee pool, so we include it in the total supply. Stellar’s transaction fees are extremely low so the fee pool grows very slowly. The Fee Pool is tracked by the protocol itself, and the current number is visible on the [List All Ledgers](../../data/apis/horizon/api-reference/list-all-ledgers.api.mdx) Horizon API endpoint as `_embedded.records.fee_pool`. See all Stellar Mainnet Horizon data providers [here](../../data/apis/horizon/providers.mdx). **sdfMandate** The SDF Mandate is described in detail on the [Stellar Foundation mandate page](https://stellar.org/foundation/mandate), the authoritative source for the mandate's structure and accounts. The Foundation was funded by lumens generated at Stellar’s inception; all of those lumens will eventually be spent or distributed to enhance and promote Stellar. SDF organizes these holdings into four buckets. The on-chain accounts in each bucket, as of July 2026, are listed below — balances change continuously, so refer to the mandate page or each account's [stellar.expert](https://stellar.expert) page for live figures: #### SDF Development - [Direct Development, Available Funds](https://stellar.expert/explorer/public/account/GB6NVEN5HSUBKMYCE5ZOWSK5K23TBWRUQLZY3KNMXUZ3AQ2ESC4MY4AQ) - [Direct Development (Hot 1)](https://stellar.expert/explorer/public/account/GATL3ETTZ3XDGFXX2ELPIKCZL7S5D2HY3VK4T7LRPD6DW5JOLAEZSZBA) - [Direct Development (Hot 2)](https://stellar.expert/explorer/public/account/GAKGC35HMNB7A3Q2V5SQU6VJC2JFTZB6I7ZW77SJSMRCOX2ZFBGJOCHH) - [Direct Development (Hot 3)](https://stellar.expert/explorer/public/account/GAPV2C4BTHXPL2IVYDXJ5PUU7Q3LAXU7OAQDP7KVYHLCNM2JTAJNOQQI) #### Stellar Growth - [Growth 1](https://stellar.expert/explorer/public/account/GCVJDBALC2RQFLD2HYGQGWNFZBCOD2CPOTN3LE7FWRZ44H2WRAVZLFCU) - [Growth Hot 1](https://stellar.expert/explorer/public/account/GC3ITNZSVVPOWZ5BU7S64XKNI5VPTRSBEXXLS67V4K6LEUETWBMTE7IH) - [Growth 2](https://stellar.expert/explorer/public/account/GBEVKAYIPWC5AQT6D4N7FC3XGKRRBMPCAMTO3QZWMHHACLHTMAHAM2TP) - [Growth 3](https://stellar.expert/explorer/public/account/GDUY7J7A33TQWOSOQGDO776GGLM3UQERL4J3SPT56F6YS4ID7MLDERI4) #### Product & Innovation - [Product and Innovation 1](https://stellar.expert/explorer/public/account/GCPWKVQNLDPD4RNP5CAXME4BEDTKSSYRR4MMEL4KG65NEGCOGNJW7QI2) - [Product and Innovation 2](https://stellar.expert/explorer/public/account/GDKIJJIKXLOM2NRMPNQZUUYK24ZPVFC6426GZAEP3KUK6KEJLACCWNMX) - [Product and Innovation Hot 2](https://stellar.expert/explorer/public/account/GDWXQOTIIDO2EUK4DIGIBLEHLME2IAJRNU6JDFS5B2ZTND65P7J36WQZ) #### Assets & Liquidity - [Assets and Liquidity](https://stellar.expert/explorer/public/account/GAMGGUQKKJ637ILVDOSCT5X7HYSZDUPGXSUW67B2UKMG2HEN5TPWN3LQ) - [Assets and Liquidity (Hot)](https://stellar.expert/explorer/public/account/GANII5Y2LABEBK74NWNKS4NREX2T52YTBGQDRDKVBFRIIF5VE4ORYOVY) **circulatingSupply** The Circulating Supply is lumens in the hands of individuals and independent companies. These are lumens out in the world, used to pay network fees and fund Stellar accounts. They are also used as a general medium of exchange. We expect Stellar’s Circulating Supply to grow steadily as SDF spends and distributes lumens according to its mandate. Lumens in the Total Supply, but not in the SDF Mandate, Upgrade Reserve, or Fee Pool are assumed to be circulating. --- ## Overview of the Stellar Consensus Protocol (SCP) and Transaction Validation # Stellar Consensus Protocol Consensus is hugely important in a decentralized payment system. It distributes the monitoring and approval of transactions across many individual nodes (computers) instead of relying on one closed, central system. Nodes are run by organizations or individuals, and the goal is for all nodes to update the ledger in the same way, ensuring each ledger reaches the same state. Consensus is vital for the security of the blockchain, allowing nodes to agree on something safely and preventing double-spend attacks. The Stellar network reaches consensus using the Stellar Consensus Protocol (SCP), which is a construction of the Federated Byzantine Agreement (FBA). FBA differs from other well-known consensus mechanisms like Proof of Work (which relies on a node’s computational power) and Proof of Stake (which relies on a node’s staking power) by instead relying on the agreement of trusted nodes. In SCP, each participating Stellar Core node (also called a validator or validator node) decides what set of other nodes they want to trust. The flexibility of user-defined trust allows for open network membership (meaning anyone can become a Core node) and decentralized control (meaning no central authority dictates whose vote is required for consensus). There are no monetary rewards for being a validator on the Stellar network. Instead, users are encouraged to become a validator because they are then contributing to the security and resiliency of the network, which benefits the products and services built on Stellar. There are three desired properties of consensus mechanisms: fault tolerance, safety, and liveness. - Fault tolerance - the system can continue operating despite node failures or malfunctions - Safety - no two nodes ever agree on different values, guarantees nodes will produce the same block - Liveness - a node can output a value without the participation of any misbehaving nodes Consensus mechanisms can typically only prioritize two out of three of these properties. SCP prioritizes fault tolerance and safety over liveness. Because of prioritizing safety, blocks can sometimes get stuck while waiting for nodes to agree. ## SCP components ### Quorum set As mentioned above, each Core node decides on which other nodes it would like to trust to reach agreement. A node’s trusted set of nodes is called a **quorum set**. Validators might add each other to their quorum sets due to innate trust associated with real-world identities. ### Thresholds and quorum slices In addition to choosing a quorum set, Core nodes must also choose a **threshold**. A threshold is the minimum number of nodes in a quorum set that must agree to reach consensus. For example, let’s say node B has nodes [A, C, D] in its quorum set and sets the threshold to 2. This means that any combination of 2 nodes in the quorum set agreeing is valid: either [A,C], [C,D], or [A,D] must agree for the node to proceed. The combination of agreeing nodes within the quorum set are called **quorum slices**. ### Node blocking sets Nodes can be blocked from reaching consensus by **node blocking sets**. Node blocking sets are any set of nodes in a quorum set that prevent a node from reaching agreement. For example, if a node requires 3 out of 4 of the nodes in its quorum set to agree, any combination of two nodes is considered a node blocking set. ### Quorum A **quorum** is a set of nodes sufficient to reach an agreement wherein each node is part of a quorum slice. ### Statement Valid **statements** on Stellar express the different opinions of nodes regarding transaction sets to agree on for a given ledger. For example: “I propose this transaction set for ledger number 800”. A node’s opinion on a statement depends on the opinions of its quorum set. ## Federated voting In the SCP, agreement is achieved using federated voting. A node reasons about the state of the network based on what it learns from its quorum set- before a statement is 100% agreed upon by every honest node in the network, it goes through three steps of federated voting: (1) Vote, (2) Accept, and (3) Confirm. A node can have four opinions on a statement (let’s call the statement “A”) - I don’t know anything about A and have no opinion - I vote for A, it’s valid, but I don’t know if it’s safe to act on it yet - I accept A, because enough nodes supported this statement, but I don’t know if it’s safe to act on it yet - I confirm A, it is safe to act on it. Even if every node in my quorum has not confirmed A, they will not be able to confirm anything else but A. To transition between the states above, federated voting has the following rules: - Vote for A if it is consistent with my previous votes - Accept A if either: - Every node in my quorum slice voted for or accepted A OR - My blocking set accepted A (even if I voted for something that contradicts A in the past, I forget about that vote, and proceed with accepting A) - Confirm A if every node in a quorum slice accepted A ## Consensus rounds Each consensus round is separated into two stages: ### Nomination protocol In the nomination protocol, candidate transaction sets are selected to be included in a ledger. Once a node confirms its first candidate, it stops voting to nominate any new transaction sets. It may still accept or confirm previously nominated statements. This guarantees that at some point, all nodes will converge on a candidate set. If every node on the network stops introducing new values but continues to confirm what other nodes confirmed, eventually, everyone will end up with the same list of candidates. A node may start the ballot protocol as soon as it confirms a candidate. After it confirms its first candidate and starts the ballot protocol, nomination continues running in the background. ### Ballot protocol The ballot protocol ensures that the network can unanimously confirm and apply nominated transaction sets. It consists of two steps: 1. Prepare - verifies that a node’s quorum slice has the right value and is willing to commit it 2. Commit - ensures that a node’s quorum slice actually commits the value ## White paper Access the SCP white paper [here](https://stellar.org/learn/stellar-consensus-protocol). --- ## Blockchain Data Structures: Accounts, Smart Contracts, Assets & Ledgers # Stellar Data Structures The fundamental data structures and building blocks present on the Stellar network. --- ## Understanding Accounts: Balances, Transactions, and Network Interactions # Accounts Accounts are the central data structure in Stellar—they hold balances, sign transactions, and issue assets. Accounts can only exist with a valid keypair and the required minimum balance of XLM. To learn about minimum balance requirements, [see our section on Lumens](../lumens.mdx#minimum-balance). :::note There are two types of accounts on Stellar: Stellar accounts (`G...` addresses) and contract accounts (`C...` addresses). For a minimal contract account walkthrough, start with the [Simple Account example](../../../build/smart-contracts/example-contracts/simple-account.mdx). This section focuses on Stellar `G...` accounts. ::: `G...` accounts are made up of the below fields. Click on the field to learn more about it. - [Account ID](../../glossary.mdx#account-id) - [Balances](../../glossary.mdx#balance) - [Flags](../../glossary.mdx#flags) - [Home domain (up to 32 characters)](../../glossary.mdx#home-domain) - [Liabilities](../../glossary.mdx#liability) - [Number of entries sponsored by this account](../../../build/guides/transactions/sponsored-reserves.mdx) - [Number of sponsored reserves](../../../build/guides/transactions/sponsored-reserves.mdx) - [Number of subentries](./accounts.mdx#subentries) - [Sequence number](../../glossary.mdx#sequence-number) - [Signers](../../fundamentals/transactions/signatures-multisig.mdx) - [Thresholds](../../fundamentals/transactions/signatures-multisig.mdx#thresholds) ## Base reserves and subentries Accounts store data in subentries, and each subentry increases the account’s required minimum balance. ### Base reserves A base reserve is a unit of measurement used to calculate an account’s minimum balance. One base reserve is currently 0.5 XLM. ### Subentries Account data is stored in subentries, each of which increases an account’s minimum balance by one base reserve (0.5 XLM). An account cannot have more than 1,000 subentries. Possible subentries are: - Trustlines (includes traditional assets and pool shares) - Offers - Additional signers - Data entries (includes data made with the `manageData` operation, not smart contract ledger entries) ## Trustlines Trustlines are an explicit opt-in for an account to hold a particular asset. To hold a specific asset, an account must establish a trustline with the issuing account using the [`change_trust` operation](../transactions/list-of-operations.mdx#change-trust). Trustlines track the balance of an asset and can also limit the amount of an asset that an account can hold. A trustline must be established for an account to receive any asset except lumens (XLM). You can create a claimable balance to send assets to an account without a trustline, but the recipient has to create a trustline to claim that balance. Learn more in the [Claimable Balances guide](../../../build/guides/transactions/claimable-balances.mdx). A trustline also tracks liabilities. Buying liabilities equal the total amount of the asset offered to buy aggregated over all offers owned by an account, and selling liabilities equal the total amount of the asset offered to sell aggregated over all offers owned by an account. A trustline must always have a balance sufficiently large to satisfy its selling liabilities and a balance sufficiently below its limit to accommodate its buying liabilities. --- ## Assets on Stellar: Explore Trustlines, Smart Contract Integration & More :::info The term "custom token" has been deprecated in favor of "contract token". View the conversation in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). ::: # Assets Accounts on the Stellar network can be used to track, hold, and transfer any type of asset. Assets can represent many things: cryptocurrencies (such as bitcoin or ether), fiat currencies (such as dollars or pesos), other tokens of value (such as NFTs), pool shares, or bonds and equity. :::note Assets exist in two forms on Stellar: "Classic" assets issued by Stellar accounts (`G...` addresses) and their built-in Stellar Asset Contract (SAC) implementation, and contract tokens issued by a deployed Wasm contract (`C...` addresses). Learn more about the differences in the [Assets and Tokens section](../../../tokens/README.mdx). ::: Classic assets on Stellar have two identifying characteristics: the asset code and the issuer. Since more than one organization can issue a credit representing the same asset, asset codes often overlap (for example, multiple companies offer a USD token on Stellar). Assets are uniquely identified by the combination of their asset code and issuer. ## Asset components ### Asset code An asset’s identifying code. There are three different formats: Alphanumeric 4, Alphanumeric 12, and liquidity pool shares. Learn about liquidity pool shares in the [Liquidity Pool section](../liquidity-on-stellar-sdex-liquidity-pools.mdx). Learn more about asset codes in the [Naming an Asset section](../../../tokens/control-asset-access.mdx#naming-an-asset) ### Issuer There is no dedicated operation to create an asset on Stellar. Instead, assets are created with a payment operation: an issuing account makes a payment using the asset it’s issuing, and that payment creates the asset on the network. The public key of the issuing account is linked on the ledger to the asset. Responsibility for and control over an asset resides with the issuing account. Since settings are stored at the account level on the ledger, the issuing account is where you use set_options operations to link to meta-information about an asset and set authorization flags. Learn how to issue an asset in the [Issuing Assets Tutorial](../../../tokens/how-to-issue-an-asset.mdx). ## Representation In Horizon, assets are represented in a JSON object: ```json5 { asset_code: "AstroDollar", asset_issuer: "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF", // `asset_type` is used to determine how asset data is stored. // It can be `native` (lumens), `credit_alphanum4`, or `credit_alphanum12`. asset_type: "credit_alphanum12", } ``` In the Stellar SDKs, they’re represented with the asset class: ```js var astroDollar = new StellarSdk.Asset( "AstroDollar", "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF", ); ``` ```java KeyPair issuer = KeyPair.fromAccountId("GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF"); Asset astroDollar = Asset.createNonNativeAsset("AstroDollar", issuer.getAccountId()); ``` ```python from stellar_sdk import Asset astro_dollar = Asset("AstroDollar", "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF") ``` ## Amount precision Each asset amount is encoded as a signed 64-bit integer in the XDR structures that Stellar uses to encode transactions. The asset amount unit seen by end-users is scaled down by a factor of ten million (10,000,000) to arrive at the native 64-bit integer representation. For example, the integer amount value 25,123,456 equals 2.5123456 units of the asset. This scaling allows for seven decimal places of precision in human-friendly amount units. The smallest non-zero amount unit, also known as a stroop, is 0.0000001 (one ten-millionth) represented as an integer value of one. The largest amount unit possible is $\frac{2^{63}-1}{10^7}$ (derived from the maximum 64-bit integer, scaled down) which is 922,337,203,685.4775807. The numbers are represented as int64s. Amount values are stored as only signed integers to avoid bugs that arise from mixing signed and unsigned integers. ## Relevance in Stellar Client Libraries In client-side libraries such as js-stellar-sdk, the integer encoded value is abstracted away. Many APIs expect an amount in unit value (the scaled-up amount displayed to end-users). Some programming languages (such as JavaScript) have problems maintaining precision on a number amount. It is recommended to use “big number” libraries that can record arbitrary-precision decimal numbers without a loss of precision. ## Deleting or burning assets To delete, or "burn", an asset, you must send it back to the account that issued it. ## Using Stellar assets in smart contracts Assets issued on the Stellar network are accessible to smart contracts. Every Stellar asset has reserved a Stellar Asset Contract (SAC) that can be deployed by anyone who wants to be able to interact with the asset from a contract. The Stellar CLI can deploy a Stellar Asset Contract for a Stellar asset. Deploying the Stellar Asset Contract for a Stellar asset enables that asset for use with smart contracts. Learn more in the [SAC section](../../../tokens/stellar-asset-contract.mdx). ## Token contracts Token contracts can be deployed on Stellar by deploying a contract that implements the [Token Interface](../../../tokens/token-interface.mdx), which is the same interface implemented by the [Stellar Asset Contract (SAC)](../../../tokens/stellar-asset-contract.mdx) for Stellar assets. --- ## Smart Contracts(Stellar-data-structures) A smart contract is a programmed set of executable code and state that can be invoked on the Stellar network. Smart contracts store data and also define rules for how that data can be used. Stellar has integrated a smart contracts platform called "Soroban" into the core protocol. These contracts are programs written in the Rust language and compiled as WebAssembly (Wasm) for deployment. Learn about Stellar smart contract concepts such as storage, authorization, debugging, and more in the [Smart Contract Learn Section](../contract-development/overview.mdx). Write your first Stellar smart contract by following the [Getting Started Guide](../../../build/smart-contracts/getting-started/README.mdx). --- ## Events(Stellar-data-structures) Events are the mechanism that applications off-chain can use to monitor movement of value of any Stellar operation, as well as custom events in contracts on-chain. ## How are events emitted? `ContractEvents` are emitted in Stellar Core's `TransactionMeta`. The location of events will depend on the version of `TransactionMeta` emitted. You can see in the [TransactionMetaV3] XDR below that for Soroban transactions, there is a `sorobanMeta` field containing `SorobanTransactionMeta` which includes both `events` (custom events from contracts) and `diagnosticEvents`. Note that `events` will only be populated if the transaction succeeds. [TransactionMetaV4] is more complex because it supports events for not only Soroban, but also classic operations, fees, and refunds. The top-level `events` vector is used for transaction level events, and currently contains `fee` events for both the initial fee charged and the refund (if applicable). Events tied to operations can be found under `OperationMetaV2`. [transactionmetav3]: #transactionmetav3 [transactionmetav4]: #transactionmetav4 ### ContractEvent An event's topics don't have to be made of the same type: you can mix different types. An event also contains a data object of any value or type, including [custom types](../contract-development/types/custom-types.mdx) defined by contracts using `#[contracttype]`: ```cpp struct ContractEvent { // We can use this to add more fields, or because it // is first, to change ContractEvent into a union. ExtensionPoint ext; ContractID* contractID; ContractEventType type; union switch (int v) { case 0: struct { SCVal topics<>; SCVal data; } v0; } body; }; ``` `ContractEvent` can be emitted in the following versions of `TransactionMeta` - ### TransactionMetaV3 ```cpp struct SorobanTransactionMeta { SorobanTransactionMetaExt ext; ContractEvent events<>; // custom events populated by the // contracts themselves. SCVal returnValue; // return value of the host fn invocation // Diagnostics events that are not hashed. // This will contain all contract and diagnostic events. Even ones // that were emitted in a failed contract call. DiagnosticEvent diagnosticEvents<>; }; struct TransactionMetaV3 { ExtensionPoint ext; LedgerEntryChanges txChangesBefore; // tx level changes before operations // are applied if any OperationMeta operations<>; // meta for each operation LedgerEntryChanges txChangesAfter; // tx level changes after operations are // applied if any SorobanTransactionMeta* sorobanMeta; // Soroban-specific meta (only for // Soroban transactions). }; ``` ### TransactionMetaV4 ```cpp struct OperationMetaV2 { ExtensionPoint ext; LedgerEntryChanges changes; ContractEvent events<>; }; // Transaction-level events happen at different stages of the ledger apply flow // (as opposed to the operation events that all happen atomically after // a transaction is applied). // This enum represents the possible stages during which an event has been // emitted. enum TransactionEventStage { // The event has happened before any one of the transactions has its // operations applied. TRANSACTION_EVENT_STAGE_BEFORE_ALL_TXS = 0, // The event has happened immediately after operations of the transaction // have been applied. TRANSACTION_EVENT_STAGE_AFTER_TX = 1, // The event has happened after every transaction had its operations // applied. TRANSACTION_EVENT_STAGE_AFTER_ALL_TXS = 2 }; // Represents a transaction-level event in metadata. // Currently this is limited to the fee events (when fee is charged or // refunded). struct TransactionEvent { TransactionEventStage stage; // Stage at which an event has occurred. ContractEvent event; // The contract event that has occurred. }; struct TransactionMetaV4 { ExtensionPoint ext; LedgerEntryChanges txChangesBefore; // tx level changes before operations // are applied if any OperationMetaV2 operations<>; // meta for each operation LedgerEntryChanges txChangesAfter; // tx level changes after operations are // applied if any SorobanTransactionMetaV2* sorobanMeta; // Soroban-specific meta (only for // Soroban transactions). TransactionEvent events<>; // Used for transaction-level events (like fee payment) DiagnosticEvent diagnosticEvents<>; // Used for all diagnostic information }; ``` [Link](https://github.com/stellar/stellar-xdr/blob/4b7a2ef7931ab2ca2499be68d849f38190b443ca/Stellar-ledger.x#L440-L522) to the XDR above. ### Event types There are three `ContractEventType`'s - 1. `CONTRACT` events are events emitted by contracts that use the `contract_event` host function to convey state changes. 2. `SYSTEM` events are events emitted by the host. At the moment, there's only one system event emitted by the host. It is emitted when the `update_current_contract_wasm` host function is called, where `topics = ["executable_update", old_executable: ContractExecutable, new_executable: ContractExecutable]` and `data = []`. 3. `DIAGNOSTIC` events are meant for debugging and will not be emitted unless the host instance explicitly enables it. You can read more about this below. ## What are diagnosticEvents? While looking at the xdr above, you may have noticed the `diagnosticEvents` field. This list will be empty by default unless your stellar-core instance has `ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true` in its config file. If diagnostic events are enabled, this list will include events from failed contract calls, errors from the host, events to trace the contract call stack, and logs from the `log_from_linear_memory` host function. These events can be identified by `type == DIAGNOSTIC`. If the transaction is for a soroban invocation, the list will also contain the non-diagnostic events emitted by the contract. The diagnostic events emitted by the host to track the call stack are defined below. ### fn_call The `fn_call` diagnostic event is emitted when a contract is called and contains - - Topics 1. The symbol "fn_call". 2. The contract id of the contract about to be called. 3. A symbol containing the name of the function being called. - Data 1. A vector of the arguments passed to the function being called. ### fn_return The `fn_return` diagnostic event is emitted when a contract call completes and contains - - Topics 1. The symbol "fn_return". 2. A symbol containing the name of the function that is about to return. - Data 1. The value returned by the contract function. ### When should diagnostic events be enabled? Regular `ContractEvents` should convey information about state changes. `diagnosticEvents` on the other hand contain events that are not useful for most users, but may be helpful in debugging issues or building the contract call stack. Because they won't be used by most users, they can be optionally enabled because they are not hashed into the ledger, and therefore are not part of the protocol. This is done so a stellar-core node can stay in sync with the network while emitting these events that normally would not be useful for most users. Due to the fact that a node with diagnostic events enabled will be executing code paths that diverge from a regular node, we highly encourage only using this feature on watcher node (nodes where `NODE_IS_VALIDATOR=false` is set). ## Tracking the movement of value Starting in protocol 23, classic operations can emit `transfer`, `mint`, `burn`, `clawback`, `fee`, and `set_authorized` events so that the movement of assets and trusline updates can be tracked using a single stream of data. These events will be emitted if a node has `EMIT_CLASSIC_EVENTS=true` set. If `BACKFILL_STELLAR_ASSET_EVENTS=true` is also set, then events will be emitted for any ledger, regardless of protocol version. ## Reading events You can use the [`getEvents`](../../../data/apis/rpc/api-reference/methods/getEvents.mdx) endpoint of any RPC service to fetch and filter events by type, contract, and topic. :::warning Events are ephemeral: RPC providers typically only keep short chunks (less than a week) of history around. ::: To learn more about working with events, take a look at the [events guides](../../../build/guides/events/README.mdx) and [this example contract](../../../build/smart-contracts/example-contracts/events.mdx). For a quick high-level demonstration, though, we'll use the [TypeScript SDK](../../../tools/sdks/README.mdx) to infinitely fetch all `transfer` events (defined by the [Soroban Token Interface](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md#interface)) involving the [XLM contract](https://stellar.expert/explorer/testnet/contract/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC) and display them in a human-friendly format. ```javascript humanizeEvents, nativeToScVal, scValToNative, Address, Networks, Asset, xdr, } from "@stellar/stellar-sdk"; const s = new Server("https://soroban-testnet.stellar.org"); async function main() { const response = await s.getLatestLedger(); const xlmFilter = { type: "contract", contractIds: [Asset.native().contractId(Networks.TESTNET)], topics: [ // Defined in https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md#interface // for all compatible transfer events. [ nativeToScVal("transfer", { type: "symbol" }).toXDR("base64"), "*", // from anyone "*", // to anyone "*", // any asset (it'll be XLM anyway) ], ], }; let page = await s.getEvents({ startLedger: response.sequence - 120, // start ~10m in the past filters: [xlmFilter], limit: 10, }); // Run forever until Ctrl+C'd by user while (true) { if (!page.events.length) { await new Promise((r) => setTimeout(r, 2000)); } else { // // Two ways to output a human-friendly version: // 1. the RPC response itself for human-readable text // 2. a helper for the XDR structured-equivalent for human-readable JSON // console.log(cereal(simpleEventLog(page.events))); console.log(cereal(fullEventLog(page.events))); } // Fetch the next page until events are exhausted, then wait. page = await s.getEvents({ filters: [xlmFilter], cursor: page.cursor, limit: 10, }); } } function simpleEventLog(events) { return events.map((event) => { return { topics: event.topic.map((t) => scValToNative(t)), value: scValToNative(event.value), }; }); } function fullEventLog(events) { return humanizeEvents( events.map((event) => { // rebuild the decomposed response into its original XDR structure return new xdr.ContractEvent({ contractId: event.contractId.address().toBuffer(), type: xdr.ContractEventType.contract(), // since we filtered on 'contract' body: new xdr.ContractEventBody( 0, new xdr.ContractEventV0({ topics: event.topic, data: event.value, }), ), }); }), ); } // A custom JSONification method to handle bigints. function cereal(data) { return JSON.stringify( data, (k, v) => (typeof v === "bigint" ? v.toString() : v), 2, ); } main().catch((e) => console.error(e)); ``` You can also leverage RPC's alternate [XDR encoding formats](../../../data/apis/rpc/api-reference/structure/data-format) like JSON to see human-readable events from the command-line directly, for example by passing `xdrFormat: "json"` as an additional parameter to the `getEvents` [example](../../../data/apis/rpc/api-reference/methods/getEvents#examples). --- ## Ledgers Store Accounts, Balances, Orders, Smart Contract Data & More # Ledgers A ledger represents the state of the Stellar network at a point in time. It is shared across all Core nodes in the network and contains the list of accounts and balances, orders on the distributed exchange, smart contract data, and any other persisting data. :::note Blockchains typically refer to the **ledger** as the entire record of all transactions on the blockchain and **blocks** as individual units of data that contain a collection of transactions. In Stellar, "ledger" can refer to both. ::: In every Stellar Consensus Protocol round, the network reaches consensus on which transaction set to apply to the last closed ledger, and when the new set is applied, a new “last closed ledger” is defined. Each ledger is cryptographically linked to the unique previous ledger, creating a historical chain that goes back to the genesis ledger. Data is stored on the ledger as ledger entries. Possible ledger entries include: - [Accounts](./accounts.mdx) - [Claimable balances](../../../build/guides/transactions/claimable-balances.mdx) - [Liquidity pools](../../fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx) - [Contract data](../../fundamentals/contract-development/storage/persisting-data.mdx#ledger-entries) ## Ledger headers Every ledger has a header that references the data in that ledger and the previous ledger. These references are cryptographic hashes of the content which behave like pointers in typical data structures but with added security guarantees. Think of a historical ledger chain as a linked list of ledger headers. Time flows forward from left to right, hashes point backwards in time, from right to left. Each hash in the chain links a ledger to its previous ledger, which authenticates the entire history of ledgers in its past: ```mermaid flowchart RL subgraph genesis["Genesis"] direction LR prev1["Prev: none"] state1["Genesis state"] end subgraph block2["Ledger 2"] prev2["Prev: hash(Genesis)"] state2["Ledger 2transactionsand state"] end subgraph block3["Ledger 3"] prev3["Prev: hash(Ledger 2)"] state3["Ledger 3transactionsand state"] end subgraph dotdot["..."] end subgraph blockn["Ledger N"] prevn["Prev: hash(Ledger N-1)"] staten["Ledger Ntransactionsand state"] end genesis ~~~ block2 ~~~ block3 ~~~ dotdot ~~~ blockn prev2 --> genesis prev3 --> block2 dotdot --> block3 prevn --> dotdot ``` The genesis ledger has a sequence number of 1. The ledger directly following a ledger with sequence number `N` has a sequence number of `N+1`. Ledger `N+1` contains a hash of ledger `N` in its previous ledger field. ## Ledger header fields ### Version The protocol version of this ledger. ### Previous ledger hash Hash of the previous ledger. ### SCP value During consensus, all the validating nodes in the network run SCP and agree on a particular value, which is a transaction set they will apply to a ledger. This value is stored here and in the following three fields (transaction set hash, close time, and upgrades). ### Transaction set hash Hash of the transaction set applied to the previous ledger. ### Close time The close time is a UNIX timestamp indicating when the ledger closes. Its accuracy depends on the system clock of the validator proposing the block. Consequently, SCP may confirm a close time that lags a few seconds behind or up to 60 seconds ahead. It's strictly monotonic – guaranteed to be greater than the close time of an earlier ledger. ### Upgrades How the network adjusts overall values (like the base fee) and agrees to network-wide changes (like switching to a new protocol version). This field is usually empty. When there is a network-wide upgrade, the SDF will inform and help coordinate participants using the #validator channel on the Dev Discord and the Stellar Validators Google Group. ### Transaction set result hash Hash of the results of applying the transaction set. This data is not necessary for validating the results of the transactions. However, it makes it easier for entities to validate the result of a given transaction without having to apply the transaction set to the previous ledger. ### Bucket list hash Hash of all the objects in this ledger. The data structure that contains all the objects is called the bucket list. ### Ledger sequence The sequence number of this ledger. ### Total coins Total number of lumens in existence. ### Fee pool Number of lumens that have been paid in fees. Note this is denominated in lumens, even though a transaction’s fee field is in stroops. ### Inflation sequence Number of times inflation has been run. Note: the inflation operation was deprecated when validators voted to upgrade the network to Protocol 12 on 10/28/2019. Therefore, inflation no longer runs, so this sequence number no longer changes. ### ID pool The last used global ID. These IDs are used for generating objects. ### Maximum number of transactions The maximum number of operations validators have agreed to process in a given ledger. If more transactions are submitted than this number, the network will enter into surge pricing mode. For more about surge pricing and fee strategies, see our [Fees section](../../fundamentals/fees-resource-limits-metering.mdx). ### Base fee The fee the network charges per operation in a transaction. Calculated in stroops. See the [Fees section](../../fundamentals/fees-resource-limits-metering.mdx) for more information. ### Base reserve The reserve the network uses when calculating an account’s minimum balance. ### Skip list Hashes of ledgers in the past. Intended to accelerate access to past ledgers without walking back ledger by ledger. Currently unused. --- ## Stellar Ecosystem Proposals (SEPs): Standards for Interoperability & Development # Stellar Ecosystem Proposals (SEPs) Each SEP is a distinct blueprint meant to help users build a product or service that interoperates with other products and services on the Stellar network. :::note This page covers Stellar Ecosystem Proposals (SEPs), which define standards and protocols for projects building on the Stellar network. SEPs differ from Core Advancement Proposals (CAPs), which propose changes to the Stellar network’s core protocol. You can learn more about CAPs on [GitHub](https://github.com/stellar/stellar-protocol/tree/master/core). ::: When you build on Stellar, you generally use the Stellar RPC API to interact with the network. However, anytime you want your product or service to interoperate with other products or services in the ecosystem, you must create additional infrastructure to handle those components of an interaction. SEPs define standards for building that infrastructure on top of the Stellar network. They are designed to help different entities, such as asset issuers, wallets, exchanges, and other service providers interoperate using a single common integration. Generally, they define two sides of an interaction — often a server-side and a client-side — and using them as a blueprint allows you to connect to multiple counterparties without starting from scratch every time. SEPs are publicly-created, open-source documents that live in the [GitHub repository](https://github.com/stellar/stellar-protocol/tree/master/ecosystem) and have a lightweight approval process. New SEPs and upgrades are discussed constantly. We encourage participation in these discussions to help build new standards and make Stellar services more accessible. ## Notable SEPs There are many SEPs, and they cover a wide variety of standards for interoperation (see the full list of active SEPs [below](#complete-list-of-active-proposals)). Whatever you're building, you may want to take a look at the complete list to see if there's a standard for your use case. This section will cover a few notable SEPs that define the standards for some common Stellar use cases. ### SEP-1 - Stellar Info File Defines how to create and host a stellar.toml file: a common place where the Internet can find information about your Stellar integration. You can store a lot of information in your stellar.toml file including organization information, currency information, and contact information. TOML is a simple and commonly used configuration file format designed to be readable by both humans and machines. Using the `set_options` operation, you can link your Stellar account to the domain that hosts your stellar.toml, creating an on-chain connection between this information and that account. **Used by anchors, issuers, and validators.** [Link to GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) ### SEP-5 - Key Derivation Methods for Stellar Accounts Describes methods for key derivation for Stellar, improving key storage and moving keys between wallets and applications. Guidance in this SEP improves the Stellar ecosystem by: - Making key derivation the same across wallets and applications - Allowing users to hold keys in hardware wallets - Allowing users to hold keys in cold storage more reliably (using mnemonic codes) - Allowing users to generate multiple keys from a single seed (for example, first for storing funds and second as a signer for a shared account) **Used by wallets and other applications.** [Link to GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0005.md) ### SEP-6 - Deposit and Withdrawal API Defines the standard way for anchors and wallets to interact on behalf of users. With this SEP’s guidance, wallets and other clients can interact with anchors directly without the user needing to leave the wallet to go to the anchor’s site. This SEP defines a standard protocol enabling the following features within a wallet or other Stellar client: - Deposit external assets with an anchor - Withdraw assets from an anchor - Execute deposit/withdrawal between non-equivalent assets - Communicate deposit & withdrawal fee structure for an anchor to the user - Handle anchor KYC needs, including transmitting KYC information about the user to the anchor via SEP-12 - Check the status of ongoing deposits or withdrawals involving the user - View history of deposits and withdrawals involving the user SEP-24 is the alternative to SEP-6 which supports hosted deposits and withdrawals. **Used by anchors, wallets, and other applications.** [Link to GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md) ### SEP-7 - URI Scheme to Facilitate Delegated Signing Defines the standard URI scheme that can be used to generate a URI that will serve as a request to sign a transaction. With this SEP’s guidance, non-wallet applications can have their their users sign a transaction without seeing the wallet user's secret key in any form since the URI (request) will typically be signed by the user’s trusted wallet where the secret keys are stored. This SEP defines a standard protocol enabling the following features within a wallet or other Stellar client: - Deeplinks payments (online) - QR code payments (online and offline) - Point of sale transactions (offline) - Peer to peer payments (online and offline) **Used by wallets and other applications.** [Link to GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md) ### SEP-10 - Stellar Authentication Defines a standard way for clients (such as wallets or exchanges) to create authenticated web sessions for users holding a Stellar account. This SEP also supports authenticating users of shared or pooled Stellar accounts. Clients can use muxed accounts to distinguish users or sub-accounts of shared accounts. Proves that the user has a Stellar account and that they control the account with a single master key or sufficient signers needed. **Used by wallets and exchanges.** [Link to GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) ### SEP-12 - KYC API Allows for sharing of KYC data and defines a standard way for Stellar clients to upload KYC and other information to anchors and other services. This SEP was made with these goals in mind: - Allow a customer to enter their KYC information into their wallet once and use it across many services without re-entering information manually - Handle image and binary data - Support the set of fields defined in SEP-9 - Support authentication via SEP-10 - Support the provision of data for SEP-6, SEP-24, SEP-31, and others - Give customers control over their data by supporting complete data erasure **Used by anchors, wallets, and other applications.** [Link to GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) ### SEP-20 - Self-Verification of Validator Nodes Defines how validators self-verify by setting the home domain of their Stellar account to their website, where they publish information on-chain about their node and organization in a stellar.toml file. This allows other participants to discover other nodes and add them to their quorum sets without needing a centralized database. **Used by validators.** [Link to GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md) ### SEP-24 - Hosted Deposit and Withdrawal Defines the standard way for anchors and wallets to interact on behalf of users interactively. This means that the user’s application must open a webview hosted by a third-party anchor for the user to provide the information necessary to complete the transaction. Users use applications that implement SEP-24 to connect to businesses that will accept off-chain value (such as USD) in exchange for on-chain value (such as USDC) and vice-versa. SEP-6 is the alternative to SEP-24 that supports an API-style solution for the same use case. **Used by anchors, wallets, and other applications.** [Link to GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md) ### SEP-30 - Account Recovery: Multi-Party Recovery of Stellar Accounts Defines the standard API that enables an individual (e.g., a user or wallet) to regain access to a Stellar account that it owns after the individual has lost its private key without providing any third-party control of the account. Using this protocol, the user or wallet will preregister the account and a phone number, email, or other form of authentication with one or more servers implementing the protocol and add those servers as signers of the account. If two or more servers are used with appropriate signer configuration no individual server will have control of the account, but collectively, they may help the individual recover access to the account. The protocol also enables individuals to pass control of a Stellar account to another individual. This SEP enables the following use cases for a user: - Recover: Recover access to Stellar accounts for which they may have lost keys. - Share: Gain access to Stellar accounts that another user intends to share with them. **Used by wallets and other applications.** [Link to GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0030.md) ### SEP-31 - Cross-Border Payment API Defines the protocol for two financial accounts that exist outside the Stellar network (anchors) to interact with each other. **Used by anchors.** [Link to GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md) ## Complete list of active proposals | Number | Title | Track | | --- | --- | --- | | [SEP-1](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) | Stellar Info File | Standard | | [SEP-2](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0002.md) | Federation Protocol | Standard | | [SEP-4](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0004.md) | Tx Status Endpoint | Standard | | [SEP-5](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0005.md) | Key Derivation Methods for Stellar Accounts | Standard | | [SEP-6](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md) | Deposit and Withdrawal API | Standard | | [SEP-7](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md) | URI Scheme to facilitate delegated signing | Standard | | [SEP-8](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0008.md) | Regulated Assets | Standard | | [SEP-9](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md) | Standard KYC Fields | Standard | | [SEP-10](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) | Stellar Authentication | Standard | | [SEP-11](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0011.md) | Txrep: Human-Readable Low-Level Representation of Stellar Transactions | Standard | | [SEP-12](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) | KYC API | Standard | | [SEP-14](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0014.md) | Dynamic Asset Metadata | Standard | | [SEP-18](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0018.md) | Data Entry Namespaces | Standard | | [SEP-20](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md) | Self-verification of validator nodes | Standard | | [SEP-23](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md) | Muxed Account Strkeys | Standard | | [SEP-24](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md) | Hosted Deposit and Withdrawal | Standard | | [SEP-28](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0028.md) | XDR Base64 Encoding | Standard | | [SEP-29](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0029.md) | Account Memo Requirements | Standard | | [SEP-31](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md) | Cross-Border Payments API | Standard | | [SEP-33](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0033.md) | Identicons for Stellar Accounts | Standard | | [SEP-46](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0046.md) | Contract Meta | Standard | | [SEP-48](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0048.md) | Contract Interface Specification | Standard | | [SEP-50](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0050.md) | Non-Fungible Tokens | Standard | --- ## Learn About the Core Components and Architecture of the Stellar Network # Stellar Stack The Stellar stack is made up of the following components: the networks (Mainnet, Testnet, and Futurenet), Stellar Core, Stellar RPC, and SDKs, each of which plays a specific part in providing financial infrastructure that is resilient to failures, available to anyone, and fast and cheap enough to serve real-world use cases. ![Stellar Stack](/assets/diagrams/stellar-tech-stack.png) ## Networks Stellar has three networks: the public network (Mainnet, also called Pubnet or the Public Network), the test network (Testnet), and a dev network (Futurenet). Mainnet is the main network used by applications in production. The Testnet is a smaller, free-to-use network maintained by SDF that functions like the Mainnet but doesn’t connect to real money and is the best place for developers to test their applications. Futurenet is a dev network you can use to test more bleeding edge features. Read more about the different networks in the [Networks section](../../networks/README.mdx). ## Stellar Core Stellar Core is the program used by the individual nodes (or computers) that make up the network. Stellar Core keeps a common distributed ledger and engages in consensus to validate and process transactions. Generally, nodes reach consensus, apply a transaction set, and update the ledger every 5-7 seconds. Nodes reach consensus using the Stellar Consensus Protocol, which can you can learn more about here: [Stellar Consensus Protocol](./stellar-consensus-protocol.mdx) Anyone can run a Stellar Core node, but you don’t have to in order to build on Stellar. We recommend you do so if you issue an asset and want to ensure the accuracy of the ledger, if you want to participate in network governance by voting on protocol version, minimum fees, and resource and ledger limits, and/or if you want to contribute to Stellar’s overall health and decentralization. Check out our tutorial on installing, configuring, and maintaining your own node here: [Run a Validator Node Tutorial](../../validators/README.mdx). ## RPC Stellar's RPC is a JSON RPC server that provides an interface for users and applications to interact with smart contracts on the Stellar blockchain. When an application would like to interact with smart contracts, it sends a request to the RPC server. The server interprets these requests, translates them into a format understandable by the blockchain nodes, and forwards them. After processing the requests, the blockchain nodes send back the results. The RPC server receives these results and sends them back to the requesting application. SDF has RPC endpoints available for Futurenet and Testnet. These services are free to use, and are suitable for development and testing. SDF does not provide a publicly available RPC endpoint for Mainnet. Developers should [select an ecosystem provider](../../data/apis/rpc/providers.mdx) that works for their project before migrating to Mainnet. In some cases, projects may choose to run their own RPC instance. ## Horizon :::warning Horizon is nearing end-of-life and will eventually be deprecated in favor of Stellar RPC and [Portfolio APIs](../../data/indexers/README.mdx#portfolio-apis). While it will continue to receive updates to maintain compatibility with upcoming protocol releases, it won't receive new feature development. ::: Horizon is the client-facing RESTful HTTP API server in the platform layer which allows programmatic access to submit transactions and query the network’s historical data. It acts as the interface for applications that want to access the Stellar network. You can communicate with Horizon using an SDK, a web browser, or with simple command tools like cURL. ## SDKs SDKs simplify some of the work of accessing Horizon and the Stellar RPC by converting the data into friendlier formats and allowing you to program in the language of your choice. Stellar’s SDKs show you how to request data and create and submit transactions. Soroban's SDKs allow you to write smart contracts in Rust and interact with smart contracts in a myriad of other languages. View Stellar's [SDK library](../../tools/sdks/README.mdx) to access our SDKs and their documentation. --- ## Learn About Transactions on Stellar: Lifecycle, Operations & More # Operations & Transactions --- ## Explore Stellar Operations: Accounts, Payments, Trustlines & More # List of Operations Operations are objects that represent a desired change to the ledger and are submitted to the network grouped in a transaction. For each operation, there is a successful or failed result type. In the case of success, the user can gather information about the effect of the operation. In the case of failure, the user can learn more about the error. Learn more about transactions and operations in our [Operations and Transactions section](./operations-and-transactions.mdx). There are currently 26 operations you can use on the Stellar network, these operations, their definitions, SDKs, thresholds, parameters, and errors are listed below. :::note All these operations have an optional source account parameter. If the source account parameter is omitted, the source account of the transaction is considered as the source account of the operation. ::: ## Create account Creates and funds a new account with the specified starting balance **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.createAccount) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/CreateAccountOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#CreateAccount) **Threshold**: Medium **Result**: `CreateAccountResult` **Parameters**: | Parameter | Type | Description | | --- | --- | --- | | Destination | account ID | The account address to be created and funded by this operation. | | Starting Balance | integer | Amount of XLM to send to the newly created account. This XLM comes from the source account. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `CREATE_ACCOUNT_MALFORMED` | -1 | The `destination` is invalid. | | `CREATE_ACCOUNT_UNDERFUNDED` | -2 | The source account performing the command does not have enough funds to give `destination` the `starting balance` amount of XLM and still maintain its minimum XLM reserve plus satisfy its XLM selling liabilities. | | `CREATE_ACCOUNT_LOW_RESERVE` | -3 | This operation would create an account with fewer than the minimum number of XLM an account must hold. | | `CREATE_ACCOUNT_ALREADY_EXIST` | -4 | The `destination` account already exists. | ## Payment Sends an amount in a specific asset to a destination account **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.payment) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/PaymentOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#Payment) **Threshold**: Medium **Result**: `PaymentResult` **Parameters**: | Parameters | Type | Description | | ----------- | ---------- | ------------------------------------------- | | Destination | account ID | Account address that receives the payment. | | Asset | asset | Asset to send to the destination account. | | Amount | integer | Amount of the aforementioned asset to send. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `PAYMENT_MALFORMED` | -1 | The input to the payment is invalid. | | `PAYMENT_UNDERFUNDED` | -2 | The source account (sender) does not have enough funds to send `amount` and still satisfy its selling liabilities. Note that if sending XLM, the sender must additionally maintain its minimum XLM reserve. | | `PAYMENT_SRC_NO_TRUST` | -3 | The source account does not trust the issuer of the asset it is trying to send. | | `PAYMENT_SRC_NOT_AUTHORIZED` | -4 | The source account is not authorized to send this payment. | | `PAYMENT_NO_DESTINATION` | -5 | The receiving account does not exist. Note that this error will **not** be returned if the receiving account is the issuer of `asset` (i.e, when the asset is being _burned_). | | `PAYMENT_NO_TRUST` | -6 | The receiver does not trust the issuer of the asset being sent. For more information, see the [Assets section](../stellar-data-structures/assets.mdx). | | `PAYMENT_NOT_AUTHORIZED` | -7 | The destination account is not authorized by the asset's issuer to hold the asset. | | `PAYMENT_LINE_FULL` | -8 | The destination account (receiver) does not have sufficient limits to receive `amount` and still satisfy its buying liabilities. | ## Path payment strict send A payment where the asset sent can be different than the asset received; allows the user to specify the amount of the asset to send Learn more about path payments: [Path Payments Guide](../../../build/guides/transactions/path-payments.mdx) **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.pathPaymentStrictSend) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/PathPaymentStrictSendOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#PathPaymentStrictSend) **Threshold**: Medium **Result**: `PathPaymentStrictSendResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Send asset | asset | The asset deducted from the sender's account. | | Send amount | integer | The amount of `send asset` to deduct (excluding fees). | | Destination | account ID | Account ID of the recipient. | | Destination asset | asset | The asset the destination account receives. | | Destination min | integer | The minimum amount of `destination asset` the destination account can receive. | | Path | list of assets | The assets (other than `send asset` and `destination asset`) involved in the offers the path takes. For example, if you can only find a path from USD to EUR through XLM and BTC, the path would be USD -> XLM -> BTC -> EUR and the `path` field would contain XLM and BTC. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `PATH_PAYMENT_STRICT_SEND_MALFORMED` | -1 | The input to this path payment is invalid. | | `PATH_PAYMENT_STRICT_SEND_UNDERFUNDED` | -2 | The source account (sender) does not have enough funds to send and still satisfy its selling liabilities. Note that if sending XLM, the sender must additionally maintain its minimum XLM reserve. | | `PATH_PAYMENT_STRICT_SEND_SRC_NO_TRUST` | -3 | The source account does not trust the issuer of the asset it is trying to send. | | `PATH_PAYMENT_STRICT_SEND_SRC_NOT_AUTHORIZED` | -4 | The source account is not authorized to send this payment. | | `PATH_PAYMENT_STRICT_SEND_NO_DESTINATION` | -5 | The destination account does not exist. | | `PATH_PAYMENT_STRICT_SEND_NO_TRUST` | -6 | The destination account does not trust the issuer of the asset being sent. For more, see the [Assets section](../stellar-data-structures/assets.mdx). | | `PATH_PAYMENT_STRICT_SEND_NOT_AUTHORIZED` | -7 | The destination account is not authorized by the asset's issuer to hold the asset. | | `PATH_PAYMENT_STRICT_SEND_LINE_FULL` | -8 | The destination account does not have sufficient limits to receive `destination amount` and still satisfy its buying liabilities. | | `PATH_PAYMENT_STRICT_SEND_TOO_FEW_OFFERS` | -10 | There is no path of offers connecting the `send asset` and `destination asset`. Stellar only considers paths of length 5 or shorter. | | `PATH_PAYMENT_STRICT_SEND_OFFER_CROSS_SELF` | -11 | The payment would cross one of its own offers. | | `PATH_PAYMENT_STRICT_SEND_UNDER_DESTMIN` | -12 | The paths that could send `destination amount` of `destination asset` would fall short of `destination min`. | ## Path payment strict receive A payment where the asset received can be different from the asset sent; allows the user to specify the amount of the asset received Learn more about path payments: [Path Payments Guide](../../../build/guides/transactions/path-payments.mdx) **SDKs**: [JavaScript](https://stellar.github.io/js-stellar-sdk/Operation.html#.pathPaymentStrictReceive) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/PathPaymentStrictReceiveOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#PathPaymentStrictReceive) **Threshold**: Medium **Result**: `PathPaymentStrictReceiveResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Send asset | asset | The asset deducted from the sender's account. | | Send max | integer | The maximum amount of `send asset` to deduct (excluding fees). | | Destination | account ID | Account ID of the recipient. | | Destination asset | asset | The asset the destination account receives. | | Destination amount | integer | The amount of `destination asset` the destination account receives. | | Path | list of assets | The assets (other than `send asset` and `destination asset`) involved in the offers the path takes. For example, if you can only find a path from USD to EUR through XLM and BTC, the path would be USD -> XLM -> BTC -> EUR and the `path` field would contain XLM and BTC. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `PATH_PAYMENT_STRICT_RECEIVE_MALFORMED` | -1 | The input to this path payment is invalid. | | `PATH_PAYMENT_STRICT_RECEIVE_UNDERFUNDED` | -2 | The source account (sender) does not have enough funds to send and still satisfy its selling liabilities. Note that if sending XLM, the the sender must additionally maintain its minimum XLM reserve. | | `PATH_PAYMENT_STRICT_RECEIVE_SRC_NO_TRUST` | -3 | The source account does not trust the issuer of the asset it is trying to send. | | `PATH_PAYMENT_STRICT_RECEIVE_SRC_NOT_AUTHORIZED` | -4 | The source account is not authorized to send this payment. | | `PATH_PAYMENT_STRICT_RECEIVE_NO_DESTINATION` | -5 | The destination account does not exist. | | `PATH_PAYMENT_STRICT_RECEIVE_NO_TRUST` | -6 | The destination account does not trust the issuer of the asset being sent. For more, see the [Assets section](../stellar-data-structures/assets.mdx). | | `PATH_PAYMENT_STRICT_RECEIVE_NOT_AUTHORIZED` | -7 | The destination account is not authorized by the asset's issuer to hold the asset. | | `PATH_PAYMENT_STRICT_RECEIVE_LINE_FULL` | -8 | The destination account does not have sufficient limits to receive `destination amount` and still satisfy its buying liabilities. | | `PATH_PAYMENT_STRICT_RECEIVE_TOO_FEW_OFFERS` | -10 | There is no path of offers connecting the `send asset` and `destination asset`. Stellar only considers paths of length 5 or shorter. | | `PATH_PAYMENT_STRICT_RECEIVE_OFFER_CROSS_SELF` | -11 | The payment would cross one of its own offers. | | `PATH_PAYMENT_STRICT_RECEIVE_OVER_SENDMAX` | -12 | The paths that could send `destination amount` of `destination asset` would exceed `send max`. | ## Manage buy offer Creates, updates, or deletes an offer to buy a specific amount of an asset for another Learn more about passive sell offers: [Liquidity on Stellar: SDEX and Liquidity Pools](../liquidity-on-stellar-sdex-liquidity-pools.mdx) **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.manageBuyOffer) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ManageBuyOfferOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#ManageBuyOffer) **Threshold**: Medium **Result**: `ManageBuyOfferResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Selling | asset | Asset the offer creator is selling. | | Buying | asset | Asset the offer creator is buying. | | Amount | integer | Amount of `buying` being bought. Set to `0` if you want to delete an existing offer. | | Price | \{numerator, denominator} | Price of 1 unit of `buying` in terms of `selling`. For example, if you wanted to buy 30 XLM and sell 5 BTC, the price would be \{5,30}. | | Offer ID | unsigned integer | The ID of the offer. `0` for new offer. Set to existing offer ID to update or delete. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `MANAGE_BUY_OFFER_MALFORMED` | -1 | The input is incorrect and would result in an invalid offer. | | `MANAGE_BUY_OFFER_SELL_NO_TRUST` | -2 | The account creating the offer does not have a trustline for the asset it is selling. | | `MANAGE_BUY_OFFER_BUY_NO_TRUST` | -3 | The account creating the offer does not have a trustline for the asset it is buying. | | `MANAGE_BUY_OFFER_BUY_NOT_AUTHORIZED` | -4 | The account creating the offer is not authorized to sell this asset. | | `MANAGE_BUY_OFFER_SELL_NOT_AUTHORIZED` | -5 | The account creating the offer is not authorized to buy this asset. | | `MANAGE_BUY_OFFER_LINE_FULL` | -6 | The account creating the offer does not have sufficient limits to receive `buying` and still satisfy its buying liabilities. | | `MANAGE_BUY_OFFER_UNDERFUNDED` | -7 | The account creating the offer does not have sufficient limits to send `selling` and still satisfy its selling liabilities. Note that if selling XLM then the account must additionally maintain its minimum XLM reserve, which is calculated assuming this offer will not completely execute immediately. | | `MANAGE_BUY_OFFER_CROSS_SELF` | -8 | The account has opposite offer of equal or lesser price active, so the account creating this offer would immediately cross itself. | | `MANAGE_BUY_OFFER_NOT_FOUND` | -11 | An offer with that `offerID` cannot be found. | | `MANAGE_BUY_OFFER_LOW_RESERVE` | -12 | The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. | ## Manage sell offer Creates, updates, or deletes an offer to sell a specific amount of an asset for another Learn more about passive sell offers: [Liquidity on Stellar: SDEX and Liquidity Pools](../liquidity-on-stellar-sdex-liquidity-pools.mdx) **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.manageSellOffer) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ManageSellOfferOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#ManageSellOffer) **Threshold**: Medium **Result**: `ManageSellOfferResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Selling | asset | Asset the offer creator is selling. | | Buying | asset | Asset the offer creator is buying. | | Amount | integer | Amount of `selling` being sold. Set to `0` if you want to delete an existing offer. | | Price | \{numerator, denominator} | Price of 1 unit of `selling` in terms of `buying`. For example, if you wanted to sell 30 XLM and buy 5 BTC, the price would be \{5,30}. | | Offer ID | unsigned integer | The ID of the offer. `0` for new offer. Set to existing offer ID to update or delete. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `MANAGE_SELL_OFFER_MALFORMED` | -1 | The input is incorrect and would result in an invalid offer. | | `MANAGE_SELL_OFFER_SELL_NO_TRUST` | -2 | The account creating the offer does not have a trustline for the asset it is selling. | | `MANAGE_SELL_OFFER_BUY_NO_TRUST` | -3 | The account creating the offer does not have a trustline for the asset it is buying. | | `MANAGE_SELL_OFFER_SELL_NOT_AUTHORIZED` | -4 | The account creating the offer is not authorized to sell this asset. | | `MANAGE_SELL_OFFER_BUY_NOT_AUTHORIZED` | -5 | The account creating the offer is not authorized to buy this asset. | | `MANAGE_SELL_OFFER_LINE_FULL` | -6 | The account creating the offer does not have sufficient limits to receive `buying` and still satisfy its buying liabilities. | | `MANAGE_SELL_OFFER_UNDERFUNDED` | -7 | The account creating the offer does not have sufficient limits to send `selling` and still satisfy its selling liabilities. Note that if selling XLM then the account must additionally maintain its minimum XLM reserve, which is calculated assuming this offer will not completely execute immediately. | | `MANAGE_SELL_OFFER_CROSS_SELF` | -8 | The account has opposite offer of equal or lesser price active, so the account creating this offer would immediately cross itself. | | `MANAGE_SELL_OFFER_NOT_FOUND` | -11 | An offer with that `offerID` cannot be found. | | `MANAGE_SELL_OFFER_LOW_RESERVE` | -12 | The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. | ## Create passive sell offer Creates an offer to sell one asset for another without taking a reverse offer of equal price Learn more about passive sell offers: [Liquidity on Stellar: SDEX and Liquidity Pools](../liquidity-on-stellar-sdex-liquidity-pools.mdx) **SDKs**: [JavaScript](https://stellar.github.io/js-stellar-sdk/Operation.html#.createPassiveSellOffer) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/CreatePassiveSellOfferOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#CreatePassiveSellOffer) **Threshold**: Medium **Result**: `ManageSellOfferResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Selling | asset | Asset the offer creator is selling. | | Buying | asset | Asset the offer creator is buying. | | Amount | integer | Amount of `selling` being sold. | | Price | \{numerator, denominator} | Price of 1 unit of `selling` in terms of `buying`. For example, if you wanted to sell 30 XLM and buy 5 BTC, the price would be \{5,30}. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `MANAGE_SELL_OFFER_MALFORMED` | -1 | The input is incorrect and would result in an invalid offer. | | `MANAGE_SELL_OFFER_SELL_NO_TRUST` | -2 | The account creating the offer does not have a trustline for the asset it is selling. | | `MANAGE_SELL_OFFER_BUY_NO_TRUST` | -3 | The account creating the offer does not have a trustline for the asset it is buying. | | `MANAGE_SELL_OFFER_SELL_NOT_AUTHORIZED` | -4 | The account creating the offer is not authorized to sell this asset. | | `MANAGE_SELL_OFFER_BUY_NOT_AUTHORIZED` | -5 | The account creating the offer is not authorized to buy this asset. | | `MANAGE_SELL_OFFER_LINE_FULL` | -6 | The account creating the offer does not have sufficient limits to receive `buying` and still satisfy its buying liabilities. | | `MANAGE_SELL_OFFER_UNDERFUNDED` | -7 | The account creating the offer does not have sufficient limits to send `selling` and still satisfy its selling liabilities. Note that if selling XLM then the account must additionally maintain its minimum XLM reserve, which is calculated assuming this offer will not completely execute immediately. | | `MANAGE_SELL_OFFER_CROSS_SELF` | -8 | The account has opposite offer of equal or lesser price active, so the account creating this offer would immediately cross itself. | | `MANAGE_SELL_OFFER_NOT_FOUND` | -11 | An offer with that `offerID` cannot be found. | | `MANAGE_SELL_OFFER_LOW_RESERVE` | -12 | The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. | ## Set options Set options for an account such as flags, inflation destination, signers, home domain, and master key weight Learn more about flags: [Flags Section](../../../tokens/control-asset-access.mdx#controlling-access-to-an-asset-with-flags) Learn more about the home domain: [Stellar Ecosystem Proposals SEP-1](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) Learn more about signers operations and key weight: [Signature and Multisignature Section](../../fundamentals/transactions/signatures-multisig.mdx) **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.setOptions) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/SetOptionsOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#SetOptions) **Threshold**: High (when updating signers or other thresholds) or Medium (when updating everything else) **Result**: `SetOptionsResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Inflation Destination | account ID | Account of the inflation destination. | | Clear flags | integer | Indicates which [account](../stellar-data-structures/accounts.mdx) flags to clear. These account-level flags are primarily used by asset issuers. For details about the flags, please refer to the [Asset Design Considerations page](../../../tokens/control-asset-access.mdx). The bit mask integer subtracts from the existing flags of the account. This allows for setting specific bits without knowledge of existing flags. | | Set flags | integer | Indicates which [account](../stellar-data-structures/accounts.mdx) flags to set. These account-level flags are primarily used by asset issuers. For details about the flags, please refer to the [Asset Design Considerations page](../../../tokens/control-asset-access.mdx#controlling-access-to-an-asset-with-flags) The bit mask integer adds onto the existing flags of the account. This allows for setting specific bits without knowledge of existing flags. | | Master weight | integer | A number from 0-255 (inclusive) representing the weight of the master key. If the weight of the master key is updated to 0, it is effectively disabled. | | Low threshold | integer | A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have [a low threshold](../../fundamentals/transactions/signatures-multisig.mdx). | | Medium threshold | integer | A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have [a medium threshold](../../fundamentals/transactions/signatures-multisig.mdx). | | High threshold | integer | A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have [a high threshold](../../fundamentals/transactions/signatures-multisig.mdx). | | Home domain | string | Sets the home domain of an account. See [Federation](../../glossary.mdx#federation). | | Signer | \{Public Key, weight} | Add, update, or remove a signer from an account. Signer weight is a number from 0-255 (inclusive). The signer is deleted if the weight is 0. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `SET_OPTIONS_LOW_RESERVE` | -1 | This account does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every new signer added to an account, the minimum reserve of XLM that account must hold increases. | | `SET_OPTIONS_TOO_MANY_SIGNERS` | -2 | 20 is the maximum number of signers an account can have, and adding another signer would exceed that. | | `SET_OPTIONS_BAD_FLAGS` | -3 | The flags set and/or cleared are invalid by themselves or in combination. | | `SET_OPTIONS_INVALID_INFLATION` | -4 | The destination account set in the `inflation` field does not exist. | | `SET_OPTIONS_CANT_CHANGE` | -5 | This account can no longer change the option it wants to change. | | `SET_OPTIONS_UNKNOWN_FLAG` | -6 | The account is trying to set a flag that is unknown. | | `SET_OPTIONS_THRESHOLD_OUT_OF_RANGE` | -7 | The value for a key weight or threshold is invalid. | | `SET_OPTIONS_BAD_SIGNER` | -8 | Any additional signers added to the account cannot be the master key. | | `SET_OPTIONS_INVALID_HOME_DOMAIN` | -9 | Home domain is malformed. | ## Change trust Creates, updates, or deletes a trustline Learn more about trustlines: [Trustlines section](../stellar-data-structures/accounts.mdx#trustlines) **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.changeTrust) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ChangeTrustOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#ChangeTrust) **Threshold**: Medium **Result**: `ChangeTrustResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Line | ChangeTrustAsset | The asset of the trustline. For example, if a user extends a trustline of up to 200 USD to an anchor, the `line` is USD:anchor. | | Limit | integer | The limit of the trustline. In the previous example, the `limit` would be 200. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `CHANGE_TRUST_MALFORMED` | -1 | The input to this operation is invalid. | | `CHANGE_TRUST_NO_ISSUER` | -2 | The issuer of the asset cannot be found. | | `CHANGE_TRUST_INVALID_LIMIT` | -3 | The `limit` is not sufficient to hold the current balance of the trustline and still satisfy its buying liabilities. This error occurs when attempting to remove a trustline with a non-zero asset balance. | | `CHANGE_TRUST_LOW_RESERVE` | -4 | This account does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every new trustline added to an account, the minimum reserve of XLM that account must hold increases. | | `CHANGE_TRUST_SELF_NOT_ALLOWED` | -5 | The source account attempted to create a trustline for itself, which is not allowed. | | `CHANGE_TRUST_TRUST_LINE_MISSING` | -6 | The asset trustline is missing for the liquidity pool. | | `CHANGE_TRUST_CANNOT_DELETE` | -7 | The asset trustline is still referenced by a liquidity pool. | | `CHANGE_TRUST_NOT_AUTH_MAINTAIN_LIABILITIES` | -8 | The asset trustline is deauthorized. | ## Allow trust Updates the authorized flag of an existing trustline. This operation can only be performed by the asset issuer :::warning This operation is deprecated as of Protocol 17- prefer [_SetTrustlineFlags_](#set-trustline-flags) operation instead. ::: **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.allowTrust) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/AllowTrustOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#AllowTrust) **Threshold**: Low **Result**: `AllowTrustResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Trustor | account ID | The account that has an established trustline for the asset. | | Type | asset code | The 4 or 12 character-maximum asset code of the trustline the source account is authorizing. For example, if an issuing account wants to allow another account to hold its USD credit, the `type` is `USD`. | | Authorize | integer | Flag indicating whether the trustline is authorized. `1` if the account is authorized to transact with the asset. `2` if the account is authorized to maintain offers, but not to perform other transactions | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `ALLOW_TRUST_MALFORMED` | -1 | The asset specified in `type` is invalid. In addition, this error happens when the native asset is specified. | | `ALLOW_TRUST_NO_TRUST_LINE` | -2 | The `trustor` does not have a trustline with the issuer performing this operation. | | `ALLOW_TRUST_TRUST_NOT_REQUIRED` | -3 | The source account (issuer performing this operation) does not require trust. In other words, it does not have the flag `AUTH_REQUIRED_FLAG` set. | | `ALLOW_TRUST_CANT_REVOKE` | -4 | The source account is trying to revoke the trustline of the `trustor`, but it cannot do so. | | `ALLOW_TRUST_SELF_NOT_ALLOWED` | -5 | The source account attempted to allow a trustline for itself, which is not allowed because an account cannot create a trustline with itself. | | `ALLOW_TRUST_LOW_RESERVE` | -6 | Claimable balances can't be created on revocation of asset (or pool share) trustlines associated with a liquidity pool due to low reserves. | ## Account merge Transfers the XLM balance of an account to another account and removes the source account from the ledger A source account can only be merged once it holds no non-signer subentries: any trustlines, offers, or data entries must be removed first. Signers are _not_ a blocker — they (including sponsored signers) are removed automatically during the merge. Sponsorship also blocks a merge (`ACCOUNT_MERGE_IS_SPONSOR`), and this covers two distinct conditions with different remedies: (1) the account is already _sponsoring_ reserves for other accounts (`numSponsoring > 0`) — revoke those sponsorships first; or (2) the account has an open is-sponsoring-future-reserves relationship earlier in the same transaction — that relationship must be ended (with `EndSponsoringFutureReserves`) before the merge, and in this case there may be no created reserve to revoke. In either case, merely _being_ sponsored by another account does not block a merge. **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.accountMerge) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/AccountMergeOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#AccountMerge) **Threshold**: High **Result**: `AccountMergeResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Destination | account ID | The account that receives the remaining XLM balance of the source account. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `ACCOUNT_MERGE_MALFORMED` | -1 | The operation is malformed because the source account cannot merge with itself. The `destination` must be a different account. | | `ACCOUNT_MERGE_NO_ACCOUNT` | -2 | The `destination` account does not exist. | | `ACCOUNT_MERGE_IMMUTABLE_SET` | -3 | The source account has `AUTH_IMMUTABLE` flag set. | | `ACCOUNT_MERGE_HAS_SUB_ENTRIES` | -4 | The source account still has non-signer subentries (trustlines, offers, or data entries). Signers do not block the merge and are removed automatically. | | `ACCOUNT_MERGE_SEQNUM_TOO_FAR` | -5 | Source's account sequence number is too high. It must be less than `(ledgerSeq << 32) = (ledgerSeq * 0x100000000)`. | | `ACCOUNT_MERGE_DEST_FULL` | -6 | The `destination` account cannot receive the balance of the source account and still satisfy its lumen buying liabilities. | | `ACCOUNT_MERGE_IS_SPONSOR` | -7 | The source account cannot be merged because it is sponsoring reserves. Either it is already sponsoring reserves for other accounts (`numSponsoring > 0`), which must be revoked first, or it has an open is-sponsoring-future-reserves relationship in the same transaction, which must be ended with `EndSponsoringFutureReserves`. | ## Manage data Sets, modifies, or deletes a data entry (name/value pair) that is attached to an account Learn more about entries and subentries: [Accounts section](../stellar-data-structures/accounts.mdx#subentries) **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.manageData) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ManageDataOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#ManageData) **Threshold**: Medium **Result**: `ManageDataResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Name | string | String up to 64 bytes long. If this is a new Name it will add the given name/value pair to the account. If this Name is already present then the associated value will be modified. | | Value | binary data | (optional) If not present then the existing Name will be deleted. If present then this value will be set in the DataEntry. Up to 64 bytes long. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `MANAGE_DATA_NOT_SUPPORTED_YET` | -1 | The network hasn't moved to this protocol change yet. This failure means the network doesn't support this feature yet. | | `MANAGE_DATA_NAME_NOT_FOUND` | -2 | Trying to remove a Data Entry that isn't there. This will happen if Name is set (and Value isn't) but the Account doesn't have a DataEntry with that Name. | | `MANAGE_DATA_LOW_RESERVE` | -3 | This account does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every new DataEntry added to an account, the minimum reserve of XLM that account must hold increases. | | `MANAGE_DATA_INVALID_NAME` | -4 | Name not a valid string. | ## Bump sequence Bumps forward the sequence number of the source account to the given sequence number, invalidating any transaction with a smaller sequence number **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.bumpSequence) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/BumpSequenceOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#BumpSequence) **Threshold**: Low **Result**: `BumpSequenceResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | bumpTo | SequenceNumber | desired value for the operation's source account sequence number. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `BUMP_SEQUENCE_BAD_SEQ` | -1 | The specified `bumpTo` sequence number is not a valid sequence number. It must be between 0 and `INT64_MAX` (9223372036854775807 or 0x7fffffffffffffff). | ## Create claimable balance Moves an amount of asset from the operation source account into a new ClaimableBalanceEntry Learn more about claimable balances: [Claimable Balances Guide](../../../build/guides/transactions/claimable-balances.mdx) **SDKs**: [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/CreateClaimableBalanceOperation.java) **Threshold**: Medium **Result**: `CreateClaimableBalanceResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Asset | asset | Asset that will be held in the ClaimableBalanceEntry in the form `asset_code:issuing_address` or `native` (XLM). | | Amount | integer | Amount of `asset` stored in the ClaimableBalanceEntry. | | Claimants | list of claimants | List of Claimants (account address and ClaimPredicate pair) that can claim this ClaimableBalanceEntry. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `CREATE_CLAIMABLE_BALANCE_MALFORMED` | -1 | The input to this operation is invalid. | | `CREATE_CLAIMABLE_BALANCE_LOW_RESERVE` | -2 | The account creating this entry does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a ClaimableBalanceEntry. For every claimant in the list, the minimum amount of XLM this account must hold will increase by baseReserve. | | `CREATE_CLAIMABLE_BALANCE_NO_TRUST` | -3 | The source account does not trust the issuer of the asset it is trying to include in the ClaimableBalanceEntry. | | `CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED` | -4 | The source account is not authorized to transfer this asset. | | `CREATE_CLAIMABLE_BALANCE_UNDERFUNDED` | -5 | The source account does not have enough funds to transfer amount of this asset to the ClaimableBalanceEntry. | ## Claim claimable balance Claims a ClaimableBalanceEntry that corresponds to the BalanceID and adds the amount of an asset on the entry to the source account Learn more about claimable balances and view more parameters: [Claimable Balances Guide](../../../build/guides/transactions/claimable-balances.mdx) **SDKs**: [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ClaimClaimableBalanceOperation.java) **Threshold**: Low **Result**: `ClaimClaimableBalanceResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | BalanceID | claimableBalanceID | BalanceID on the ClaimableBalanceEntry that the source account is claiming. The balanceID can be retrieved from a successful `CreateClaimableBalanceResult`. See [Claimable Balance Guide](../../../build/guides/transactions/claimable-balances.mdx#create-claimable-balance) for more information. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST` | -1 | There is no existing ClaimableBalanceEntry that matches the input BalanceID. | | `CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM` | -2 | There is no claimant that matches the source account, or the claimants predicate is not satisfied. | | `CLAIM_CLAIMABLE_BALANCE_LINE_FULL` | -3 | The account claiming the ClaimableBalanceEntry does not have sufficient limits to receive amount of the asset and still satisfy its buying liabilities. | | `CLAIM_CLAIMABLE_BALANCE_NO_TRUST` | -4 | The source account does not trust the issuer of the asset it is trying to claim in the ClaimableBalanceEntry. | | `CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED` | -5 | The source account is not authorized to claim the asset in the ClaimableBalanceEntry. | ## Begin sponsoring future reserves Allows an account to pay the base reserves for another account; sponsoring account establishes the is-sponsoring-future-reserves relationship There must also be an end sponsoring future reserves operation in the same transaction Learn more about sponsored reserves: [Sponsored Reserves Guide](../../../build/guides/transactions/sponsored-reserves.mdx) **SDKs**: [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/BeginSponsoringFutureReservesOperation.java) **Threshold**: Medium **Result**: `BeginSponsoringFutureReservesResult` **Parameters**: | Parameters | Type | Description | | ----------- | ---------- | ---------------------------------------------- | | SponsoredID | account ID | Account that will have its reserves sponsored. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `BEGIN_SPONSORING_FUTURE_RESERVES_MALFORMED` | -1 | Source account is equal to sponsoredID. | | `BEGIN_SPONSORING_FUTURE_RESERVES_ALREADY_SPONSORED` | -2 | Source account is already sponsoring sponsoredID. | | `BEGIN_SPONSORING_FUTURE_RESERVES_RECURSIVE` | -3 | Either source account is currently being sponsored, or sponsoredID is sponsoring another account. | ## End sponsoring future reserves Terminates the current is-sponsoring-future-reserves relationship in which the source account is sponsored Learn more about sponsored reserves: [Sponsored Reserves Guide](../../../build/guides/transactions/sponsored-reserves.mdx) **SDKs**: [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/EndSponsoringFutureReservesOperation.java) **Threshold**: Medium **Result**: `EndSponsoringFutureReservesResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | begin_sponsor | account ID | The id of the account which initiated the sponsorship. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `END_SPONSORING_FUTURE_RESERVES_NOT_SPONSORED` | -1 | Source account is not sponsored. | ## Revoke sponsorship Sponsoring account can remove or transfer sponsorships of existing ledgerEntries and signers; the logic of this operation depends on the state of the source account Learn more about sponsored reserves: [Sponsored Reserves Guide](../../../build/guides/transactions/sponsored-reserves.mdx) **Threshold**: Medium **Result**: `RevokeSponsorshipResult` This operation is a union with **two** possible types: | Union Type | Parameters | Type | Description | | --- | --- | --- | --- | | `REVOKE_SPONSORSHIP_LEDGER_ENTRY` | LedgerKey | ledgerKey | Ledger key that holds information to identify a specific ledgerEntry that may have its sponsorship modified. See [LedgerKey](../../glossary.mdx#ledgerkey) for more information. | Or | Union Type | Parameters | Type | Description | | --- | --- | --- | --- | | `REVOKE_SPONSORSHIP_SIGNER` | Signer | \{account ID, Signer Key} | Signer that may have its sponsorship modified. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `REVOKE_SPONSORSHIP_DOES_NOT_EXIST` | -1 | The ledgerEntry for LedgerKey doesn’t exist, the account ID on signer doesn’t exist, or the Signer Key doesn’t exist on account ID’s account. | | `REVOKE_SPONSORSHIP_NOT_SPONSOR` | -2 | If the ledgerEntry/signer is sponsored, then the source account must be the sponsor. If the ledgerEntry/signer is not sponsored, the source account must be the owner. This error will be thrown otherwise. | | `REVOKE_SPONSORSHIP_LOW_RESERVE` | -3 | The sponsored account does not have enough XLM to satisfy the minimum balance increase caused by revoking sponsorship on a ledgerEntry/signer it owns, or the sponsor of the source account doesn’t have enough XLM to satisfy the minimum balance increase caused by sponsoring a transferred ledgerEntry/signer. | | `REVOKE_SPONSORSHIP_ONLY_TRANSFERABLE` | -4 | Sponsorship cannot be removed from this ledgerEntry. This error will happen if the user tries to remove the sponsorship from a ClaimableBalanceEntry. | | `REVOKE_SPONSORSHIP_MALFORMED` | -5 | One or more of the inputs to the operation was malformed. | ## Clawback Burns an amount in a specific asset from an account. Only the issuing account for the asset can perform this operation. Learn more about clawbacks: [Clawback Guide](../../../build/guides/transactions/clawbacks.mdx) **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.clawback) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ClawbackOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#Clawback) **Threshold**: Medium **Result**: `ClawbackResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | From | account ID | Account address from which asset needs to be clawed back. | | Asset | asset | Asset held by the destination account. | | Amount | integer | Amount of the aforementioned asset to burn. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `CLAWBACK_MALFORMED` | -1 | The input to the clawback is invalid. | | `CLAWBACK_NOT_CLAWBACK_ENABLED` | -2 | The trustline between From and the issuer account for this Asset does not have clawback enabled. | | `CLAWBACK_NO_TRUST` | -3 | The From account does not trust the issuer of the asset. | | `CLAWBACK_UNDERFUNDED` | -4 | The From account does not have a sufficient available balance of the asset (after accounting for selling liabilities). | ## Clawback claimable balance Claws back an unclaimed ClaimableBalanceEntry, burning the pending amount of the asset. Only the issuing account for the asset can perform this operation. Learn more about clawbacks: [Clawback Guide](../../../build/guides/transactions/clawbacks.mdx) Learn more about claimable balances: [Claimable Balances Guide](../../../build/guides/transactions/claimable-balances.mdx) **SDKs**: [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ClawbackClaimableBalanceOperation.java) **Threshold**: Medium **Result**: `ClaimClaimableBalanceResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | BalanceID | claimableBalanceID | The BalanceID on the ClaimableBalanceEntry that the source account is claiming, which can be retrieved from a successful `CreateClaimableBalanceResult` | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `CLAWBACK_CLAIMABLE_BALANCE_DOES_NOT_EXIST` | -1 | There is no existing ClaimableBalanceEntry that matches the input BalanceID. | | `CLAWBACK_CLAIMABLE_BALANCE_NOT_ISSUER` | -2 | The source account is not the issuer of the asset in the claimable balance. | | `CLAWBACK_CLAIMABLE_BALANCE_NOT_CLAWBACK_ENABLED` | -3 | `The CLAIMABLE_BALANCE_CLAWBACK_ENABLED_FLAG` is not set for this trustline. | ## Set trustline flags Allows issuing account to configure authorization and trustline flags to an asset The Asset parameter is of the `TrustLineAsset` type. If you are modifying a trustline to a regular asset (i.e. one in a Code:Issuer format), this is equivalent to the Asset type. If you are modifying a trustline to a pool share, however, this is composed of the liquidity pool's unique ID. Learn more about flags: [Flags Glossary Entry](../../glossary.mdx#flags) **SDKs**: [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/SetTrustlineFlagsOperation.java) **Threshold**: Low **Result**: `SetTrustLineFlagsResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Trustor | account ID | The account that established this trustline. | | Asset | TrustLineAsset | The asset trustline whose flags are being modified. | | SetFlags | integer | One or more flags (combined via bitwise-OR) indicating which flags to set. Possible flags are: 1 if the trustor is authorized to transact with the asset or 2 if the trustor is authorized to maintain offers but not to perform other transactions. | | ClearFlags | integer | One or more flags (combined via bitwise OR) indicating which flags to clear. Possibilities include those for SetFlags as well as 4, which prevents the issuer from clawing back its asset (both from accounts and claimable balances). | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `SET_TRUST_LINE_FLAGS_MALFORMED` | -1 | This can happen for a number of reasons: the asset specified by AssetCode and AssetIssuer is invalid; the asset issuer isn't the source account; the Trustor is the source account; the native asset is specified; or the flags being set/cleared conflict or are otherwise invalid. | | `SET_TRUST_LINE_FLAGS_NO_TRUST_LINE` | -2 | The Trustor does not have a trustline with the issuer performing this operation. | | `SET_TRUST_LINE_FLAGS_CANT_REVOKE` | -3 | The issuer is trying to revoke the trustline authorization of Trustor, but it cannot do so because AUTH_REVOCABLE_FLAG is not set on the account. | | `SET_TRUST_LINE_FLAGS_INVALID_STATE` | -4 | If the final state of the trustline has both AUTHORIZED_FLAG (1) and AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG (2) set, which are mutually exclusive. | | `SET_TRUST_LINE_FLAGS_LOW_RESERVE` | -5 | Claimable balances can't be created on revocation of asset (or pool share) trustlines associated with a liquidity pool due to low reserves. | ## Liquidity pool deposit Deposits assets into a liquidity pool, increasing the reserves of a liquidity pool in exchange for pool shares Parameters to this operation depend on the ordering of assets in the liquidity pool: “A” refers to the first asset in the liquidity pool, and “B” refers to the second asset in the liquidity pool. If the pool is empty, then this operation deposits maxAmountA of A and maxAmountB of B into the pool. If the pool is not empty, then this operation deposits at most maxAmountA of A and maxAmountB of B into the pool. The actual amounts deposited are determined using the current reserves of the pool. You can use these parameters to control a percentage of slippage. Learn more about liquidity pools: [Liquidity Pools section](../liquidity-on-stellar-sdex-liquidity-pools.mdx) **SDKs**: [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/LiquidityPoolDepositOperation.java) **Threshold**: Medium **Result**: `LiquidityPoolDepositResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Liquidity Pool ID | liquidityPoolID | The PoolID for the Liquidity Pool to deposit into. | | Max Amount A | integer | Maximum amount of first asset to deposit. | | Max Amount B | integer | Maximum amount of second asset to deposit. | | Min Price | \{numerator, denominator} | Minimum depositA/depositB. | | Max Price | \{numerator, denominator} | Maximum depositA/depositB. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `LIQUIDITY_POOL_DEPOSIT_MALFORMED` | -1 | One or more of the inputs to the operation was malformed. | | `LIQUIDITY_POOL_DEPOSIT_NO_TRUST` | -2 | No trustline exists for one of the assets being deposited. | | `LIQUIDITY_POOL_DEPOSIT_NOT_AUTHORIZED` | -3 | The account does not have authorization for one of the assets. | | `LIQUIDITY_POOL_DEPOSIT_UNDERFUNDED` | -4 | There is not enough balance of one of the assets to perform the deposit. | | `LIQUIDITY_POOL_DEPOSIT_LINE_FULL` | -5 | The pool share trustline does not have a sufficient limit. | | `LIQUIDITY_POOL_DEPOSIT_BAD_PRICE` | -6 | The deposit price is outside of the given bounds. | | `LIQUIDITY_POOL_DEPOSIT_POOL_FULL` | -7 | The liquidity pool reserves are full. | ## Liquidity pool withdraw Withdraw assets from a liquidity pool, reducing the number of pool shares in exchange for reserves of a liquidity pool The minAmountA and minAmountB parameters can be used to control a percentage of slippage from the "spot price" on the pool. Learn more about liquidity pools: [Liquidity Pools section](../liquidity-on-stellar-sdex-liquidity-pools.mdx) **SDKs**: [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/LiquidityPoolWithdrawOperation.java) **Threshold**: Medium **Result**: `LiquidityPoolWithdrawResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Liquidity Pool ID | liquidityPoolID | The PoolID for the Liquidity Pool to withdraw from. | | Amount | integer | Amount of pool shares to withdraw. | | Min Amount A | integer | Minimum amount of the first asset to withdraw. | | Min Amount B | integer | Minimum amount of the second asset to withdraw. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `LIQUIDITY_POOL_WITHDRAW_MALFORMED` | -1 | One or more of the inputs to the operation was malformed. | | `LIQUIDITY_POOL_WITHDRAW_NO_TRUST` | -2 | There is no trustline for one of the assets. | | `LIQUIDITY_POOL_WITHDRAW_UNDERFUNDED` | -3 | Insufficient balance for the pool shares. | | `LIQUIDITY_POOL_WITHDRAW_LINE_FULL` | -4 | The withdrawal would exceed the trustline limit for one of the assets. | | `LIQUIDITY_POOL_WITHDRAW_UNDER_MINIMUM` | -5 | Unable to withdraw enough to satisfy the minimum price. | ## Invoke Host Function Invoke and deploy Soroban smart contracts with `InvokeHostFunctionOp`. The `InvokeHostFunctionOp` can be used to perform the following Soroban operations: - Invoke contract functions: `HOST_FUNCTION_TYPE_INVOKE_CONTRACT` - Upload Wasm of the contracts: `HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM` - Deploy new contracts using the uploaded Wasm or built-in implementations: `HOST_FUNCTION_TYPE_CREATE_CONTRACT` Note that Soroban transactions can only contain one operation per transaction. Learn more [here](../../fundamentals/contract-development/contract-interactions/stellar-transaction.mdx#invokehostfunctionop). **SDKs**: [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/InvokeHostFunctionOperation.java) **Threshold**: Medium **Result**: `InvokeHostFunctionResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Host Function | HostFunction | The host function to invoke | | Auth | Soroban Authorization Entry | Per-address authorizations for this host function. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `INVOKE_HOST_FUNCTION_MALFORMED` | -1 | One or more of the inputs to the operation was malformed. | | `INVOKE_HOST_FUNCTION_TRAPPED` | -2 | The function invocation trapped in the Soroban runtime. | | `INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED` | -3 | The function invocation could not complete within the currently configured resource constraints of the network. | | `INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED` | -4 | A ledger entry required for this function's footprint is in an archived state, and must be restored. | | `INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE` | -5 | The refundable Soroban fee provided was not sufficient to pay for the compute resources required by this function invocation. | ## Extend Footprint TTL Extend the time to live (TTL) of entries for Soroban smart contracts with the `ExtendFootprintTTLOp`. This operation extends the TTL of the entries specified in the `readOnly` footprint of the transaction so that they will live at least until the `extendTo` ledger sequence number is reached. Note that Soroban transactions can only contain one operation per transaction. Learn more in the [State Archival section](../../fundamentals/contract-development/storage/state-archival.mdx). **SDKs**: [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ExtendFootprintTTLOperation.java) **Threshold**: Medium **Result**: `ExtendFootprintTTLResult` **Parameters**: | Parameters | Type | Description | | --- | --- | --- | | Ext | ExtensionPoint | Reserved for later use. | | Extend To | integer | The ledger sequence number the entries will live until. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `EXTEND_FOOTPRINT_TTL_MALFORMED` | -1 | One or more of the inputs to the operation was malformed. | | `EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED` | -2 | The TTL extension could not be completed within the currently configured resource constraints of the network. | | `EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE` | -3 | The refundable Soroban fee provided was not sufficient to pay for TTL extension of the specified ledger entries. | ## Restore Footprint Make archived Soroban smart contract entries accessible again by restoring them with `RestoreFootprintOp`. This operation restores the archived entries specified in the `readWrite` footprint. Note that Soroban transactions can only contain one operation per transaction. Learn more in the [State Archival section](../../fundamentals/contract-development/storage/state-archival.mdx). **SDKs**: [JavaScript](https://stellar.github.io/js-stellar-sdk/Operation.html#.restoreFootprint) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/RestoreFootprintOperation.java) **Threshold**: Medium **Result**: `RestoreFootprintResult` **Parameters**: | Parameters | Type | Description | | ---------- | -------------- | ----------------------- | | Ext | ExtensionPoint | Reserved for later use. | **Possible errors**: | Error | Code | Description | | --- | --- | --- | | `RESTORE_FOOTPRINT_MALFORMED` | -1 | One or more of the inputs to the operation was malformed. | | `RESTORE_FOOTPRINT_RESOURCE_LIMIT_EXCEEDED` | -2 | The archive restoration could not be completed within the currently configured resource constraints of the network. | | `RESTORE_FOOTPRINT_INSUFFICIENT_REFUNDABLE_FEE` | -3 | The refundable Soroban fee provided was not sufficient to pay for archive restoration of the specified ledger entries. | --- ## Operations & Transactions: How Blockchain Actions Are Executed # Operations and Transactions :::note Find how-to guides for smart contract and non-smart contract transactions in the [Transactions How-To Guides section](../../../build/guides/transactions/README.mdx). ::: ## Operations and transactions: how they work To perform actions with an account on the Stellar network, you compose operations, bundle them into a transaction, and then sign and submit the transaction to the network. Smart contract transactions (those with `InvokeHostFunctionOp`, `ExtendFootprintTTLOp`, or `RestoreFootprintOp` operations) can only have one operation per transaction. ### Operations Operations are individual commands that modify the ledger. Operations are used to send payments, invoke a smart contract function, enter orders into the decentralized exchange, change settings on accounts, and authorize accounts to hold assets. All operations fall into one of three threshold categories: low, medium, or high, and each threshold category has a weight between 0 and 255 (which can be determined using set_options). Thresholds determine what signature weight is required for the operation to be accepted. For example, let’s say an account sets the medium threshold weight to 5. If the account wants to successfully establish a trustline with the `changeTrust` operation, the weight of the signature(s) must be greater than or equal to 5. To learn more about signature weight, see the [Signatures and Multisig section](../../fundamentals/transactions/signatures-multisig.mdx). View a comprehensive list of Stellar operations and their threshold levels in the [List of Operations section](./list-of-operations.mdx). ### Transactions The Stellar network encodes transactions using a standardized protocol called External Data Representation (XDR). You can read more about this in our [XDR Section](../data-format/xdr.mdx). Accounts can only perform one transaction at a time. Transactions comprise a bundle of between 1-100 operations (except smart contract transactions, which can only have one operation per transaction) and are signed and submitted to the ledger by accounts. Transactions always need to be authorized by the source account’s public key to be valid, which involves signing the transaction object with the public key’s associated secret key. A transaction plus its signature(s) is called a transaction envelope. A transaction may need more than one signature- this happens if it has operations that affect more than one account or if it has a high threshold weight. Check out the [Signature and Multisignature Section](../../fundamentals/transactions/signatures-multisig.mdx) for more information. Transactions are atomic. Meaning if one operation in a transaction fails, all operations fail, and the entire transaction is not applied to the ledger. Operations are executed for the source account of the transaction unless an operation override is defined. Smart contract transactions also go through a simulation process where developers can test how the transaction would be executed on the network using the RPC endpoint `simulateTransaction`. Read more in the [Soroban docs](../../fundamentals/contract-development/contract-interactions/transaction-simulation.mdx). ## Memo Memos are an optional unstructured data field that can be used to embed any additional identifying information about the transaction relevant to the sender or receiver. They were previously used to differentiate between individual accounts in a pooled account - something we used muxed accounts for now. For more information on muxed accounts, see our [Pooled Accounts - Muxed Accounts & Memos Guide](../../../build/guides/transactions/pooled-accounts-muxed-accounts-memos.mdx) Memos can be one of the following types: - `MEMO_TEXT`: A string encoded using either ASCII or UTF-8, up to 28-bytes long. - `MEMO_ID`: A 64-bit unsigned integer. - `MEMO_HASH`: A 32-byte hash. - `MEMO_RETURN`: A 32-byte hash intended to be interpreted as the hash of the transaction the sender is refunding. ## Memo content examples - Notifying that the transaction is a refund or reimbursement - Reference to an invoice the transaction is paying - Any further internal routing information - Links to relevant data #### Transaction attributes - [Fee](../fees-resource-limits-metering.mdx) - [List of operations](./list-of-operations.mdx) - [List of signatures](../../fundamentals/transactions/signatures-multisig.mdx) - [Memo or muxed account](../../../build/guides/transactions/pooled-accounts-muxed-accounts-memos.mdx) - [Sequence number](../../glossary.mdx#sequence-number) - [Source account](../../glossary.mdx#source-account) - [Preconditions (optional)](#preconditions) ## Transaction and operation validity Before being successfully submitted to the Stellar network, transactions go through several validity checks. These checks are grouped into three categories: ### Preconditions (optional) {/* #preconditions */} Preconditions are checked first. All preconditions are optional. Time bounds are encouraged, but the other preconditions are used in more specialized circumstances. You can set multiple preconditions as long as the combination is logically sound. #### Time bounds Valid if within set time bounds of the transaction Time bounds are an optional UNIX timestamp (in seconds), determined by ledger time, of a lower and upper bound of when a transaction will be valid. If a transaction is submitted too early or too late, it will fail to make it into the transaction set. Setting time bounds on transactions is highly encouraged, and many SDKs enforce them. If `maxTime` is 0, upper time bounds are not set. In this case, if a transaction does not make it to the transaction set, it is kept in memory and continuously tries to make it to the next transaction set. Because of this, we advise that all transactions are created with time bounds to invalidate transactions after a certain amount of time, especially if you plan to resubmit your transaction at a later time. #### Ledger bounds Valid if within the set ledger bounds of the transaction Ledger bounds apply to ledger numbers. With these defined, a transaction will only be valid for ledger numbers that fall within the determined range. The lower bound is inclusive (less than or equal to) while the upper bound is not (just greater than). If the upper bound is set to 0, this indicates there is no upper bound. #### Minimum sequence number If a minimum sequence number is set, the transaction will only be valid when its source account’s sequence number (call it S) is large enough. Specifically, it’s valid when S satisfies `minSeqNum <= S < tx.seqNum`. If this precondition is omitted, the default behavior applies: the transaction’s sequence number must be exactly one greater than the account’s sequence number. Note that after a transaction is executed, the account will always set its sequence number to the transaction’s sequence number. #### Minimum sequence age Transaction is valid after a particular duration (expressed in seconds) elapses since the account’s sequence number age. Minimum sequence age is a precondition relating to time, but unlike time bounds, which express absolute times, minimum sequence age is relative to when the transaction source account’s sequence number was touched. #### Minimum sequence ledger gap Valid if submitted in a ledger meeting or exceeding the source account’s sequence number age This is similar to the minimum sequence age, except it is expressed as a number of ledgers rather than a duration of time. #### Extra signers Valid if submitted with signatures that fulfill each of the extra signers A transaction can specify up to two extra signers as a precondition, meaning it must have signatures that correspond to those extra signers, even if those signatures would not otherwise be required to authorize the transaction (i.e., for its sources account or operations). The additional signers can be of any type besides the pre-authorized transaction signer since to pre-authorize a transaction, you need to know its hash, but be hash must include the extra signers. This Catch-22 relationship means including this type of extra signer will return an error. ### Operation validity When a transaction is submitted to a node, the node checks the validity of each operation in the transaction before attempting to include it in a candidate transaction set. These initial operation validity checks are intended to be fast and simple, with more intensive checks coming after the fees have been consumed. For an operation to pass this validity check, it has to meet the following conditions: #### The signatures on the transaction must be valid for the operation The signatures are from valid signers for the source account of the operation. The combined weight of all signatures for the source account of the operation meets the threshold for the operation. #### The operation must be well-formed Typically this means checking the parameters for the operation to see if they’re in a valid format. For example, only positive values can be set for the amount of a payment operation. #### The operation must be valid in the current protocol version of the network Deprecated operations, such as inflation, are invalid by design. ### Transaction validity Finally, the following transaction checks take place: #### Source account The source account must exist on the ledger. #### Fee The fee must be greater than or equal to the network minimum fee for the number of operations submitted as part of the transaction. This does not guarantee that the transaction will be applied, only that it is valid. In addition, the source account must be able to pay the fee specified. #### Fee-bump (if applicable) See Validity of a [Fee-Bump Transaction Guide](../../../build/guides/transactions/fee-bump-transactions.mdx) for more information. #### Sequence number The sequence number must be one greater than the sequence number stored in the source account entry when the transaction is applied unless sequence number preconditions are set. An account can only have one transaction (and, therefore, one sequence number) consumed per ledger. #### List of operations Each operation must pass all the validity checks for an operation, described in the Operation Validity section above. #### List of signatures - Meet signature requirements for each operation in the transaction - Appropriate network passphrase is part of the transaction hash signed by each signer - Combined weight of the signatures for the source account of the transaction meets the low threshold for the source account. #### Memo (if applicable) The memo type must be a valid type, and the memo itself must adhere to the formatting of the memo type. --- ## Signatures and Multisig :::note This section details signing non-smart contract transactions. For auth related to smart contract transactions, see [authorization](../../fundamentals/contract-development/authorization.mdx).) ::: Signatures are authorization for transactions on the network. Transactions always need authorization from at least one public key to be valid and generally, the signature comes from the source account. Sometimes transactions require more signatures, which we’ll get into in the multisig section. Transaction signatures are created by signing the transaction object contents with a secret key. Stellar uses the ed25519 signature scheme, but there is also a mechanism for adding additional types of public and private key schemes. A transaction with an attached signature is considered to have authorization from that public key. ### Thresholds Each operation falls under a specific threshold category: low, medium, or high with a number level between 0-255 (to read more about this see our section on [Operations and Transactions](../../fundamentals/transactions/operations-and-transactions.mdx#operations)). This threshold determines what signature weight is needed to authorize an operation. To view each operation’s threshold, see our [List of Operations section](../../fundamentals/transactions/list-of-operations.mdx). Accounts can set their own signature weight, threshold values, and additional signing keys with the Set Options operation. By default, all operation threshold levels are set to 0, and the master key is set to weight 1. For most cases, it is recommended to set thresholds such that `low <= medium <= high`. If the master key’s weight is set at 0, it cannot be used to sign transactions, even for operations with a threshold value of 0. Be very careful setting your master key weight to 0. Doing so may permanently lock you out of your account (although if there are other signers listed on the account, they can still continue to sign transactions.) ### Authorization To determine if a transaction has the necessary authorization to run, the weights of all the signatures in the transaction envelope are added up. If this sum is equal to or greater than the threshold for that operation type, then the operation is authorized. This scheme is very flexible. You can require many signers to authorize payments from a particular account. You can have an account that any number of people can authorize for. You can have a master key that grants access or revokes access from others. It supports any m of n setup. ## Multisig In some cases, a transaction may need more than one signature: - If the transaction has operations with multiple source accounts, it requires the source account signature for each operation - Additional signatures are required if the account associated with the transaction has multiple public keys Each additional signer beyond the master key increases the account’s minimum balance by one base reserve. Up to 20 signatures can be attached to one transaction. Once a signature threshold is met, if there are leftover signatures, the transaction will fail. For example, if your transaction requires three signatures, providing more than three signatures, even if they are all valid, will result in a failed transaction error: `TX_BAD_AUTH_EXTRA`. This design is because unnecessary signature verification has a large effect on performance before accepting transactions in consensus. ### Alternate signature types To enable some advanced smart contract features there are a couple of additional signature types. These signature types also have weights and can be added and removed similarly to normal signature types. But rather than check a cryptographic signature for authorization they have a different method of proving validity to the network. #### Pre-authorized Transaction It is possible for an account to pre-authorize a particular transaction by adding the hash of the future transaction as a signer on the account. To do that, you need to prepare the transaction beforehand with the proper sequence number. Then you can obtain the hash of this transaction and add it as a signer to the account. Signers of this type are automatically removed from the account when a matching transaction is applied, regardless of whether the transaction succeeds or fails. In case a matching transaction is never submitted, the signer remains, and must be manually removed using the Set Options operation. This type of signer is especially useful in escrow accounts. You can pre-authorize two different transactions. Both could have the same sequence number but different destinations. This means that only one of them can be executed. #### Hash(x) :::note Hash(x) signing is a different concept from transaction hashes. Transaction hashes are unique identifiers generated by applying a cryptographic hash function to the data of a transaction in a blockchain. They serve as a digital fingerprint and allow users to verify and reference transactions on the network. Hash(x) is a signature type. ::: Adding a signature of type hash(x) allows anyone who knows x to sign the transaction. This type of signer is especially useful in [atomic cross-chain swaps](https://en.bitcoin.it/wiki/Atomic_cross-chain_trading) which are needed for inter-blockchain protocols like [lightning networks](https://lightning.network). First, create a random 256-bit value, which we call x. The SHA256 hash of that value can be added as a signer of type hash(x). Then in order to authorize a transaction, x is added as one of the signatures of the transaction. Keep in mind that x will be known to the world as soon as a transaction is submitted to the network with x as a signature. This means anyone will be able to sign for that account with the hash(x) signer at that point. Often you want there to be additional signers so someone must have a particular secret key and know x in order to reach the weight threshold required to authorize transactions on the account. ## Examples We'll go through some examples of how powerful thresholding and weights are, below. If you want to learn more about using advanced signers like `hash(x)`, check out [this blog post](https://stellar.org/blog/developers/messing-around-with-multi-sig) for some fun examples. - Example 1: Anchors - Example 2: Joint accounts - Example 3: Expense accounts - Example 4: Company accounts ### Example 1: Anchors You run an anchor that would like to keep its issuing key offline. That way, it's less likely a bad actor can get ahold of the anchor's key and start issuing credit improperly. However, your anchor needs to authorize people holding credit by running the `Set Trust Line Flags` operation. Before you issue credit to an account, you need to verify that account is OK. Multisig allows you to do all of this without exposing the master key of your anchor. You can add another signing key to your account with the operation `Set Options`. This additional key should have a weight below your anchor account's medium threshold. Since `Set Trust Line Flags` is a low-threshold operation, this extra key authorizes users to hold your anchor's credit. But, since `Payment` is a medium-threshold operation, this key does not allow anyone who compromises your anchor to issue credit. Your account setup: ``` Master Key Weight: 2 Additional Signing Key Weight: 1 Low Threshold: 0 Medium Threshold: 2 High Threshold: 2 ``` ### Example 2: Joint accounts You want to set up a joint account with Bilal and Carina such that any of you can authorize a payment. You also want to set up the account so that, if you choose to change signers (e.g., remove or add someone), a high-threshold operation, all 3 of you must agree. You add Bilal and Carina as signers to the joint account. You also ensure that it takes all of your key weights to clear the high threshold but only one to clear the medium threshold. Joint account setup: ``` Master Key Weight: 1 Low Threshold: 0 Medium Threshold: 0 High Threshold: 3 Bilal's Signing Key Weight: 1 Carina's Signing Key Weight: 1 ``` ### Example 3: Expense accounts You fully control an expense account, but you want your two coworkers Diyuan and Emil to be able to authorize transactions from this account. You add Diyuan and Emil’s signing keys to the expense account. If either Diyuan or Emil leave the company, you can remove their signing key, a high-threshold operation. Expense account setup: ``` Master Key Weight: 3 Low Threshold: 0 Medium Threshold: 0 High Threshold: 3 Diyuan's Key Weight: 1 Emil's Key Weight: 1 ``` ### Example 4: Company accounts **Warning**: this example involves setting the master key weight of an account to 0. Be very careful if you decide to do that: that key will no longer be able to sign any kind of transaction, so you are in danger of permanently locking yourself out of your account. Make sure you’ve thought carefully about what you’re doing, that you understand the implications, and that you change weights in the correct order. Your company wants to set up an account that requires 3 of 6 employees to agree to any transaction from that account. Company account setup: ``` Master Key Weight: 0 (Turned off so this account can't do anything without an employee.) Low Threshold: 3 Medium Threshold: 3 High Threshold: 3 Employee 1 Key Weight: 1 Employee 2 Key Weight: 1 Employee 3 Key Weight: 1 Employee 4 Key Weight: 1 Employee 5 Key Weight: 1 Employee 6 Key Weight: 1 ``` --- ## Transaction Lifecycle ### 1. Creation (Transaction Creator) A user creates a transaction by setting the source account, sequence number, list of operations and their respective parameters, fee or fee-bump, and optionally a memo and/or preconditions. #### 1b. Transaction Simulation Smart contract transactions undergo transaction simulation, where the transaction is run in a simulated environment to check for errors and ensure it can be executed successfully. Read more in the [Transaction Simulation section](../contract-development/contract-interactions/transaction-simulation.mdx). ### 2. Signing (Transaction Signers) Once the transaction is complete, it becomes a transaction envelope containing the transaction itself and a list of signers. All the required signatures must be collected and added to the transaction envelope’s list of signers. Commonly, it’s just the signature of the account doing the transaction, but more complicated setups can require collecting signatures from multiple parties. Read more about signatures in the [Signatures and Multisig section](./signatures-multisig.mdx). ### 3. Submitting (Transaction Submitter) After signing, the transaction can now be submitted to the Stellar network. If the transaction is invalid, it will be rejected immediately by Stellar Core, the account’s sequence number will not be incremented, and no fee will be consumed from the source account. Only one transaction (and one sequence number) for the same account can be consumed per ledger. Transactions are typically submitted using Stellar RPC, but you can also submit the transaction directly to an instance of Stellar Core. ### 4. Propagating (Validator) Once Stellar Core has determined that a transaction is valid, it will propagate the transaction to all other servers to which it’s connected. This way, a valid transaction is flooded to the entire Stellar network. ### 5. Crafting a candidate transaction set (Validator) When it’s time to close the ledger, each Stellar Core validator takes all valid transactions it is aware of since the last ledger close and collects them into a candidate transaction set. If it hears about any incoming transactions now, it puts them aside for the next ledger close. If the number of operations in the candidate transaction set is greater than the maximum number of operations per ledger, transactions will be prioritized by their fee for inclusion in the set. ### 6. Nominating a transaction set (Validator) Once each validator has crafted a candidate transaction set, the set is nominated to the network. ### 7. Stellar Consensus Protocol (SCP) determines the final transaction set (Validator Network) SCP resolves any differences between candidate transaction sets and ultimately determines a single transaction set to apply, the close time of the ledger, and any upgrades to the protocol that need to be applied network-wide at the apply time. If a transaction doesn’t make it into the transaction set, it is kept around in memory to be added to the next transaction set on a best-effort basis. If a transaction is kept in memory after a certain number of ledger closes, it will be banned for several additional ledgers. This means no attempt will be made to include it in a candidate transaction set additional ledgers during this time. ### 8. Transaction apply order is determined (Validator Network) Once SCP agrees on a particular transaction set, the apply order is computed for the transaction set. This shuffles the set's order to create uncertainty for competing transactions. ### 9. Fees are collected (Validator) Fees are collected for all transactions simultaneously. ### 10. Application (Validator) Each transaction is applied in the previously-determined order. For each transaction, the account’s sequence number is consumed (increased by 1), the transaction’s validity is rechecked, and each operation is applied in the order they occur in the transaction. Operations may fail at this stage due to errors that can occur outside of the transaction and operation validity checks. For example, an insufficient balance for a payment is not checked at submission and would fail at this time. The entire transaction will fail if any operation fails, and all previous operations will be rolled back. ### 11. Protocol Upgrades (Validator) Finally, upgrades are run if an upgrade took place. This can include arbitrary logic to upgrade the ledger state for protocol upgrades, along with ledger header modifications, including the protocol version, base fee, maximum number of operations per ledger, etc. Once this has been completed, the life cycle begins anew. --- ## Blockchain Glossary: Key Terms and Concepts for Understanding Stellar # Glossary ### Account A central Stellar data structure to hold balances, sign transactions, and issue assets. See the [Accounts section](./fundamentals/stellar-data-structures/accounts.mdx) to learn more. ### Account ID The public key used to create an account. This key persists across different key assignments. It is [represented](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md) in base32. ### Anchor The on and off-ramps on the Stellar network that facilitate one-to-one conversion of off-chain representations to and from tokenized assets, for example, digital tokens representing bank deposits. Read more in the [Anchors section](./fundamentals/anchors.mdx). ### Application (app) {/* #app */} A software program designed for users to carry out a specific task (other than operating the computer itself). ### Asset Fiat, physical, or other tokens of value that are tracked, held, or transferred by the Stellar distributed network. See the [Assets section](./fundamentals/stellar-data-structures/assets.mdx) to learn more. ### Balance The amount of a given asset an account holds. Each asset has its own balance and these balances are stored in trustlines for every asset except XLM, which is held directly by the account. ### BalanceID Parameter required when claiming a newly created entry via the Claim claimable balance operation. See [ClaimableBalanceID](#claimablebalanceid). ### Base Fee The fee you’re willing to pay per operation in a transaction. This differs from the Effective Base Fee which is the actual fee paid per operation for a transaction to make it to the ledger. When the network is in surge pricing mode, the effective base fee varies based on an auction mechanism. When it's not, the effective base fee defaults to the network minimum currently at 100 stroops per operation. Learn more in our [Fees section](./fundamentals/fees-resource-limits-metering.mdx). ### Base Reserve A unit of measurement used to calculate an account’s minimum balance. One base reserve is currently 0.5 XLM. Learn more in our [Lumens section](./fundamentals/lumens.mdx#base-reserves). ### Burn Remove an asset from circulation, which can happen in two ways: 1) a holder sends the asset back to the issuing account 2) an issuer claws back a clawback-enabled asset from a holder's account. ### Claim Predicate A recursive data structure used to construct complex conditionals with different values of ClaimPredicateType. ### ClaimableBalanceID A SHA-256 hash of the OperationID for claimable balances. ### Claimant An object that holds both the destination account that can claim the ClaimableBalanceEntry and a ClaimPredicate that must evaluate to true for the claim to succeed. ### Clawback An amount of asset from a trustline or claimable balance removed (clawed back) from a recipient’s balance sheet. Learn more in our [Clawback guide](../build/guides/transactions/clawbacks.mdx). ### Composable Data Platform {/* #cdp */} An architecture for building custom Stellar data pipelines from raw ledger metadata. CDP uses tools such as Galexie to export ledger metadata to external storage, then lets downstream consumers use the Ingest SDK or other processors to transform that data into application-specific models, analytics, indexes, or streams. Learn more in our [Ingest SDK tutorial](../build/apps/ingest-sdk/overview.mdx) and the [Composable Data Platform announcement](https://stellar.org/blog/developers/composable-data-platform). ### Contract Account An account that is implemented as a smart contract, allowing the contract to define custom authorization logic and on-chain policy enforcement before authorization succeeds instead of relying on built-in protocol features. ### Contract Token Tokens created and managed through smart contracts. These assets are programmable and governed by on-chain logic instead of built-in protocol features. :::note Contract tokens used to be referred to as "custom tokens", which has been deprecated. ::: ### Core Advancement Proposals (CAPs) {/* #caps */} Proposals of standards to improve the Stellar protocol. CAPs deal with changes to the core protocol of the Stellar network. Find a list of all draft, accepted, implemented, and rejected CAPs in [GitHub](https://github.com/stellar/stellar-protocol/tree/master/core). ### Create account operation Makes a payment to a 0-balance public key (Stellar address), thereby creating the account. You must use this operation to initialize an account rather than a standard payment operation. ### Cross-Asset Payments A payment that automatically handles the conversion of dissimilar assets. ### Custom Token This term has been deprecated in favor of [contract tokens](#contract-token). ### Decentralized Exchange A distributed exchange that allows the trading and conversion of assets on the network. Learn more in our [Liquidity on Stellar](./fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#sdex) section. ### External Data Representation (XDR) The type of encoding used for operations and data running on stellar-core. ### Federation The Stellar federation protocol maps Stellar addresses to an email-like identifier that provides more information about a given user. It’s a way for Stellar client software to resolve email-like addresses such as name\*yourdomain.com into `G...` account IDs. Federated addresses provide an easy way for users to share payment details by using a syntax that interoperates across different domains and providers. Read more in [GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0002.md). ### Flags Flags control access to an asset on the account level. Learn more about flags in our [Controlling Access to an Asset section](../tokens/control-asset-access.mdx#controlling-access-to-an-asset-with-flags). ### Fuzzing An automated test that rapidly stuffs massive amounts of randomized, malformed data into a system to reveal adverse or unexpected results that indicate vulnerabilities. Read more in the [Fuzz Testing Tutorial](../build/smart-contracts/example-contracts/fuzzing.mdx). ### GitHub An online repository for documents that can be accessed and shared among multiple users; host for the Stellar platform’s source code, documentation, and other open-source repos. ### Home Domain A fully qualified domain name (FQDN) linked to a Stellar account, used to generate an on-chain link to a Stellar Info File, which holds off-chain metadata. See the Set Options operation. Can be up to 32 characters. ### Inflation The inflation operation is deprecated because it wasn’t working as intended. Most users either ignored it or used it for personal gain, and the costs kept rising, so the network voted to disable it in Protocol 12 through [CAP-26: Disable Inflation Mechanism](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0026.md). Read about the implementation [here](https://github.com/stellar/stellar-core/releases/tag/v12.0.0). Read the related blog [here](https://stellar.org/blog/foundation-news/our-proposal-to-disable-inflation). ### JSON A standardized human-readable and machine-readable format for the exchange of structured data. ### Keypair A combined public and private key used to secure transactions. You can use any Stellar wallet, SDK, or the Stellar Lab to generate a valid keypair. ### Keystore An encrypted store or file that serves as a repository of private keys, certificates, and public keys. ### Ledger A representation of the state of the Stellar universe at a given point in time, shared across all network nodes. Learn more in the [Ledgers section](./fundamentals/stellar-data-structures/ledgers.mdx). ### LedgerKey LedgerKey holds information to identify a specific ledgerEntry. It is a union that can be any one of the LedgerEntryTypes (ACCOUNT, TRUSTLINE, OFFER, DATA, CLAIMABLE_BALANCE, Liquidity Pool, Contract Data, Contract Code, Config Setting or TTL). ### Liability A buying or selling obligation, required to satisfy (selling) or accommodate (buying) transactions. ### Lumen (XLM) {/* #lumen */} The native, built-in token on the Stellar network. Learn more about lumens in our [Lumens section](./fundamentals/lumens.mdx). ### Mainnet or Pubnet The Stellar Public Network, aka mainnet, the main network used by applications in production. Read more in our [Networks section](../networks/README.mdx). ### Master key The private key used in initial account creation. ### Minimum balance The smallest permissible balance in lumens for a Stellar account, currently 1 lumen. Learn more in our [Lumens section](./fundamentals/lumens.mdx#minimum-balance). ### Network capacity The maximum number of operations per ledger, as determined by validator vote. As of July 2026, the limit is 1,000 operations for the mainnet and 200 operations for the testnet; check current values on Stellar Lab's [Network Limits](https://lab.stellar.org/network-limits) page. ### Number of subentries The number of entries owned by an account, used to calculate the account’s minimum balance. ### Operation An individual command that modifies the ledger. Learn more in our [Operations and Transactions](./fundamentals/transactions/operations-and-transactions.mdx) section. ### OperationID A unique identifier for an operation, derived from the transaction source [Account ID](#account-id), the transaction sequence number, and the operation's index within the transaction. ### Order An offer to buy or sell an asset. Learn more in our [Liquidity on Stellar: SDEX and Liquidity Pools section](./fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#orders). ### Orderbook A record of outstanding orders on the Stellar network. Learn more in our [Liquidity on Stellar: SDEX and Liquidity Pools section](./fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#order-books). ### Passive Order An order that does not execute against a marketable counter order with the same price; filled only if the prices are not equal. ### Passphrase The Mainnet and Testnet each have their own unique passphrase, which are used to validate signatures on a given transaction. Learn more about network passphrases in the [Networks section](../networks/README.mdx#network-passphrases). ### Pathfinding The process of determining the best path of a payment, evaluating the current orderbooks, and finding the series of conversions to achieve the best rate. It happens during [Stellar Core](#stellar-core)'s market-order execution and in [Horizon aggregation](../data/apis/horizon/api-reference/aggregations/README.mdx). ### Payment Channel Allows two parties who frequently transact with one another to move the bulk of their activity off-chain, while still recording opening balances and final settlement on-chain. ### Precondition Optional requirements you can add to control a transaction’s validity. See the [Operation and Transaction Validity section](./fundamentals/transactions/operations-and-transactions.mdx#preconditions) for more information. ### Price The ratio of the quote asset and the base asset in an order. ### Public Key The public part of a keypair that identifies a Stellar account. The public key is public- it is visible on the ledger, anyone can look it up, and it is used when sending payments to the account, identifying the issuer of an asset, and verifying that a transaction is authorized. ### Secret (private) key The private key is part of a keypair, which is associated with an account. Do not share your secret key with anyone. ### Sequence Number Used to identify and verify the order of transactions with the source account. A transaction’s sequence number must always increase by one (unless minimum sequence number preconditions are set, or a bump sequence operation is used). SDKs and the Stellar Lab automatically increment the account’s sequence number by one when you build a transaction. ### Signer Refers to the master key or to any other signing keys added later. A signer is defined as the pair: public key + weight. Signers can be set with the Set Options operation. See our [Signatures and Multisig section](./fundamentals/transactions/signatures-multisig.mdx) for more information. ### Smart Contract Self-executing contracts with the terms of the agreement directly written into code, automatically enforceable without the need for intermediaries. ### Soroban The smart contract platform on the Stellar network. The name "Soroban" comes from the Japanese abacus, which is a traditional counting tool used for mathematical calculations. The Soroban abacus is a lightweight instrument known for its efficiency and accuracy in performing arithmetic operations. ### Source Account The account that originates a transaction. This account also provides the fee and sequence number for the transaction. ### Starlight An experimental layer 2 payment channel protocol for Stellar that allowed for bi-directional payment channels. The project is no longer maintained, and its [repository](https://github.com/stellar-deprecated/starlight) has been archived since April 2024. ### Stellar A decentralized, federated peer-to-peer network that allows people to send payments in any asset anywhere in the world instantaneously, and with minimal fees. ### Stellar Consensus Protocol (SCP) {/* #scp */} Provides a way to reach consensus without relying on a closed system to accurately record financial transactions. See our [SCP section](./fundamentals/stellar-consensus-protocol.mdx) to learn more. ### Stellar Core A replicated state machine that maintains a local copy of a cryptographic ledger and processes transactions against it, in consensus with a set of peers; also, the reference implementation for the peer-to-peer agent that manages the Stellar network. ### Stellar Decentralized Exchange (SDEX) {/* #sdex */} An always-on native marketplace for asset conversions. Pay what you want for something else, and trade when someone agrees on price. Or farm yield with deposits that help network liquidity. Learn more in our [Liquidity on Stellar section](./fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx). ### Stellar Development Foundation (SDF) {/* #sdf */} A non-profit organization founded to support the development and growth of the Stellar network. ### Stellar Ecosystem Proposals (SEPs) {/* #seps */} Standards and protocols to allow the Stellar ecosystem to interoperate. Learn more in our [SEPs section](./fundamentals/stellar-ecosystem-proposals.mdx). ### Stellar RPC A node that provides an interface for submitting transactions and reading data from the Stellar network. ### Stellar.toml {/* #stellar-toml */} A formatted configuration file containing published information about a node and an organization. For more, see the [Stellar Info File spec (SEP-1)](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md). ### Stroop As cents are to dollars, stroops are to assets: the smallest unit of an asset, one ten-millionth. ### Testnet The Stellar Test Network is maintained by the Stellar Development Foundation, which developers can use to test applications. Testnet is free to use and provides the same functionality as the main (public) network. Read more in our [Networks](../networks/README.mdx). ### Threshold The level of access for an operation. Also used to describe the ratio of validator nodes in a quorum set that must agree in order to reach consensus as part of the Stellar Consensus Protocol. Read more about operation thresholds in the [Operations and Transactions section](./fundamentals/transactions/operations-and-transactions.mdx#operations). Learn more about quorum set validators in our [Stellar Consensus Protocol section](./fundamentals/stellar-consensus-protocol.mdx). ### Time Bounds An optional feature you can apply to a transaction to enforce a time limit on the transaction; either the transaction makes it to the ledger or times out (fails) depending on your time parameters. Read more about time bounds in our [Operation and Transaction Validity section](./fundamentals/transactions/operations-and-transactions.mdx#transaction-and-operation-validity). ### Transaction A group of 1 to 100 operations that modify the ledger state. Read more in the [Operations and Transactions section](./fundamentals/transactions/operations-and-transactions.mdx#transactions). ### Transaction Envelope A transaction plus its signature(s) is called a transaction envelope. ### Transaction Fee Stellar requires a small fee for all transactions to prevent ledger spam and prioritize transactions during surge pricing. Learn more in our [Lumens section](./fundamentals/lumens.mdx#transaction-fees). ### Trustline An explicit opt-in for an account to hold a particular asset that tracks liabilities, the balance of the asset, and can also limit the amount of an asset that an account can hold. Learn more in our [Accounts section](./fundamentals/stellar-data-structures/accounts.mdx#trustlines). ### TTL (Time To Live) {/* #ttl */} A smart contract's TTL is how many ledgers remain until the data entry is no longer live. Read more in the [State Archival section](./fundamentals/contract-development/storage/state-archival.mdx#ttl). ### Type The classification of data that dictates the kind of data that can be stored and how it can be manipulated within a smart contract. ### UNIX Timestamp An integer representing a given date and time, as used on UNIX and Linux computers. ### Validator A basic validator keeps track of the ledger and submits transactions for possible inclusion. It ensures reliable access to the network and sign-off on transactions. A full validator performs the functions of a basic validator, but also publishes a history archive containing snapshots of the ledger, including all network transactions and their results. ### Wallet An interface that gives a user access to an account stored on the ledger; that access is controlled by the account’s secret key. The wallet allows users to store and manage their assets. ### XLM (lumens) The native currency of the Stellar network. --- ## Interactive Learning Learn by doing in this collection of resources designed to get you up and running on the Stellar network in no time! --- ## Master Rust Smart Contract Development on Stellar Learn to develop secure, efficient smart contracts on the Stellar network using Rust. This hands-on course is perfect for blockchain developers aiming to leverage Stellar's decentralized ecosystem. Check out the course on App World [Master Rust Smart Contract Development on Stellar](https://dapp-world.com/course/master-rust-smart-contract-development-on-stellar)! --- ## Fast, Cheap, and 0ut 0f Control Fast, Cheap, and 0ut 0f Control (FCA00C) is a collection of games and tutorials designed to familiarize you with smart contracts on the Stellar network. The Tutorial walks you through the basics of smart contract development in a series of six quests. This is a great place to get started learning about Stellar smart contracts and smart contract development. Learn more and play all games at the [fca00c site](https://fastcheapandoutofcontrol.com). --- ## Stellar Quest Stellar Quest teaches you about Stellar operations and fundamentals in a gamified experience. It includes three sets of five quests along with an additional three side quests (fee-bumps, minting NFTs, and muxed accounts) and a pioneer quest that teaches you about setting up a wallet. Stellar Quest can be played by using [the Stellar Lab](https://lab.stellar.org/?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;) for non-coders or by using manual code. Learn more and play the games by going to the [Stellar Quest site](https://quest.stellar.org)! --- ## Migrate from Another Chain Adapt your existing projects to work with Stellar. --- ## EVM Networks Adapt your existing Ethereum-compatible smart contracts to work with Stellar. --- ## Introduction to Solidity, Rust, and Soroban # Introduction We are excited to introduce you to Soroban, a powerful smart contracts platform designed to be sensible, built-to-scale, and developer-friendly, with a focus on Rust's performance and safety benefits. With its user-friendly interface and powerful features, Soroban offers an ideal platform for developers who are seeking a more efficient and effective way to build decentralized applications and transition from Solidity to Rust. In this article, we will explore the fundamentals of Solidity, Rust, and Soroban, and guide you through setting up the development environment to start your journey with Soroban's Rust smart contract compatibility. ## Solidity and Dapp Development Solidity is a high-level, statically-typed programming language primarily used for developing smart contracts on the Ethereum Virtual Machine (EVM). It enables the creation of decentralized applications (dapps) that can run on various blockchain platforms, automating complex transactions and interactions without the need for a central authority. ## Rust: Performance and Safety Rust is a systems programming language that emphasizes safety, concurrency, and performance. Its unique features, such as a strong static type system, ownership model, and memory safety guarantees, make it an ideal choice for developing high-performance, secure applications, including smart contracts. ## Soroban: Programmable Logic Soroban, a sensible and built-to-scale smart contracts platform, offers a developer-friendly and batteries-included experience. While it shares the same principles of scalability and practicality as Stellar, Soroban can also function as a standalone platform and integrate with other transaction processors, such as L2s, permissioned ledgers, and even other blockchains. With Soroban, Classic Stellar gains the added functionality of programmable logic in the form of custom operations encapsulated within smart contracts. You can learn more about Soroban's features and benefits in the [Soroban Overview](../../../build/smart-contracts). ## Setting Up the Development Environment for Rust and Soroban To get started with Rust and Soroban, follow the steps on the [Setup Page](../../../build/smart-contracts/getting-started/setup.mdx). Or if you want to jump right in, you can open up our examples in a ready to go dev Environment in Devcontainers by clicking the button below: [![Open in Codespaces](https://github.com/codespaces/badge.svg)][open-in-github-codespaces] [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)][open-in-code-anywhere] [open-in-github-codespaces]: https://github.com/codespaces/new?repo=stellar/soroban-examples&editor=web [open-in-code-anywhere]: https://app.codeanywhere.com/#https://github.com/stellar/soroban-examples # Summary As you embark on your journey with Soroban, remember that the platform is designed to provide a seamless, efficient, and enjoyable experience for developers transitioning from Solidity to Rust. By leveraging Rust's performance and safety benefits, alongside Soroban's advanced tooling, you'll be well-equipped to develop high-quality, efficient, and secure smart contracts. --- ## Smart Contract Development with Soroban and Hardhat In this tutorial, we will discover the similarities in smart contract deployment by examining workflows with Soroban and [Hardhat](https://hardhat.org). We will dive into the intricacies of each framework, learn to write secure and efficient smart contract code, and harness the power of Rust and Soroban to create customized contract logic. ## Table of Contents 1. [Soroban and Hardhat Comparison](#soroban-and-hardhat-comparison) 2. [Hardhat vs Soroban SDKs](#hardhat-vs-soroban-sdks) 3. [Using Rust and Soroban for Smart Contract Development](#developing-smart-contracts-with-rust-and-soroban) 4. [Vault Contract Deployment and Interaction](#vault-contract-deployment-and-interaction) ## Soroban and Hardhat Comparison ### Introduction Soroban and Hardhat are both frameworks that enable developers to build, test, and deploy smart contracts. In this section, we will delve into the similarities and distinctions between these two frameworks. ### Soroban Framework Soroban is a Rust-based framework tailored for developing smart contracts on the Stellar network. Designed as a lightweight framework, with [tools to support developers](../../../tools/developer-tools/README.mdx), Soroban allows developers to develop smart contracts through a simple and intuitive workflow. ### Hardhat Hardhat serves as a development environment for compiling, deploying, testing, and debugging smart contracts for the EVM. It assists developers in managing and automating recurring tasks inherent to building smart contracts. ### Similarities Soroban and Hardhat are powerful frameworks designed to streamline the process of building, testing, and deploying smart contracts. Equipped with a comprehensive suite of tools, these frameworks facilitate the development of smart contracts and their deployment on their respective virtual machines. ### Differences Soroban, with its lightweight design, offers developers an exceptional platform for writing Rust-based smart contracts and deploying them effortlessly on the Stellar network. In contrast, Hardhat serves primarily as a development environment tailored for the Ethereum Virtual Machine, providing a different focus and target audience. ## Hardhat vs. Soroban SDKs Hardhat offers a streamlined workflow for deploying smart contracts on the Ethereum Virtual Machine, with key components such as `ethers.js`, `scripts`, and `testing` playing crucial roles. On the other hand, Soroban presents a compelling alternative, boasting powerful SDKs that facilitate smart contract development and deployment. In the upcoming section, we will delve into [Soroban's SDKs](../../../tools/sdks/README.mdx), drawing comparisons with Hardhat components, and highlighting the unique advantages each platform brings to the table. ### Ethers.js `Ethers.js` is a widely-used `JavaScript` library designed for seamless interaction with the EVM. It offers a user-friendly interface that simplifies connecting to Ethereum nodes, managing accounts, and sending transactions. Additionally, `Ethers.js` provides a robust API for efficient communication with smart contracts. This library is a core component of the Hardhat framework and can be imported into scripts to streamline the deployment of smart contracts. ```javascript const { ethers } = require("hardhat"); async function main() { const [deployer] = await ethers.getSigners(); console.log("Deploying contracts with the account:", deployer.address); } ``` ### Soroban Client Soroban offers a comparable library, [`stellar-sdk`](../../../tools/sdks/client-sdks.mdx#javascript-sdk), that enables seamless interaction smart contracts deployed on the Stellar Network. This library supplies a comprehensive networking layer API for Stellar RPC methods as well as the deprecated Horizon API, simplifying the process of building and signing transactions. Additionally, `stellar-sdk` streamlines communication with RPC instances and supports submitting transactions or querying network state with ease. ### Scripts Hardhat scripts streamline the automation of routine tasks, such as deploying and managing smart contracts. Developers can create these scripts using either JavaScript or TypeScript, catering to their preferred programming style. They are stored in the `scripts` directory of a Hardhat project and can be executed using the `npx hardhat run` command. ```javascript // scripts/deploy.js async function main() { // Compile and deploy the smart contract const MyContract = await ethers.getContractFactory("MyContract"); const myContract = await MyContract.deploy(); console.log("MyContract deployed to:", myContract.address); } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); ``` ### Soroban Scripts Soroban offers an extensive collection of SDKs that include scripting capabilities, ensuring a smooth workflow for deploying and managing smart contracts. Developers can automate tasks such as compiling, deploying, and interacting with smart contracts using a variety of SDKs that support scripting in languages like [`JavaScript`, `TypeScript`, `Python`, and others](../../../tools/sdks/client-sdks.mdx). ```python # This example shows how to deploy a compiled contract to the Stellar network. # https://github.com/stellar/soroban-quest/blob/main/quests/6-asset-interop/py-scripts/deploy-contract.py from stellar_sdk import Network, Keypair, TransactionBuilder from stellar_sdk import xdr as stellar_xdr from stellar_sdk import SorobanServer # TODO: You need to replace the following parameters according to the actual situation secret = "SAAPYAPTTRZMCUZFPG3G66V4ZMHTK4TWA6NS7U4F7Z3IMUD52EK4DDEV" rpc_server_url = "https://soroban-testnet.stellar.org" network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE contract_file_path = "/path/to/compiled/soroban_contract.wasm" kp = Keypair.from_secret(secret) soroban_server = SorobanServer(rpc_server_url) print("installing contract...") source = soroban_server.load_account(kp.public_key) # with open(contract_file_path, "rb") as f: # contract_bin = f.read() tx = ( TransactionBuilder(source, network_passphrase) .set_timeout(300) .append_upload_contract_wasm_op( contract=contract_file_path, # the path to the contract, or binary data source=kp.public_key, ) .build() ) ... ``` ### Testing Hardhat provides a testing framework that allows developers to write tests for their smart contracts. These tests can be written in JavaScript or TypeScript and run using the `npx hardhat test` command. ```javascript // test/my-contract.js const { expect } = require("chai"); describe("MyContract", function () { it("Should return the correct name", async function () { const MyContract = await ethers.getContractFactory("MyContract"); const myContract = await MyContract.deploy(); await myContract.deployed(); expect(await myContract.name()).to.equal("MyContract"); }); }); ``` ### Soroban Testing Soroban enables users to leverage the power of Rust's testing framework to write tests for their smart contracts. These tests can be written in Rust and run using the `cargo test` command. ```rust #![cfg(test)] use super::*; use soroban_sdk::{vec, Env, Symbol, symbol_short}; #[test] fn test() { let env = Env::default(); let contract_id = env.register(HelloContract, ()); let client = HelloContractClient::new(&env, &contract_id); let words = client.hello(&symbol_short!("Dev")); assert_eq!( words, vec![&env, symbol_short!("Hello"), symbol_short!("Dev"),] ); } ``` In summary, while Hardhat provides an excellent environment for deploying smart contracts on the EVM, Soroban's Rust-based framework offers significant advantages in terms of performance, making it an ideal choice for building secure and efficient smart contracts. ## Developing Smart Contracts with Rust and Soroban ### Introduction Now that we've examined the deployment workflow with Hardhat, let's explore developing and deploying smart contracts with Rust and Soroban. The key advantage of using Soroban is its ability to leverage Rust's safety features and performance, making it an excellent choice for developing secure and efficient smart contracts. We've learned that Smart contracts are self-executing contracts that can be programmed to automatically enforce the rules and regulations of a particular agreement. They are a core component of decentralized applications (dApps) and blockchain technology. In this section, we will learn how to use Rust and Soroban to develop and deploy custom smart contract logic. ### Setup If you haven't already setup up the dev environment for Soroban, you can get started by following the steps on the [Setup Page](../../../build/smart-contracts/getting-started/setup.mdx). This project requires using the `soroban_token_contract.wasm` file which you will need to import manually. First, you will need to clone the `main` branch of `soroban-examples` repository: ```bash git clone -b main https://github.com/stellar/soroban-examples ``` Then, navigate to the `soroban-examples/token` directory ```bash cd soroban-examples/token ``` Next, build the Token contract using the following command: ```bash stellar contract build ``` This will build the `soroban_token_contract.wasm` file which you will need to import into your project. The `soroban_token_contract.wasm` file is located in the `soroban-examples/target/wasm32v1-none/release` directory. ``` soroban-examples ├── target │ └── wasm32v1-none │ └── release │ └── soroban_token_contract.wasm └── ``` Once we have the Token, let's create a new smart contract that uses it. ### Writing a Smart Contract Let's start by writing a simple example of a vault contract that allows users to deposit funds and withdraw their funds with generated yield. Here is a breakdown of the contract mechanics - Shares are minted when a user deposits. - The DeFi protocol uses the users' deposits to generate yield. - User burns shares to withdraw their tokens + yield. In a new terminal, let's create a new Rust project by running the following command: ```bash cargo new --lib vault ``` This will create a new Rust project called `vault`. Now let's add the `soroban_token_contract.wasm` file to the `vault` project. To do this, we can drag and drop the file into the `vault` project directory. ![vault-project](/img/migrating/vault-project.png) Next, we'll need to add the Soroban SDK as a dependency. To do this, open the `Cargo.toml` file in your project and ensure that it matches the following: ```toml [package] name = "vault" version = "0.0.0" edition = "2021" publish = false [lib] crate-type = ["cdylib"] [dependencies] soroban-sdk = { version = "27" } num-integer = { version = "0.1.45", default-features = false, features = ["i128"] } [dev-dependencies] soroban-sdk = { version = "27", features = ["testutils"] } [profile.release] opt-level = "z" overflow-checks = true debug = 0 strip = "symbols" debug-assertions = false panic = "abort" codegen-units = 1 lto = true [profile.release-with-logs] inherits = "release" debug-assertions = true ``` In this project we will need to create 3 files: - `src/lib.rs` - This is where we will write our vault smart contract logic. - `src/test.rs` - This is where we will write our tests. - `src/token.rs` - This is file inherits the token contact that we imported earlier. It's also where we will write our token creation logic. To interact with the token contract, we'll use a built in interface that you can find in the `token_interface.rs` tab. The token sets its admin and metadata in a `__constructor` function, which the host runs as part of the deployment itself, and exposes a `mint` function that we will use to mint tokens for our vault contract. Because the constructor runs at deployment, there is no separate initialization call to make — or to front-run. If you want to see the full code of the token contract, you can check it out [here](https://github.com/stellar/soroban-examples/tree/main/token/src). ```rust #![no_std] mod test; mod token; use soroban_sdk::{ contract, contractimpl, contractmeta, Address, BytesN, ConversionError, Env, String, TryFromVal, Val, }; use token::create_contract; #[derive(Clone, Copy)] #[repr(u32)] pub enum DataKey { Token = 0, TokenShare = 1, TotalShares = 2, Reserve = 3, } impl TryFromVal for Val { type Error = ConversionError; fn try_from_val(_env: &Env, v: &DataKey) -> Result { Ok((*v as u32).into()) } } fn get_token(e: &Env) -> Address { e.storage().instance().get(&DataKey::Token).unwrap() } fn get_token_share(e: &Env) -> Address { e.storage().instance().get(&DataKey::TokenShare).unwrap() } fn get_total_shares(e: &Env) -> i128 { e.storage().instance().get(&DataKey::TotalShares).unwrap() } fn get_reserve(e: &Env) -> i128 { e.storage().instance().get(&DataKey::Reserve).unwrap() } fn get_balance(e: &Env, contract: Address) -> i128 { token::Client::new(e, &contract).balance(&e.current_contract_address()) } fn get_token_balance(e: &Env) -> i128 { get_balance(e, get_token(e)) } fn get_balance_shares(e: &Env) -> i128 { get_balance(e, get_token_share(e)) } fn put_token(e: &Env, contract: Address) { e.storage().instance().set(&DataKey::Token, &contract); } fn put_token_share(e: &Env, contract: Address) { e.storage().instance().set(&DataKey::TokenShare, &contract); } fn put_total_shares(e: &Env, amount: i128) { e.storage().instance().set(&DataKey::TotalShares, &amount) } fn put_reserve(e: &Env, amount: i128) { e.storage().instance().set(&DataKey::Reserve, &amount) } fn burn_shares(e: &Env, amount: i128) { let total = get_total_shares(e); let share_contract_id = get_token_share(e); token::Client::new(e, &share_contract_id).burn(&e.current_contract_address(), &amount); put_total_shares(e, total - amount); } fn mint_shares(e: &Env, to: Address, amount: i128) { let total = get_total_shares(e); let share_contract_id = get_token_share(e); token::Client::new(e, &share_contract_id).mint(&to, &amount); put_total_shares(e, total + amount); } // Metadata that is added on to the Wasm custom section contractmeta!( key = "Description", val = "A Vault with a 1% return on investment per deposit." ); pub trait VaultTrait { // Sets the token contract addresses for this vault. The host invokes // `__constructor` as part of the deployment itself. fn __constructor(e: Env, token_wasm_hash: BytesN<32>, token: Address); // Returns the token contract address for the vault share token fn share_id(e: Env) -> Address; // Deposits token. Also mints vault shares for the `from` Identifier. The amount minted // is determined based on the difference between the reserves stored by this contract, and // the actual balance of token for this contract. fn deposit(e: Env, from: Address, amount: i128); // transfers `amount` of vault share tokens to this contract, burns all pools share tokens in this contracts, and sends the // corresponding amount of token to `to`. // Returns amount of token withdrawn fn withdraw(e: Env, to: Address, amount: i128) -> i128; fn get_rsrvs(e: Env) -> i128; } #[contract] struct Vault; #[contractimpl] impl VaultTrait for Vault { fn __constructor(e: Env, token_wasm_hash: BytesN<32>, token: Address) { // The share token's admin and metadata are passed straight to its // `__constructor`, so it is deployed and initialized in a single step. let share_contract_id = create_contract( &e, token_wasm_hash, &token, e.current_contract_address(), 7u32, String::from_str(&e, "Vault Share Token"), String::from_str(&e, "VST"), ); put_token(&e, token); put_token_share(&e, share_contract_id.try_into().unwrap()); put_total_shares(&e, 0); put_reserve(&e, 0); } fn share_id(e: Env) -> Address { get_token_share(&e) } fn deposit(e: Env, from: Address, amount: i128) { // Depositor needs to authorize the deposit from.require_auth(); let token_client = token::Client::new(&e, &get_token(&e)); token_client.transfer(&from, &e.current_contract_address(), &amount); let balance = get_token_balance(&e); mint_shares(&e, from, amount); put_reserve(&e, balance); } fn withdraw(e: Env, to: Address, amount: i128) -> i128 { to.require_auth(); // First transfer the vault shares that need to be redeemed let share_token_client = token::Client::new(&e, &get_token_share(&e)); share_token_client.transfer(&to, &e.current_contract_address(), &amount); let token_client = token::Client::new(&e, &get_token(&e)); token_client.transfer( &e.current_contract_address(), &to, &(&amount + (&amount / &100)), ); let balance = get_token_balance(&e); let balance_shares = get_balance_shares(&e); burn_shares(&e, balance_shares); put_reserve(&e, balance - amount); amount } fn get_rsrvs(e: Env) -> i128 { get_reserve(&e) } } ``` ```rust #![cfg(test)] extern crate std; use crate::{token, VaultClient}; use soroban_sdk::{ symbol_short, testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, Address, BytesN, Env, IntoVal, }; fn create_token_contract<'a>(e: &Env, admin: &Address) -> token::Client<'a> { token::Client::new(e, &e.register_stellar_asset_contract_v2(admin.clone()).address()) } fn create_vault_contract<'a>( e: &Env, token_wasm_hash: &BytesN<32>, token: &Address, ) -> VaultClient<'a> { VaultClient::new( e, &e.register(crate::Vault {}, (token_wasm_hash.clone(), token.clone())), ) } fn install_token_wasm(e: &Env) -> BytesN<32> { soroban_sdk::contractimport!(file = "./soroban_token_contract.wasm"); e.deployer().upload_contract_wasm(WASM) } #[test] fn test() { let e = Env::default(); e.mock_all_auths(); let admin1 = Address::generate(&e); let token = create_token_contract(&e, &admin1); let user1 = Address::generate(&e); let vault = create_vault_contract(&e, &install_token_wasm(&e), &token.address); let contract_share = token::Client::new(&e, &vault.share_id()); let token_share = token::Client::new(&e, &contract_share.address); token.mint(&user1, &200); assert_eq!(token.balance(&user1), 200); token.mint(&vault.address, &100); assert_eq!(token.balance(&vault.address), 100); vault.deposit(&user1, &100); assert_eq!( e.auths(), std::vec![( user1.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( vault.address.clone(), symbol_short!("deposit"), (&user1, 100_i128).into_val(&e) )), sub_invocations: std::vec![AuthorizedInvocation { function: AuthorizedFunction::Contract(( token.address.clone(), symbol_short!("transfer"), (&user1, &vault.address, 100_i128).into_val(&e) )), sub_invocations: std::vec![] }] } )] ); assert_eq!(token_share.balance(&user1), 100); assert_eq!(token_share.balance(&vault.address), 0); assert_eq!(token.balance(&user1), 100); assert_eq!(token.balance(&vault.address), 200); e.budget().reset_unlimited(); vault.withdraw(&user1, &100); assert_eq!( e.auths(), std::vec![( user1.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( vault.address.clone(), symbol_short!("withdraw"), (&user1, 100_i128).into_val(&e) )), sub_invocations: std::vec![AuthorizedInvocation { function: AuthorizedFunction::Contract(( token_share.address.clone(), symbol_short!("transfer"), (&user1, &vault.address, 100_i128).into_val(&e) )), sub_invocations: std::vec![] }] } )] ); assert_eq!(token.balance(&user1), 201); assert_eq!(token_share.balance(&user1), 0); assert_eq!(token.balance(&vault.address), 99); assert_eq!(token_share.balance(&vault.address), 0); } ``` ```rust #![allow(unused)] use soroban_sdk::{xdr::ToXdr, Address, Bytes, BytesN, Env, String}; soroban_sdk::contractimport!(file = "./soroban_token_contract.wasm"); pub fn create_contract( e: &Env, token_wasm_hash: BytesN<32>, token: &Address, admin: Address, decimal: u32, name: String, symbol: String, ) -> Address { let mut salt = Bytes::new(e); salt.append(&token.to_xdr(e)); let salt = e.crypto().sha256(&salt); e.deployer() .with_current_contract(salt) // `deploy_v2` invokes the deployed contract's `__constructor` with // these arguments, in the same transaction as the deployment. .deploy_v2(token_wasm_hash, (admin, decimal, name, symbol)) } ``` ```rust //! This contract demonstrates a sample implementation of the Soroban token //! interface. use crate::admin::{read_administrator, write_administrator}; use crate::allowance::{read_allowance, spend_allowance, write_allowance}; use crate::balance::{is_authorized, write_authorization}; use crate::balance::{read_balance, receive_balance, spend_balance}; use crate::event; use crate::metadata::{read_decimal, read_name, read_symbol, write_metadata}; use crate::storage_types::INSTANCE_TTL_EXTEND_AMOUNT; use soroban_sdk::{contract, contractimpl, Address, Env, MuxedAddress, String}; use soroban_token_sdk::TokenMetadata; pub trait TokenTrait { fn allowance(e: Env, from: Address, spender: Address) -> i128; fn approve(e: Env, from: Address, spender: Address, amount: i128, live_until_ledger: u32); fn balance(e: Env, id: Address) -> i128; fn spendable_balance(e: Env, id: Address) -> i128; fn authorized(e: Env, id: Address) -> bool; fn transfer(e: Env, from: Address, to_muxed: MuxedAddress, amount: i128); fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128); fn burn(e: Env, from: Address, amount: i128); fn burn_from(e: Env, spender: Address, from: Address, amount: i128); fn clawback(e: Env, from: Address, amount: i128); fn set_authorized(e: Env, id: Address, authorize: bool); fn mint(e: Env, to: Address, amount: i128); fn set_admin(e: Env, new_admin: Address); fn decimals(e: Env) -> u32; fn name(e: Env) -> String; fn symbol(e: Env) -> String; } fn check_nonnegative_amount(amount: i128) { if amount < 0 { panic!("negative amount is not allowed: {}", amount) } } #[contract] pub struct Token; // `__constructor` is not part of the token interface — it is an inherent // function on the contract type that the host invokes once, at deployment. // Because it can only ever run as part of the deploy, it needs no // "already initialized" guard. #[contractimpl] impl Token { pub fn __constructor(e: Env, admin: Address, decimal: u32, name: String, symbol: String) { if decimal > 18 { panic!("Decimal must not be greater than 18"); } write_administrator(&e, &admin); write_metadata( &e, TokenMetadata { decimal, name, symbol, }, ) } } #[contractimpl] impl TokenTrait for Token { fn allowance(e: Env, from: Address, spender: Address) -> i128 { e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); read_allowance(&e, from, spender).amount } fn approve(e: Env, from: Address, spender: Address, amount: i128, live_until_ledger: u32) { from.require_auth(); check_nonnegative_amount(amount); e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); write_allowance(&e, from.clone(), spender.clone(), amount, live_until_ledger); event::approve(&e, from, spender, amount, live_until_ledger); } fn balance(e: Env, id: Address) -> i128 { e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); read_balance(&e, id) } fn spendable_balance(e: Env, id: Address) -> i128 { e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); read_balance(&e, id) } fn authorized(e: Env, id: Address) -> bool { e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); is_authorized(&e, id) } // The destination is a `MuxedAddress`, so a single account can be shared by // many end users, each identified by a muxed ID. Resolve it to the // underlying `Address` before touching balances. fn transfer(e: Env, from: Address, to_muxed: MuxedAddress, amount: i128) { from.require_auth(); check_nonnegative_amount(amount); e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); spend_balance(&e, from.clone(), amount); let to: Address = to_muxed.address(); receive_balance(&e, to.clone(), amount); event::transfer(&e, from, to, amount); } fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128) { spender.require_auth(); check_nonnegative_amount(amount); e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); spend_allowance(&e, from.clone(), spender, amount); spend_balance(&e, from.clone(), amount); receive_balance(&e, to.clone(), amount); event::transfer(&e, from, to, amount) } fn burn(e: Env, from: Address, amount: i128) { from.require_auth(); check_nonnegative_amount(amount); e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); spend_balance(&e, from.clone(), amount); event::burn(&e, from, amount); } fn burn_from(e: Env, spender: Address, from: Address, amount: i128) { spender.require_auth(); check_nonnegative_amount(amount); e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); spend_allowance(&e, from.clone(), spender, amount); spend_balance(&e, from.clone(), amount); event::burn(&e, from, amount) } fn clawback(e: Env, from: Address, amount: i128) { check_nonnegative_amount(amount); let admin = read_administrator(&e); admin.require_auth(); e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); spend_balance(&e, from.clone(), amount); event::clawback(&e, admin, from, amount); } fn set_authorized(e: Env, id: Address, authorize: bool) { let admin = read_administrator(&e); admin.require_auth(); e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); write_authorization(&e, id.clone(), authorize); event::set_authorized(&e, admin, id, authorize); } fn mint(e: Env, to: Address, amount: i128) { check_nonnegative_amount(amount); let admin = read_administrator(&e); admin.require_auth(); e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); receive_balance(&e, to.clone(), amount); event::mint(&e, admin, to, amount); } fn set_admin(e: Env, new_admin: Address) { let admin = read_administrator(&e); admin.require_auth(); e.storage().instance().extend_ttl(INSTANCE_TTL_EXTEND_AMOUNT); write_administrator(&e, &new_admin); event::set_admin(&e, admin, new_admin); } fn decimals(e: Env) -> u32 { read_decimal(&e) } fn name(e: Env) -> String { read_name(&e) } fn symbol(e: Env) -> String { read_symbol(&e) } } ``` Now that we've added these files to our project, let's break down what happens in the `lib.rs` file above and discover how "yield" is generated from our vault contract First, let's take a look at what happens when a user deposits tokens into the vault contract. ```rust fn deposit(e: Env, from: Address, amount: i128) { // Depositor needs to authorize the deposit from.require_auth(); let token = token::Client::new(&e, &get_token(&e)); token.transfer(&from, &e.current_contract_address(), &amount); // Now calculate how many new vault shares to mint let balance = get_token_balance(&e); let shares = amount; mint_shares(&e, from, shares); put_reserve(&e, balance + shares); } ``` - The `deposit` function is called by the depositor to deposit tokens into the vault contract. - The `transfer` method of the `token_client` instance transfers tokens from the depositor to the vault contract. - The current token balance and total shares issued by the vault contract are obtained using the `get_token_balance` and `get_total_shares` functions, respectively. - `mint_shares` is called to issue new shares to the depositor and updates the total shares issued by the vault contract. - `put_reserve` stores the current token balance in a reserved location. If the user were to call the `deposit` method with 100 tokens, the following would happen: - 100 tokens would be transferred from the depositor to the vault contract. - The current token balance would be stored in a reserved location. - The total shares issued by the vault contract would be updated to 100. - 100 shares would be issued to the depositor. Now let's see what happens when a user withdraws tokens from the vault. ```rust fn withdraw(e: Env, to: Address, amount: i128) -> i128 { to.require_auth(); // First transfer the vault shares that need to be redeemed let share_token_client = token::Client::new(&e, &get_token_share(&e)); share_token_client.transfer(&to, &e.current_contract_address(), &amount); // Calculate total amount including yield let total_amount = amount + (amount / 100); let token_client = token::Client::new(&e, &get_token(&e)); token_client.transfer(&e.current_contract_address(), &to, &total_amount); let balance = get_token_balance(&e); let balance_shares = get_balance_shares(&e); burn_shares(&e, balance_shares); put_reserve(&e, balance); // Update the reserve with the actual balance total_amount } ``` - The `withdraw` function is called by the withdrawer to withdraw tokens from the vault contract. - The `transfer` method of the `share_token_client` instance transfers shares from the withdrawer to the vault contract. - The `transfer_token` method of the `token_client` instance transfers tokens from the vault contract to the withdrawer. - `burn_shares` is called to burn the shares that were transferred to the vault contract. - `put_reserve` stores the current token balance in a reserved location. - Returns the total amount of tokens withdrawn by the user. > _Note_ : In the withdrawal function, you'll notice that the transfer amount is defined as `&(&amount + (&amount / &100))`. This is a simple yield calculation that assumes the yield to be 1% of the amount being withdrawn. However, it's important to note that this is a very simplistic approach and may not be suitable for production-grade systems. In reality, yield calculations are more complex and involve various factors such as market conditions, risk management, and fees. If the user were to call the `withdraw` method with 100 shares, the following would happen: - 100 shares would be transferred from the withdrawer to the vault contract. - The current token balance would be stored in a reserved location. - 100 shares would be burned. - 100 + (100/100) tokens would be transferred from the vault contract to the withdrawer. ### Testing To test the vault contract, we will can simply run the following command in our terminal from our vault contract directory: ```bash #cd vault cargo test ``` This will run the tests that we've written in the `src/test.rs` file. ```bash running 1 test test test::test ... ok ``` ### Vault Contract Deployment and Interaction Now that we have a working vault contract, we can deploy it to a network and interact with it. This section requires you to have a funded Keypair to use with Stellar's Testnet. You can create and fund one using the [Stellar Lab](https://lab.stellar.org/account/create). Below you will find a series of commands that will help you build, deploy and interact with the vault and token contracts. You can use them to follow along as we walk through the process of building, deploying, and interacting with the contracts. It may behoove you to keep these commands in a `scripts` directory in your project. This way, you can easily run them from your terminal. > _Note_ : If you decide to use scripts, be sure to double-check your import paths. ```bash stellar contract build ``` Everything after the `--` is passed to the token's `__constructor`, so the token is deployed and initialized in one transaction. ```bash stellar contract deploy \ --wasm soroban_token_contract.wasm \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ --admin \ --decimal 18 \ --name \ --symbol ``` ```bash stellar contract invoke \ --id \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ mint \ --to \ --amount 100 ``` ```bash stellar contract invoke \ --id \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ balance \ --id ``` ```bash stellar contract upload \ --wasm soroban_token_contract.wasm \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' ``` Everything after the `--` is passed to the vault's `__constructor`, so `upload.sh` must be run first to get the token Wasm hash. ```bash stellar contract deploy \ --wasm target/wasm32v1-none/release/vault.wasm \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ --token_wasm_hash 73593275ee3bcacc2aef8d641a1d5108618064bdfff84a826576b8caff395add \ --token ``` ```bash stellar contract invoke \ --id \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ share_id ``` ```bash stellar contract invoke \ --id \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ deposit \ --from \ --amount 100 ``` ```bash stellar contract invoke \ --id \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ get_rsrvs ``` ```bash stellar contract invoke \ --id \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ withdraw \ --to \ --amount 100 ``` First, we need to build the vault contract. We can do this by running the `build.sh` script from our vault directory. ```bash ##cd vault stellar contract build ``` Next, we need to deploy the token contract. We can do this by running the `deploy_token.sh` script. The arguments after the `--` are the token's constructor arguments, so this single command both deploys the token and sets its admin and metadata. ```bash stellar contract deploy \ --wasm soroban_token_contract.wasm \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ --admin \ --decimal 18 \ --name \ --symbol ``` We should receive an output with the token contract ID. We will need this ID for the next step. There is no follow-up initialization call — the constructor already ran. ```bash CBYMG7OPIT67AG4S2FZU7LAYCXUSXEHRGHLDE6H26VCVWNOV7QUQTGNU ``` Next, we need to get the Wasm hash of the token contract, which is an argument to the vault's constructor. We can do this by running the `upload.sh` script. ```bash stellar contract upload \ --wasm soroban_token_contract.wasm \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' ``` We should receive the Wasm hash of the token contract. ```bash 6b7e4bfbf47157a12e24e564efc1f9ac237e7ae6d7056b6c2ab47178b9e7a510 ``` Now we can deploy the vault contract by running the `deploy_vault.sh` script, passing the token Wasm hash and the token contract address after the `--` as the constructor arguments. ```bash stellar contract deploy \ --wasm target/wasm32v1-none/release/vault.wasm \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ --token_wasm_hash 6b7e4bfbf47157a12e24e564efc1f9ac237e7ae6d7056b6c2ab47178b9e7a510 \ --token ``` We should receive an output with the vault contract ID. We will need this ID for the next step. ```bash CBBPLE6TGYOMO5HUF2AMYLSYYXM2VYZVAVYI5QCCM5OCFRZPBE2XA53F ``` Next, we will mint some tokens to **both** our User Account and Vault Contract addresses. We can do this by running the `mint.sh` script. ```bash stellar contract invoke \ --id \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ mint \ --to \ --amount 100 ``` After submitting the transaction, we can check the balance of the account. We can do this by running the `balance.sh` script. ```bash stellar contract invoke \ --id \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ balance \ --id ``` We should receive an output with the balance of the account. ```bash 100 ``` Now we can deposit some tokens into our vault. We can do this by running the `deposit.sh` script. ```bash stellar contract invoke \ --id \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ deposit \ --from \ --amount 100 ``` After submitting the transaction, we can check the reserves of the vault. We can do this by running the `reserves.sh` script. ```bash stellar contract invoke \ --id \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ get_rsrvs ``` We should receive an output with the reserves of the vault. ```bash "200" ``` 100 from the deposit and 100 from the mint. Now we can withdraw some tokens from the vault. We can do this by running the `withdraw.sh` script. ```bash stellar contract invoke \ --id \ --source-account \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase 'Test SDF Network ; September 2015' \ -- \ withdraw \ --to \ --amount 100 ``` We should receive an output with the withdrawal amount. ```bash "100" ``` Now is a good time to check our account balance again. We can do this by running the `balance.sh` script. We should see our balance has increased the amount we withdrew plus yield (amount/100) or %1 of our withdraw amount. ```bash 101 ``` And finally, we can check the reserves of the vault again. We can do this by running the `get_rsrv.sh` script. We should see the reserves of the vault have decreased by the amount we withdrew + yield. ```bash "99" ``` And there you have it! You have successfully deployed and interacted with the vault contract! Its important to note that this is not a production ready contract and is only meant to demonstrate the capabilities of the Soroban smart contract platform. We hope to see much more complex yield contracts deployed with Soroban in the future, and we hope you will be a part of it! --- ## Advanced Smart Contract Concepts with Solidity and Rust In this tutorial, we will cover advanced Solidity and Rust concepts such as inheritance, interfaces, libraries, and modifiers. Additionally, we will learn how to write safe and efficient Rust code for smart contracts. Finally, we will learn how to convert common Solidity concepts to Rust. ## Table of Contents 1. [Advanced Solidity Concepts](#advanced-solidity-concepts) 2. [Advanced Rust Concepts](#advanced-rust-concepts) 3. [Writing Safe and Efficient Rust Code for Smart Contracts](#writing-safe-and-efficient-rust-code-for-smart-contracts) 4. [Solidity to Soroban: Common Concepts and Best Practices](#solidity-to-soroban-common-concepts-and-best-practices) ## Advanced Solidity Concepts ### Inheritance In Solidity, smart contracts can inherit properties and functions from other contracts. This is achieved using the `is` keyword. Here is an example of a parent contract that defines a function called `messageFromParent` that returns a string: ```solidity contract Parent { function messageFromParent() public pure returns (string memory) { return "Hello from Parent"; } } contract Child is Parent { function messageFromChild(string memory newMessage) public pure returns (string memory) { string memory messageFromParent = messageFromParent(); return string(abi.encodePacked(messageFromParent,', ', newMessage)); } } ``` In this example, the `Child` contract inherits the `messageFromParent` function from the `Parent` contract. The `Child` contract can then call the `messageFromParent` function directly. ### Interfaces Interfaces are similar to contracts, but they cannot have any function implementations. They only contain function signatures. Contracts can implement interfaces using the `is` keyword, similar to inheritance. Here is an example of an interface that defines a function called `doSomething` that returns a `uint256`: ```solidity interface SomeInterface { function doSomething() external returns (uint256); } contract SomeContract is SomeInterface { uint256 private counter; function doSomething() external override returns (uint256) { counter += 1; return counter; } } ``` In this example, the `SomeContract` contract implements the `SomeInterface` interface. Its implementation returns a `u256` that is incremented each time the `doSomething` function is called. ### Libraries Libraries are similar to contracts, but they cannot have any state variables. They are used to store reusable code that can be used by other contracts. Libraries are deployed once and can be used by multiple contracts. They are defined using the `library` keyword. They are invoked by using the `using` keyword. ```solidity library SafeMath { function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a, "Addition overflow"); return c; } } contract MyContract { using SafeMath for uint256; uint256 public value; function increment(uint256 amount) public { value = value.add(amount); } } ``` In this example, the `SafeMath` library is used in the `increment` function. The `increment` function uses the `add` function from the `SafeMath` library to increment the `value` variable. ### Modifiers Modifiers are used to change the behavior of functions in a declarative way. They are defined using the `modifier` keyword. Modifiers can be used to perform common checks such as validating inputs, checking permissions, and more. ```solidity contract Ownable { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Caller is not the owner"); _; } } contract MyContract is Ownable { function doSomething() public onlyOwner { // This function can only be called by the owner of the contract } } ``` In this example, the `onlyOwner` modifier is used to restrict access to the `doSomething` function. The `doSomething` function can only be called by the `owner` of the contract which was defined during deployment as `msg.sender`. ## Advanced Rust Concepts ### Crates A crate in Rust is a collection of precompiled programs, scripts, or routines that can be easily reused by programmers when writing code. This allows them to avoid reinventing the wheel by not having to implement the same logic or program multiple times. There are two types of crates in Rust: [Binary crates and Library crates](https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html). Binary crates are crates that can be executed as standalone programs. Library crates are crates that are meant to be used by other programs. Library crates can be imported into other programs using the `use` keyword. Here is an example of a workflow that implements allocation (`alloc`) logic within a smart contract: First a user would include the `alloc` crate in their `Cargo.toml` file: ```toml [dependencies] soroban-sdk = { workspace = true, features = ["alloc"] } [dev_dependencies] soroban-sdk = { workspace = true, features = ["testutils", "alloc"] } ``` Then they would import the `alloc` crate into their smart contract: ```rust // Imports #![no_std] use soroban_sdk::{contractimpl, Env}; extern crate alloc; #[contract] pub struct AllocContract; #[contractimpl] impl AllocContract { /// Allocates a temporary vector holding values (0..count), then computes and returns their sum. pub fn sum(_env: Env, count: u32) -> u32 { let mut v1 = alloc::vec![]; (0..count).for_each(|i| v1.push(i)); let mut sum = 0; for i in v1 { sum += i; } sum } } ``` In this example, the `alloc` crate is imported into the smart contract using the `extern crate alloc;` statement. The `alloc` crate is then used to create a temporary vector that holds values from 0 to `count`. The values in the vector are then summed and returned. For more details on how to use the `alloc` crate, including a hands-on practical exercise, visit the [alloc example contract](../../../build/smart-contracts/example-contracts/alloc.mdx#how-it-works). #### Inheriting Functionality from Other Crates We can illustrate another example of inheritance by importing functionality into a crate from other crates in the same project in the following example: Below is a function from the [`event.rs`](https://github.com/stellar/soroban-examples/tree/main/events/src) file from our [Token](https://github.com/stellar/soroban-examples/tree/main/token) example. ```rust use soroban_sdk::{Address, Env, Symbol}; ... pub(crate) fn mint(e: &Env, admin: Address, to: Address, amount: i128) { let topics = (symbol_short!("mint"), admin, to); e.events().publish(topics, amount); } ``` This function will publish a mint event to the blockchain with the following output: ``` Emit event with topics = ["mint", admin: Address, to: Address], data = amount: i128 ``` We'll also use a function from our [`admin.rs`](https://github.com/stellar/soroban-examples/blob/main/token/src/admin.rs) file. ```rust // Metering: covered by components pub fn read_administrator(e: &Host) -> Result { let key = DataKey::Admin; let rv = e.get_contract_data(key.try_into_val(e)?)?; Ok(rv.try_into_val(e)?) } ``` This function returns a `Result` object that contains the administrator's address. Lastly, we'll implement a function from our [`balance.rs`](https://github.com/stellar/soroban-examples/blob/main/token/src/balance.rs) file. ```rust pub fn receive_balance(e: &Env, addr: Address, amount: i128) { let balance = read_balance(e, addr.clone()); if !is_authorized(e, addr.clone()) { panic!("can't receive when deauthorized"); } write_balance(e, addr, balance + amount); } ``` This function writes an amount to an address' balance. The `event.rs` is imported into the [`contract.rs`](https://github.com/stellar/soroban-examples/blob/main/token/src/contract.rs) file which holds the logic for our token contract. ```rust //contract.rs //imports use crate::event; use crate::admin::{read_administrator}; use crate::balance::{receive_balance}; // trait logic pub trait TokenTrait { fn mint(e: Env, to: Address, amount: i128); } // struct logic #[contract] pub struct Token; // impl logic #[contractimpl] impl TokenTrait for Token { fn mint(e: Env, to: Address, amount: i128) { check_nonnegative_amount(amount); let admin = read_administrator(&e); admin.require_auth(); receive_balance(&e, to.clone(), amount); event::mint(&e, admin, to, amount); } } ``` As you can see, the `event.rs`, `admin.rs`, and `balance.rs` files are imported into the `contract.rs` file using the `use` keyword. This allows us to use the functions from those files in the `mint` function of our `contract.rs` file. #### Inheriting Functionality using `contractimport!` The Soroban Rust SDK provides a powerful macro, [`contractimport`](https://docs.rs/soroban-sdk/latest/soroban_sdk/macro.contractimport.html), which allows a user to import a contract from its Wasm file, generating a client, types, and constant holding the contract file. Here is an example of how to use the `contractimport` macro taken from the token.rs file from our [Liquidity Pool example](https://github.com/stellar/soroban-examples/blob/main/liquidity_pool): First, we see that the wasm file from the [previously built token example](https://github.com/stellar/soroban-examples/blob/main/token) is imported into the `token.rs` file using the `contractimport` macro: ```rust //token.rs soroban_sdk::contractimport!( file = "../token/target/wasm32v1-none/release/soroban_token_contract.wasm" ); ``` We see then that our token contract is imported into our [`lib.rs`](https://github.com/stellar/soroban-examples/blob/main/liquidity_pool/src/lib.rs) file and a `Client` is generated for us to access functionality from the token contract: ```rust //lib.rs mod token; fn get_balance(e: &Env, contract_id: BytesN<32>) -> i128 { token::Client::new(e, &contract_id).balance(&e.current_contract_address()) } fn transfer(e: &Env, contract_id: BytesN<32>, to: Address, amount: i128) { token::Client::new(e, &contract_id).transfer(&e.current_contract_address(), &to, &amount); } struct LiquidityPool; #[contractimpl] impl LiquidityPoolTrait for LiquidityPool { let token_a_client = token::Client::new(&e, &get_token_a(&e)); } ``` In the above example, we use `contractimport` to interact with the token file via a `Client` that was generated for the `token` module. This `Client` was created using a Contract trait that matches the interface of the contract, a ContractClient struct that contains functions for each function in the contract, and types for all contract types defined in the contract. #### A Note on Inheritance and Composability While we've been using the term "inheritance" to help make the transition from Solidity smoother, let's clarify an important aspect of Rust: it does not support inheritance as we traditionally understand it. Instead, Rust practices "composability", meaning it uses functions from different crates, which are akin to packages, in a modular fashion. So, when we discuss `contractimport!`, we're actually observing composability in action, not "inheritance". Rust does not foster the "is a" relationship inherent in OOP languages. Instead, it enables us to reuse and assemble code effectively across different scopes. This is a technical truth that is important to understand; however, it's worth noting that this fact doesn't impact the practical usage of Soroban throughout this guide. ### Modules In Rust, modules consist of a cohesive set of related functions and types that are often organized together for better organization and reusability. These modules can be reused across multiple projects by publishing them as crates. Here is an example of a module that implements `SafeMath` logic with an `add` function: ```rust #![no_std] mod safe_math // mod safe_math { // pub fn add(a: u32, b: u32) -> Result { // a.checked_add(b).ok_or("Addition overflow") // } // } // Imports use soroban_sdk::{contractimpl, Env}; use safe_math::add; pub trait MathContract { fn add(&self, env: Env, a: u32, b: u32) -> u32; } #[contract] pub struct Adder; impl MathContract for Adder { fn add(&self, _env: Env, a: u32, b: u32) -> u32 { add(a, b).unwrap() } } #[contractimpl] impl Adder {} // test module #[cfg(test)] mod test; ``` Notice that we use the `checked_add` function from the standard library to ensure that the addition does not overflow. This is important because if the addition overflows, it could lead to unexpected behavior in the contract. Even when Rust code is compiled with the `#![no_std]` flag, it is still possible to use some of the standard library's features, such as the `checked_add` function. This is because Rust provides the option to selectively import modules and functions from the standard library, allowing developers to use only the specific features they need. ### Traits Rust does not have a built-in modifier system like Solidity. However, you can achieve similar functionality using `traits` and their implementations. In the example below, we will illustrate the inheritance of traits using the `Ownable` trait. ```rust #![no_std] // Imports use soroban_sdk::{contracttype, Address}; // Define the `Ownable` trait trait Ownable { fn is_owner(&self, owner: &Address) -> bool; } // Implement the `Ownable` trait for the `OwnableContract` struct impl Ownable for OwnableContract { fn is_owner(&self, owner: &Address) -> bool { self.owner == *owner } } // Define a modifier that requires the caller to be the owner of the contract fn only_owner(contract: &OwnableContract, owner: &Address) -> bool { contract.is_owner(owner) } // Implement the contract for the `OwnableContract` struct #[contracttype] // Define the `OwnableContract` struct pub struct OwnableContract { owner: Address, number: u32, } impl OwnableContract { // Define a public method that requires the caller to be the owner of the contract pub fn change_number(&mut self, new_number: u32) { if only_owner(self, &self.owner) { self.number = new_number; } } } #[cfg(test)] mod test; ``` Here's a breakdown of the code above: - First, we define the `Ownable` trait, which defines a single method called `is_owner`. This method takes an `Address` as an argument and returns a boolean value indicating whether or not the address is the owner of the contract. - Next, we implement the `Ownable` trait for the `OwnableContract` struct. This allows us to use the `is_owner` method on instances of the `OwnableContract` struct. - Then, we define a "modifier" called `only_owner` that takes an instance of the `OwnableContract` struct and an `Address` as arguments. This "modifier" returns a boolean value indicating whether or not the address is the owner of the contract. - Finally, we implement the contract for the `OwnableContract` struct. This allows only the `owner` of the contract to use the `change_number` method on instances of the `OwnableContract` struct. It's worth mentioning that the Soroban Rust SDK comes with several built-in requirements that developers can use, such as the [`require_auth`](https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Address.html#method.require_auth) method provided by the `Address` struct. ### Interfaces Interfaces are an essential part of building smart contracts with Soroban. There are many types of smart contract interfaces, and each has a specific purpose. One example of an interface built with Soroban is the [Token Interface](../../../tokens/token-interface.mdx). This interface ensures that tokens deployed on Soroban are interoperable with Soroban's built-in tokens (such as the Stellar Asset Contract). The Token Interface consists of three compatibility requirements: - `function interface` - `authorization` - `events` For more information on smart contract interfaces built with Soroban, including the Token Interface, visit the [tokens section](../../../tokens/README.mdx) of the documentation. ## Writing Safe and Efficient Rust Code for Smart Contracts When writing Rust code for smart contracts, it's important to focus on safety and efficiency. Some tips include: - Use the [`Result`](https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html) type to handle errors in a safe and predictable way. In smart contracts, it's important to avoid panicking, as this can lead to unpredictable behavior. Instead, Result can be used to handle errors and ensure that the contract behaves as expected. In the example below, the `add` function returns a `Result` type, which can either be `Ok` or `Err`. If the addition does not overflow, the function returns `Ok`, otherwise it returns `Err`. ```rust pub fn add(a: u32, b: u32) -> Result { a.checked_add(b).ok_or("Addition overflow") } ``` - Use the `checked_` family of functions, such as `checked_add`, `checked_sub`, etc., to perform arithmetic operations in a safe and efficient manner. These functions check for overflows and underflows and return an error if one occurs. In the example below, the `add` function uses the `checked_add` function to perform the addition. If the addition overflows, the function returns an error. ```rust pub fn add(a: u32, b: u32) -> u32 { a.checked_add(b).expect("Addition overflow") } ``` - Use `cargo` and `clippy` to enforce code quality, style, and efficiency in Rust. `cargo` is Rust's package manager and provides a number of tools for building and testing Rust code. `clippy` is a linter that can help identify potential issues in the code, such as unused variables or functions that could be optimized. To use clippy with cargo, you'll first need to install it. You can do this by running the following command in your terminal: ```bash cargo install clippy ``` Once clippy is installed, you can run it by running the following command in your terminal: ```bash cargo clippy ``` This will run clippy on your entire project, checking for potential issues and providing suggestions for improvement. Clippy will output any issues it finds, along with suggestions for how to fix them. - Use `cargo` and `rustfmt` to enforce code style. `rustfmt` is a tool that can automatically format Rust code according to the Rust style guide. This can help ensure that the code is consistent and easy to read. To use rustfmt with cargo, you'll first need to install it. You can do this by running the following command in your terminal: ```bash cargo install rustfmt ``` Once rustfmt is installed, you can run it by running the following command in your terminal: ```bash cargo fmt ``` Before: ```rust fn main() { let x=5; if x==5 { println!("Hello, world!"); } } ``` After: ```rust fn main() { let x = 5; if x == 5 { println!("Hello, world!"); } } ``` ## Solidity to Soroban: Common Concepts and Best Practices In this section we will explore key Solidity concepts and provide their Soroban equivalents. We will discuss the following topics: - Message Properties - Error Handling - Address-related functionality - Function visibility specifiers - Time-based variables ### Message Properties The Soroban Rust SDK and Solidity provide a number of message properties that can be used to access information about the current transaction. These properties include: #### Solidity - `msg.sender`: The address of the account that sent the transaction. - `msg.value`: The amount of Ether sent with the transaction. - `msg.data`: The data sent with the transaction. Here's a simple example of a smart contract that demonstrates the use of each ```solidity pragma solidity ^0.8.0; contract SimpleContract { address public sender; uint public value; bytes public data; // Caller must send Ether and data to this function. // This function will store the sender, value, and data. function sendData(bytes calldata _data) external payable { sender = msg.sender; value = msg.value; data = _data; } } ``` These are a part of Solidity's global variables, which are accessible from any function in the contract. #### Soroban In contrast to Solidity's global variables, Soroban relies on passing an [`Env`](https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Env.html) argument to all functions which provides access to the environment the contract is executing within. The `Env` provides access to information about the currently executing contract, who invoked it, contract data, functions for signing, hashing, etc. For instance, you would use `env.storage().persistent().get(key)` to access a `persistent` target value from the contract's [storage](https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Env.html). Read more about the different storage types [here](../../fundamentals/contract-development/storage/persisting-data.mdx). - `env.storage()` is used to get a struct for accessing and updating contract data that has been stored. - Used as `env.storage().persistent().get()` or `env.persistent().storage().set()`. - Additionally, we utilize the `clone()` method, a prevalent trait in Rust that allows for the explicit duplication of an object. See the example below for implementations of `env.storage()` and `clone()` - `env.storage().persistent().set()` ```rust use soroban_sdk::{Env, Symbol}; pub fn set_storage(env: Env) { let key = symbol_short!("key"); let value = symbol_short!("value"); env.storage().persistent().set(&key, &value); } ``` - `env.storage().persistent().get()` ```rust use soroban_sdk::{Env}; pub fn get_storage(env: Env) -> value { env.storage().persistent().get(&key); } ``` - `clone()` ```rust use soroban_sdk::{Env, Address}; pub fn return_user(user: Address) -> Address { let user_address: Address = user.clone(); user_address } ``` ### Error Handling The Soroban Rust SDK and Solidity provide a number of ways to handle errors. These include: #### Solidity Solidity provides a `require` function that can be used to check for certain conditions and revert the transaction if they are not met. For example, the following code sets a minimum value for the amount of Ether sent with the transaction: ```solidity function deposit() public payable { require(msg.value >= 1 ether, "Not enough Ether sent"); // ... } ``` #### Soroban The [panic!](https://doc.rust-lang.org/book/ch09-00-error-handling.html) macro serves as Rust's error-handling mechanism, which closely resembles the `require` function in Solidity. ```rust pub fn simple_deposit(amount: u32) { if amount < 1_000_000 { panic!("amount too low"); } // ... } ``` ### Address-Related Functionality Both Soroban and Solidity provide provide a number of functions for working with addresses. These functions include: #### Solidity - `address(this)`: Returns the address of the current contract. - `address payable(this)`: Returns the address of the current contract as a payable address. - `address(address)`: Returns the address of the specified account. - `address payable(address)`: Returns the address of the specified account as a payable address. Below is an example of a smart contract that illustrates how contracts can be retrieved ```solidity pragma solidity ^0.8.0; contract SimpleContract { address public contractAddress = address(this); address public randomAddress = 0x1234567890123456789012345678901234567890; address public payableAddress = payable(address(this)); address public payableRandomAddress = payable(0x1234567890123456789012345678901234567890); } ``` There would be no difference in appearance between a regular address and a payable address in Solidity. #### Soroban - `e.current_contract_address()`: Returns the Address object corresponding to the current executing contract. The `Env` not only provides essential information about the currently executing contract and its invoker, but also offers access to contract data and functions for signing, hashing, and more. The construction or conversion of most types in Soroban requires access to an `Env` instance. Here is an example of a smart contract that illustrates how contracts can be retrieved: ```rust #![no_std] use soroban_sdk::{contractimpl, log, Address, Env, Symbol}; #[contract] pub struct SimpleContract; #[contractimpl] impl SimpleContract { ///Example contract for returning a contract Address. pub fn return_address(env: Env) -> Address { let current_contract_address = env.current_contract_address(); current_contract_address } } ``` #### Why Soroban Differs Soroban has some differences from Solidity in terms of addresses and other functionalities. These differences arise due to the design principles and goals of Soroban. One significant difference is the use of the `Env` object in Soroban. The `Env` object encapsulates various functionalities related to contract execution, data access, and more. It provides a unified interface for interacting with the Soroban environment within the context of a contract. By utilizing the `Env` object, Soroban enables a more modular and flexible approach to contract development. To further explain, the `Env` type offers a gateway to the environment where the contract operates. It provides information about the ongoing contract, the entity invoking it, contract data, and functions for signing, hashing, and so forth. Most types demand access to an `Env` for their construction or conversion. Meanwhile, the `Address` object serves as a potent tool for authentication and [authorization](#authorization). For instance, it can be used to authorize token transfers, acting as a security gatekeeper within the system. This feature amplifies the functionality of addresses in Soroban, making them not just a means of identification or storage, but also a key player in verifying and authorizing transactions. ### Function Visibility Specifiers The Soroban Rust SDK and Solidity provide a number of function visibility specifiers that can be used to control who can call a function. These specifiers include: #### Solidity - `public`: Anyone can call the function. - `external`: Only other contracts can call the function. - `internal`: Only the current contract and contracts that inherit from it can call the function. - `private`: Only the current contract can call the function. Here is an example of a smart contract that illustrates how function visibility is used in Solidity: ```solidity pragma solidity ^0.8.0; contract SimpleContract { function publicFunction() public {} function externalFunction() external {} function internalFunction() internal {} function privateFunction() private {} } ``` #### Soroban - `pub`: The item (function, struct, etc.) is accessible from any module or scope. - `pub(crate)`: The item is accessible only within the current crate. - `pub(super)`: The item is accessible only within its parent module. - `pub(in path::to::module)`: The item is accessible only within the specified module path. - `private`: The item is not marked as pub and is therefore private to its own module, meaning it can only be accessed within the same module and is not accessible from outside the module. Here is an example of a module that illustrates how function visibility is used in Soroban: ```rust #![no_std] use soroban_sdk::{contractimpl, Env}; mod outer { pub struct PublicStruct { pub field: u32, } pub(crate) struct CrateStruct { pub(crate) field: u32, } // This struct is private because it is not marked as `pub`. struct PrivateStruct { field: u32, } mod inner { pub(super) struct SuperStruct { pub(super) field: u32, } } pub fn get_all_fields() -> u32 { let public_struct = PublicStruct { field: 1 }; let crate_struct = CrateStruct { field: 2 }; let private_struct = PrivateStruct { field: 3 }; let super_struct = inner::SuperStruct { field: 4 }; public_struct.field + crate_struct.field + private_struct.field + super_struct.field } } #[contract] pub struct NewContract; trait OuterTrait { fn get_all_fields() -> u32; fn get_public_fields() -> u32; } #[contractimpl] impl OuterTrait for NewContract { fn get_public_fields() -> u32 { let public_struct = outer::PublicStruct { field: 1 }; let crate_struct = outer::CrateStruct { field: 2 }; // private structs cannot be accessed from outside the crate public_struct.field + crate_struct.field } fn get_all_fields() -> u32 { outer::get_all_fields() } } ``` ### Time-Based Variables The Soroban Rust SDK and Solidity provide a number of time-based variables that can be used to access information about the current block(EVM) or ledger(Soroban). These variables include: #### Solidity - `block.timestamp`: The timestamp of the current block. - `block.number`: The number of the current block. Here is an example of a smart contract that illustrates how time-based variables are used in Solidity: ```solidity pragma solidity ^0.8.0; contract SimpleContract { function getTimestamp() public view returns (uint256) { return block.timestamp; } function getBlockNumber() public view returns (uint256) { return block.number; } } ``` #### Soroban - `env.ledger().timestamp()`: Returns a unix timestamp for when the most recent ledger was closed. - `env.ledger().sequence()`: Returns the sequence number of the most recently closed ledger. Here is an example of a smart contract that illustrates how time-based variables are used in Rust: ```rust #![no_std] use soroban_sdk::{contractimpl, Address, Env}; #[contract] pub struct SimpleContract; #[contractimpl] impl SimpleContract { pub fn get_timestamp(env: Env) { let timestamp = env.ledger().timestamp(); let sequence = env.ledger().sequence(); } } ``` ### Authorization Soroban differs from Solidity in its approach to authorization and modifiers. While Solidity has a built-in modifier system, Sorotban does not. Instead, Soroban leverages traits, their implementations, and core features to achieve similar functionality. #### Solidity In Solidity, the ERC20 token standard includes the `approve` function, which allows a token holder to authorize another address to spend a certain amount of tokens on their behalf. This function is commonly used in decentralized exchanges and other token transfer scenarios. Furthermore, we're ensuring that the spender is authorized to spend the amount of tokens requested by the token holder. Here's an example of how the approve function acts as an authorization mechanism in Solidity: ```solidity pragma solidity ^0.8.0; contract ERC20Token { constructor() { address owner = msg.sender; ownerAddress = owner; } mapping(address => uint256) public balances; mapping(address => mapping(address => uint256)) public allowances; mapping(address => bool) public isAuthorized; address public ownerAddress; function approve(address spender, uint256 amount) public returns (bool) { allowances[msg.sender][spender] = amount; return true; } function setAuthorization(address newAuth) public returns (bool) { require(msg.sender == ownerAddress); isAuthorized[newAuth] = true; return true; } function transfer(address to, uint256 amount) public returns (bool) { require(allowances[msg.sender][to] >= amount, "Not enough allowance"); require(isAuthorized[to] == true, "Not authorized"); balances[msg.sender] -= amount; balances[to] += amount; return true; } } ``` The approve function allows the token holder to authorize spender to spend amount tokens on their behalf. The transfer function then checks if the spender is authorized to spend the amount of tokens requested by the token holder. If so, the transfer is executed. These Solidity examples illustrate some common authorization patterns used in Ethereum smart contracts. Soroban provides alternative approaches to achieve similar functionality, leveraging core functionality derived right from the soroban SDK. #### Soroban Soroban's design principles prioritize flexibility, security, and testability, which have led to differences in how authorization is handled compared to Solidity. Soroban provides built-in functions such as `require_auth` and `require_auth_for_args` through the `Address` struct. These functions help enforce authorization rules within contracts. During on-chain execution, the Soroban host performs the necessary authentication, including verifying signatures and **ensuring replay prevention**. This alleviates the burden of authentication from the contracts themselves, promoting security and reducing potential vulnerabilities. Here is an example of a smart contract that illustrates how authorization is handled in Soroban: ```rust #![no_std] use soroban_sdk::{contractimpl, testutils::Address as _, Address, Symbol, Env, IntoVal}; #[contract] pub struct Contract; #[contractimpl] impl Contract { pub fn transfer(env: Env, address: Address, amount: i128) { address.require_auth(); } pub fn transfer2(env: Env, address: Address, amount: i128) { address.require_auth_for_args((amount / 2,).into_val(&env)); } } ``` In this example, we have a Soroban contract that includes two public functions: `transfer` and `transfer2`, both of which involve authorization checks. Inside the Transfer function, the `require_auth` method is invoked on the address object. This method ensures that the caller of the contract has the necessary authorization to execute the transfer. The `transfer2` function follows a similar pattern but uses the `require_auth_for_args` method instead. It takes the same parameters as transfer but provides a tuple (amount / 2,) as the argument to `require_auth_for_args`. This method verifies that the caller has authorized the contract invocation with the specific arguments. By utilizing these authorization methods provided by the `Address` object from the Soroban Rust SDK, the contract enforces that only authorized callers can perform the transfers. This approach enhances the security of the contract by ensuring that sensitive operations can only be executed by authorized parties. Soroban's approach to authorization in this example offers several advantages over Solidity's model of ERC20 by eliminating the need for separate approval management. Instead, authorization checks can be directly incorporated into any Soroban function. This simplifies the contract codebase and reduces the complexity associated with managing separate approval states. Soroban authorization provides Contract-level Authorization, Account Abstraction Functionality, and more advanced Authorization checks. To learn more about these advantages, visit the [Authorization section](../../fundamentals/contract-development/authorization.mdx) of the documentation. ## Summary Overall the Soroban equivalents of Solidity concepts are very similar. However, there are notable differences worth highlighting. Soroban uses `env` instead of `msg` to access information about the entire contract execution environment, including the state of the contract, addresses involved, and more. Authorization is also handled differently in Soroban, as it is built into the core functionality of the SDK and is more robust than relying on smart contract code alone. For more information on Solidity concepts and their Soroban equivalents, it is recommended that one refer to both the [Soroban Rust SDK documentation](https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Env.html) and the [Solidity documentation](https://docs.soliditylang.org/en/v0.8.19/cheatsheet.html). --- ## Solidity and Rust Syntax, Data Types, and Basic Constructs # Getting Started with Rust and Solidity In this tutorial, we'll explore Rust and Solidity, two powerful programming languages. Rust, a systems programming language, is renowned for its safety, concurrency, and performance features, which can be advantageous when building smart contracts. On the other hand, Solidity is a high-level language specifically designed for creating smart contracts on the Ethereum Virtual Machine. This section aims to provide a high-level overview of the similarities and differences between the two languages. ## Table of Contents 1. [Solidity Syntax, Data Types, and Basic Constructs](#solidity-syntax) 2. [Rust Syntax, Data Types, and Ownership Model](#rust-syntax) 3. [Writing and Interacting With Simple Smart Contracts](#writing-and-interacting-with-simple-smart-contracts) ### Solidity Syntax Solidity is a programming language designed specifically for creating smart contracts on the Ethereum Virtual Machine (EVM). It has a syntax similar to JavaScript and supports a variety of data types and constructs. ```solidity pragma solidity ^0.8.0; contract HelloWorld { function sayHello() public pure returns (string memory) { return "Hello, World!"; } } ``` ### Data Types Solidity supports various data types, such as: - Boolean: `bool` - Integer: `int` (signed) and `uint` (unsigned) - Address: `address` - String: `string` - Bytes: `bytes` (dynamic-size) and `bytes32` (fixed-size) - Arrays: dynamic-size or fixed-size and can be declared with various types. - Structs: `struct` - Enums: `enum` - Mapping: `mapping` Here are examples of implementations for each data type: ```solidity pragma solidity ^0.8.0; contract DataTypesExample { // Boolean bool public isCompleted = false; // Integer (signed and unsigned) int256 public signedInteger = -10; uint256 public unsignedInteger = 10; // Address address public userAddress = 0x742d35Cc6634C0532925a3b844Bc454e4438f44e; // String string public greeting = "Hello, World!"; // Bytes (dynamic-size and fixed-size) bytes public dynamicBytes = "hello, solidity"; bytes32 public fixedBytes = "hello, solidity"; // Arrays (dynamic-size and fixed-size) uint[] public dynamicArray = [1, 2, 3]; uint[5] public fixedArray = [1, 2, 3, 4, 5]; address[] public dynamicAddressArray = [0xd41d1744871f42Bb724D777A2d0Bf53FB43a0040, 0x1f514ae9834aEAF6c2c3eb6D20E27e865F419010]; address[3] public fixedAddressArray = [0xC90cd0D820D6dc447B3cD9545185B046873786A6, 0x401997E856CE51e0D4A8f26ce64952313BEA0E25, 0x221d3b9821f3Cc49B42E7dd487E2a6d1b3ed0E05]; bool[] public dynamicBoolArray = [true, false, true]; bool[2] public fixedBoolArray = [true, false]; // Struct struct Person { string name; uint age; } Person public person = Person("Alice", 30); // Enums enum Status { Open, Closed, Pending } Status public currentStatus = Status.Open; Status public nextStatus = Status.Closed; Status public previousStatus = Status.Pending; // Mapping mapping(address => uint) public balances; constructor() { balances[msg.sender] = 100; } } ``` ### Basic Constructs Some of the basic constructs in Solidity include: 1. `Variables`: Declared with a data type and an identifier. 2. `Functions`: Defined with the `function` keyword. 3. `Modifiers`: Used to modify functions' behavior. 4. `Events`: Used to log changes in the contract state. 5. `Inheritance`: Solidity supports single and multiple inheritance. We will explore some of these constructs in more detail in the next article, [Advanced Solidity Concepts](./solidity-and-rust-advanced-concepts.mdx#advanced-solidity-concepts). ### Rust Syntax Rust is a programming language that is well-suited for building smart contracts due to its emphasis on safety, concurrency, and performance. It enforces strict ownership and borrowing rules to prevent data races and other common bugs. ```rust fn main() { println!("Hello, world!"); } ``` ### Data Types The [Soroban Rust SDK](https://docs.rs/soroban-sdk/latest/soroban_sdk/index.html) supports a variety of [Built-In Types](../../fundamentals/contract-development/types/built-in-types.mdx) which consist of both Primitive and [Custom Types](../../fundamentals/contract-development/types/custom-types.mdx), such as: #### Primitive Data Types - 32-bit Integers: signed (`i32`) and unsigned (`u32`) - 64-bit Integers: signed (`i64`) and unsigned (`u64`) - 128-bit Integers: signed (`i128`) and unsigned (`u128`) - Bool (`bool`) - Bytes, Strings (`Bytes`, `BytesN`): byte arrays and strings that can be passed to contracts and stores - Vec (`Vec`): sequential and indexable growable collection type - Map (`Map`): ordered key-value dictionary - Address (`Address`): universal opaque identifier used in contracts - String (`String`): a contiguous growable array type containing u8s and requires an env to be passed in - Symbol: - (`Symbol::new`): small efficient strings up to 32 characters in length and requires an env to be passed in - (`symbol_short!`) small efficient strings up to 9 characters in length Both are limited to the characters `a-zA-Z0-9_` and are encoded into 64-bit integers. #### Custom Data Types - `Structs` (with Named Fields): A custom type consisting of named fields stored on the ledger as a `map` of key-value pairs. - `Structs` (with Unnamed Fields): A custom type consisting of unnamed fields stored on the ledger as a vector of values. - `Enum` (Unit and Tuple Variants): A custom type consisting of unit and tuple variants stored on the ledger as a two-element vector, with the first element being the name of the variant and the second being the value. - `Enum` (Integer Variants): A custom type consisting of integer variants stored on the ledger as the `u32` value. The following are examples of implementations for each data type: ```rust // Integer (signed and unsigned) let unsigned_32_bit: u32 = 42; let signed_32_bit: i32 = -42; let unsigned_64_bit: u64 = 42; let signed_64_bit: i64 = -42; let unsigned_128_bit: u128 = 42; let signed_128_bit: i128 = -42; // Boolean let boolean: bool = true; // String let msg: &str = "Hello"; String::from_slice(&env, msg) // Symbols (short and new) let symbol_short = symbol_short!("Sample"); // up to 9 chars // env is &Env let symbol_new = Symbol::new(env, "SampleSymbolExpression"); // Bytes (Bytes and BytesN) let bytes = Bytes::from_slice(&env, &[1; 32]); let bytes_n = BytesN::from_array(&env, &[0; 32]); // Vec let vec = vec![&env, 0, 1, 2, 3]; // Map let map = map![&env, (2, 20), (1, 10)]; // Address let address = Address::from_str(&env, "G..."); // Struct (named fields) pub struct State { pub count: u32, pub last_incr: u32, } struct Tuple(u32, String); // Struct (unnamed fields) pub struct State(pub u32, pub u32); // Enum (unit and tuple variants) pub enum Enum { A, B(u32), } // Enum (integer variants) pub enum Enum { A = 0, B = 1, } ``` ### A Brief Introduction to Modules, Macros, Structs, Traits, and Attribute Macros In this section, we will provide a concise introduction to some fundamental concepts in Rust: `Modules`, `Macros`, `Structs`, `Traits`, and `Attribute Macros`. These concepts are essential for understanding and writing efficient Rust code, and they will assist you on your journey as a smart contract developer. #### 1. Modules Modules in Rust are used to organize and separate code into different namespaces. They enable better code organization, reusability, and encapsulation. To define a module, use the `mod` keyword followed by a block containing the module's contents. ```rust mod my_module { pub fn my_function() { println!("Hello from my_module!"); } } ``` #### 2. Macros [Macros](https://doc.rust-lang.org/book/ch19-06-macros.html) in Rust are powerful tools that allow you to do metaprogramming, enabling you to build chunks of reusable code at compile time. There are two basic types: [declarative](https://doc.rust-lang.org/book/ch19-06-macros.html#declarative-macros-with-macro_rules-for-general-metaprogramming) and [procedural](https://doc.rust-lang.org/book/ch19-06-macros.html#procedural-macros-for-generating-code-from-attributes) macros. The most common is the declarative macro, or plain "macro", which is defined with `macro_rules!` ```rust macro_rules! my_macro { () => { println!("Hello from my_macro!"); }; } fn main() { my_macro!(); } ``` #### 3. Structs Structs are custom data types in Rust that enable you to bundle data together. They provide a way to define and create more complex data structures. ```rust struct MyStruct { field1: i32, field2: String, } fn main() { let my_instance = MyStruct { field1: 42, field2: String::from("Hello"), }; } ``` #### 4. Traits Traits in Rust define a shared set of behaviors that types can then either use as-is (default implementations) or implement themselves. They can be thought of as interfaces in other languages. Traits are defined with the `trait` keyword, and their methods can be implemented for different types using the `impl` keyword. ```rust trait MyTrait { fn my_method(&self); } struct MyStruct; impl MyTrait for MyStruct { fn my_method(&self) { println!("Hello from MyTrait's my_method!"); } } ``` #### 5. Attribute Macros Attribute macros in Rust are a form of procedural macros that enable you to define custom attributes for various language elements such as functions, structs, and enums. They can modify or generate code based on the annotated items. ```rust // To use an attribute macro, first import it with `use` use my_attribute_macro::my_attribute; // Then apply the attribute to an element in your code #[my_attribute] fn my_function() { println!("Hello from my_function!"); } ``` During your smart contract developer journey, you will frequently encounter the attribute macro [`#[contractimpl]`](https://docs.rs/soroban-sdk/latest/soroban_sdk/index.html) which exports publicly accessible functions to the Soroban environment. Functions that are publicly accessible in the implementation are invocable by other contracts, or directly by transactions, when deployed. ```rust #[contractimpl] impl HelloContract { pub fn hello(env: Env, to: Symbol) -> Vec { vec![&env, symbol_short!("Hello"), to] } } ``` ### Ownership Model Rust enforces [**strict ownership rules**](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html) to manage memory and resources: - Each value has a single owner. - When the owner goes out of scope, the value is automatically deallocated. - [`Borrowing`](https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html): Values can be borrowed as immutable or mutable references. - [`Lifetimes`](https://doc.rust-lang.org/rust-by-example/scope/lifetime.html): Used to ensure that references remain valid. ### Smart Contract Dialect Contract development in Rust involves certain restrictions due to either unavailable features in the deployment environment or high runtime costs. Thus, the code written for contracts can be seen as a distinct _dialect_ of Rust, focusing on deterministic behavior and minimized code size. To learn more about Rust's Contract Dialect, check out the [Contract Rust Dialect Page](../../fundamentals/contract-development/rust-dialect.mdx). ## Writing and Interacting with Simple Smart Contracts In this section, we'll learn how to write and interact with simple smart contracts in Solidity and Rust. ### Writing a Smart Counter in Solidity Here's an example of a simple Solidity smart contract for a counter: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; contract Counter { uint256 private _count; function getCount() public view returns (uint256) { return _count; } function increment() public { _count += 1; } } ``` Let's break down the layout of the code line by line: ```solidity // SPDX-License-Identifier: UNLICENSED ``` This is a comment that identifies the [license](https://docs.soliditylang.org/en/v0.8.19/layout-of-source-files.html#spdx-license-identifier) for the code. It's not required for the code to run, but it's good practice to include licensing information. ```solidity pragma solidity ^0.8.0; ``` This specifies the version of Solidity that this code was written for. In this case, it's version `0.8.0` or higher. ```solidity contract Counter {} ``` This defines a new Solidity contract called `Counter`. ```solidity uint256 private _count; ``` This is a [`private variable`](https://docs.soliditylang.org/en/v0.8.19/cheatsheet.html#function-visibility-specifiers) called `_count` of type `uint256` (unsigned integer). This variable will be used to store the current value of the counter. It is marked as `private`, which means it can only be accessed from within the contract. ```solidity function getCount() public view returns (uint256) { return _count; } ``` This is a function called `getCount()` that returns the current value of the counter. The function is marked as `public`, which means it can be called from outside the contract. The `view` keyword indicates that this function doesn't modify the state of the contract. The `returns` keyword specifies the return type of the function. ```solidity function increment() public { _count += 1; } ``` This is a function called `increment()` that increments the counter by 1. It doesn't return anything, but it modifies the state of the contract. Like `getCount()`, it's marked as `public`, which means it can be called from both inside and outside the contract. ### Interacting with the Solidity Smart Counter We can interact with the smart contract using the `Remix IDE`. To do so, follow these steps: 1. Click the following link to [open the Gist in Remix](https://remix.ethereum.org/#version=soljson-v0.8.18+commit.87f61d96.js&optimize=false&runs=200&gist=416ab15a6beed9d91cf2f615625ffe48&lang=en&evmVersion=null). 2. Navigate to the `Counter.sol` file in the file explorer. ![Counter](/img/migrating/counter.png) 3. Press `Ctrl/Cmd + s` to compile the contract. 4. Navigate to the `Deploy & Run Transactions` tab and click the `Deploy` button. ![Deploy](/img/migrating/deploy-counter.png) The contract should appear under the `Deployed Contracts` tab: ![Deployed](/img/migrating/deployed-contracts.png) 5. Click the `increment` button to increment the counter. 6. Click the `getCount` button to get the current count. ![Increment](/img/migrating/increment.png) Up to this point, we've covered the basics of writing, deploying to a sandbox EVM, and interacting with a simple smart contract using Solidity. In the following section, we will extend our knowledge by learning how to achieve the same outcomes using Rust. ### Writing a Smart Counter in Rust In this section, we'll create a Rust program that simulates the functionality of the Counter smart contract. Here's an example of a simple counter in Rust: ```rust #![no_std] use soroban_sdk::{contractimpl, log, Env, Symbol}; const COUNTER: Symbol = symbol_short!("COUNTER"); #[contract] pub struct IncrementContract; #[contractimpl] impl IncrementContract { /// Increment increments an internal counter, and returns the value. pub fn increment(env: Env) -> u32 { // Get the current count. let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0. log!(&env, "count: {}", count); // Increment the count. count += 1; // Save the count. env.storage().instance().set(&COUNTER, &count); // Return the count to the caller. count } /// get_count returns the current value of the counter. pub fn get_count(env: Env) -> u32 { env.storage().instance().get(&COUNTER).unwrap_or(0) } } ``` This code is an implementation of a smart contract written in Rust using the [`Soroban Rust SDK`](../../../tools/sdks/contract-sdks.mdx#soroban-rust-sdk), a Rust-based smart contract development toolkit developed by the [Stellar Development Foundation (SDF)](https://stellar.org/foundation). The Soroban Rust SDK provides a powerful set of tools for writing smart contracts that run on the Soroban Virtual Machine. Here's a line-by-line explanation of what the code is doing: ```rust #![no_std] ``` This is a Rust attribute that tells the Rust compiler not to link the [Rust standard library](https://doc.rust-lang.org/std). The standard library is extensive, and when deploying Soroban applications, we want to streamline the process as much as possible. By using `no_std`, we establish a leaner, "barebones" starting point for projects, encompassing only the Rust core and a few other essential components, rather than the full breadth of the standard library. ```rust use soroban_sdk::{contractimpl, log, Env, Symbol}; ``` This code imports necessary items from the Soroban Rust SDK for writing a smart contract. The `contractimpl` [macro](https://doc.rust-lang.org/book/ch19-06-macros.html) is used to implement the smart contract, while the `log` macro is used for logging messages. The `Env` struct represents the environment the contract is executing in, and the `Symbol` type is a small, efficient string type. ```rust const COUNTER: Symbol = symbol_short!("COUNTER"); ``` This creates a new `Symbol` value with the string "COUNTER". The constant `COUNTER` is then used as a key to identify the count value stored in the contract `storage`. ```rust #[contract] pub struct IncrementContract; ``` This defines a public struct, `IncrementContract`, which will contain the implementation of the smart contract. ```rust #[contractimpl] impl IncrementContract {} ``` This is a macro that implements the `IncrementContract` struct as a smart contract. As previously noted, the `#[contractimpl]` attribute exports public functions to the Soroban environment. Meaning, these functions become accessible within the implementation and can be invoked by other contracts or directly by transactions upon deployment. ```rust pub fn increment(env: Env) -> u32 {} ``` This is a public function called `increment` that takes an `Env` struct as an argument and returns a `u32`. `Env` is the environment the contract is executing in, and `u32` is the type of value returned by the function. ```rust let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0)); // If no value set, assume 0. ``` In this line of code, a mutable variable named `count` of type unsigned 32-bit integer (`u32`) is being created. The storage environment is accessed using `env.storage()`, and the value associated with the key `COUNTER` is retrieved using the `get` method. If there is no value set for the key `COUNTER`, a default value of 0 is used. ```rust log!(&env, "count: {}", count); ``` This logs the current count using the `log` macro provided by the Soroban Rust SDK. ```rust count += 1; ``` This increments the count by 1. ```rust env.storage().instance().set(&COUNTER, &count); ``` This saves the updated count back to the contract storage using the `set` method on the storage object. ```rust count ``` This returns the updated count to the caller of the function. ```rust pub fn get_count(env: Env) -> u32 {} ``` This is a public function called `get_count` that takes an `Env` struct as an argument and returns a `u32`. Once more we see the `Env` which is the environment the contract is executing in, and `u32` as the type of the value returned by the function. ```rust env.storage().instance().get(&COUNTER).unwrap_or(0) ``` This is a repeat of the code we saw earlier, which retrieves the value associated with the key `COUNTER` from the contract storage. If there is no value set for the key `COUNTER`, a default value of 0 is used. Finally, the `unwrap()` method is called to extract the actual value from the `Ok` wrapper, which is then returned to the caller of the function. Now that we have written our smart contract, it's time to explore how we can interact with it using the [Stellar CLI](../../../tools/cli/stellar-cli.mdx), one of many robust [Developer Tools](../../../tools/developer-tools/README.mdx) available. This powerful command-line tool allows us to interact with the Soroban Virtual Machine from a local machine, providing us with an efficient and flexible way to manage our smart contract. ### Interacting with the Rust Smart Counter To interact with the Rust counter, create a new Rust library using the cargo new command. ```bash cargo new --lib increment ``` Once the project is created, replace the `src/lib.rs` file with the [code example above](#writing-a-smart-counter-in-rust). ```rust // Remember to replace your lib.rs file with the code example above. // This is just a reference to point you in the right direction. #[contractimpl] impl IncrementContract {...} ``` Then, add the following dependencies to the `Cargo.toml` file: ```toml [package] name = "increment" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [features] testutils = ["soroban-sdk/testutils"] [dependencies] soroban-sdk = "27" [dev-dependencies] soroban-sdk = { version = "27", features = ["testutils"] } [profile.release] opt-level = "z" overflow-checks = true debug = 0 strip = "symbols" debug-assertions = false panic = "abort" codegen-units = 1 lto = true [profile.release-with-logs] inherits = "release" debug-assertions = true ``` > _Note_: For a detailed explanation of the `Cargo.toml` configuration used in this tutorial, check out the [Hello World Example](../../../build/smart-contracts/getting-started/hello-world.mdx). Next, build the project using the `stellar contract build` command. ```bash cd increment stellar contract build ``` The compiled contract will be located in the `target/wasm32v1-none/release` directory. To interact with the contract, we can use the `stellar contract invoke` command from the `stellar-cli` tool. Here's an example of invoking the `increment` function on a contract with ID 1: ```bash stellar contract invoke \ --wasm target/wasm32v1-none/release/increment.wasm \ --id 1 \ -- \ increment ``` The output should be the current value of the counter, which in this case is: ```bash 1 ``` You can use the same `stellar contract invoke` command to increment the counter multiple times. To get the current value of the counter, you can use the following command: ```bash stellar contract invoke \ --wasm target/wasm32v1-none/release/increment.wasm \ --id 1 \ -- \ get_count ``` The output should be the current value of the counter, assuming the counter has been incremented 3 times, the output will be: ```bash 3 ``` And that's it! You've learned how to write and interact with simple smart contracts in Solidity and Rust. In the upcoming sections, we'll learn about advanced smart contract concepts, the similarities and differences between Solidity and Rust, and how to develop and deploy smart contracts with Soroban. --- ## Solidity support via Solang Leverage the Hyperledger Solang compiler to compile Solidity contracts to Soroban. - Learn about the Solidity SDK and links to Solang in Tools: [Contract SDKs](../../../tools/sdks/contract-sdks.mdx) - Solang docs (Soroban target): https://solang.readthedocs.io/en/latest/targets/soroban.html - Solang repository and examples: https://github.com/hyperledger/solang/tree/main/examples/soroban - Solang Web IDE: https://solang.io/ :::caution Solidity support via Solang for Stellar is experimental and evolving. Not all Solidity features are supported yet, and breaking changes may occur. We Don't recommend using Solang for production contracts at this time. ::: - The easiest way to use Solang is via the Web IDE, but you can also build Solang from source or use prebuilt binaries. - For deployment and interaction, use the Stellar CLI and client SDKs after compiling your Wasm artifact. --- ## Explore Mainnet, Testnet & Futurenet: Roles, Use Cases & Connectivity # Networks Stellar has three networks: the public network (Mainnet, also called Pubnet or the Public Network), the test network (Testnet), and a dev network (Futurenet). - **Mainnet** is the main network used by applications in production. It connects to real financial rails and requires XLM to cover minimum balances, transaction fees, and rent. - **Testnet** is a smaller, free-to-use network maintained by SDF that functions like Mainnet but doesn't connect to real money. It resets on a regular cadence, making it the best place for developers to test applications in a stable environment that mirrors Mainnet functionality. - **Futurenet** is a dev network for testing bleeding-edge features. It resets whenever necessary, so it's less predictable than Testnet, but it's where new features are introduced before stable releases. ## Network Comparison | Feature | Mainnet | Testnet | Futurenet | | --- | --- | --- | --- | | **Purpose** | Production network | Stable testing environment | Bleeding-edge feature testing | | **Network Passphrase** | `Public Global Stellar Network ; September 2015` | `Test SDF Network ; September 2015` | `Test SDF Future Network ; October 2022` | | **Network ID** | 7ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a979 | cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472 | a3a1c6a78286713e29be0e9785670fa838d13917cd8eaeb4a3579ff1debc7fd5 | | **Validator Nodes** | Run by the public | SDF runs three core validator nodes | SDF runs core validator nodes | | **Validator** | `core-live-a.stellar.org` `core-live-b.stellar.org` `core-live-c.stellar.org` | `core-live-testnet.stellar.org` | `core-live-futurenet.stellar.org` | | **Funding** | Real XLM required from another account | Free via Friendbot | Free via Friendbot | | **Horizon API** | [Multiple providers available](../data/apis/horizon/providers.mdx) | https://horizon-testnet.stellar.org | https://horizon-futurenet.stellar.org | | **Stellar RPC** | [Third-party providers only](../data/apis/rpc/providers.mdx) | `https://soroban-testnet.stellar.org` | `https://rpc-futurenet.stellar.org` | | **Operations per Ledger** | 1,000\* | 200\* | 1,000\* | | **Smart Contract Transactions per Ledger** | Max 2,000\* | Max 2,000\* | Max 2,000\* | | **Network Resets** | Never | Regular cadence | As needed (unpredictable) | | **Friendbot Available** | No | Yes (10,000 XLM) | Yes (10,000 XLM) | | **Friendbot API** | N/A | `https://friendbot.stellar.org` | `https://friendbot-futurenet.stellar.org` | | **History Archive** | `http://history.stellar.org/prd/core-live/core_live_001/`, `http://history.stellar.org/prd/core-live/core_live_002/`, `http://history.stellar.org/prd/core-live/core_live_003/` | `http://history.stellar.org/prd/core-testnet/core_testnet_001`, `http://history.stellar.org/prd/core-testnet/core_testnet_002`, `http://history.stellar.org/prd/core-testnet/core_testnet_003` ([more info](https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_testnet.cfg)) | `http://history.stellar.org/dev/core-futurenet/core_futurenet_001/`, `http://history.stellar.org/dev/core-futurenet/core_futurenet_002/`, `http://history.stellar.org/dev/core-futurenet/core_futurenet_003/` | \*Values as of July 2026. These limits are set by validator vote and can change over time — check the current values on Stellar Lab's [Network Limits](https://lab.stellar.org/network-limits) page, or see [Resource Limits & Fees](./resource-limits-fees.mdx) for other ways to query them live. The precise smart contract capacity per ledger can also vary based on [resource limits](../learn/fundamentals/fees-resource-limits-metering.mdx#resource-limitations). See [Fees and Metering](../learn/fundamentals/fees-resource-limits-metering.mdx) for detailed smart contract network settings. ## Friendbot Friendbot is a bot that funds accounts and contracts with fake XLM on Testnet or Futurenet. You can request XLM from Friendbot using the [Stellar Lab](../tools/lab/account.mdx#fund-account) or with various SDKs. **Key details:** - Requests are rate limited, so use wisely - Provides 10,000 fake XLM when funding a new account - Can fund both account addresses (G...) and contract addresses (C...) - For multiple accounts: fund your first account with Friendbot, then use that account to fund subsequent accounts using the Create Account operation ## Getting Started **For Production:** Use Mainnet with real XLM and production-ready infrastructure. **For Testing:** Use Testnet for stable development and testing that mirrors production behavior. **For Experimental Features:** Use Futurenet to test the latest features before they're released. ## Testnet and Futurenet data reset Testnet and Futurenet are reset periodically to the genesis ledger to declutter the network, remove spam, reduce the time needed to catch up on the latest ledger, and help maintain the system. Resets clear all ledger entries (accounts, trustlines, offers, smart contract data, etc.), transactions, and historical data from Stellar Core, Horizon, and the Stellar RPC- which is why developers should not rely on the persistence of accounts or the state of any balances when using Testnet or Futurenet. Futurenet resets are on a less regular cadence than Testnet resets and don't have a set schedule. Testnet resets typically happen 2-4 times per year at 17:00 UTC and are announced at least two weeks in advance on the [Stellar Dashboard](https://dashboard.stellar.org) and through several developer community channels. Here are the scheduled 2026 dates: - December 16, 2026 If you run a Testnet or Futurenet Horizon instance, you need to re-join and re-sync to the network after a reset. Check out how to do that here: [Testnet Reset](https://github.com/stellar/packages/blob/master/docs/testnet-reset.md). Check out [this How-To Guide](../build/guides/basics/automate-reset-data.mdx) on automating Testnet and Futurenet reset data. ## Test data automation It is recommended that you have testing infrastructure that can repopulate the Testnet and Futurenet with useful data after a reset. This will make testing more reliable and will help you scale your testing infrastructure to a private network if you choose to do so. For example, you may want to: - Generate issuers of assets for testing the development of a wallet; - Generate orders on the order book (both current and historical) for testing the development of a trading client; - Recreate liquidity pools; - Redeploy smart contracts. If you maintain an application, you should think about creating a data set that is representative enough to test your primary use cases, and allow for robust testing even when Testnet or Futurenet are not available. A script can automate this entire process by creating an account with Friendbot and submitting a set of transactions that are predefined as a part of your testing infrastructure. ## Network passphrases Stellar’s Mainnet, Testnet, and Futurenet each have their own unique passphrase. These are used when validating signatures on a given transaction. If you sign a transaction for one network but submit it to another, it won’t be considered valid. By convention, the format of a passphrase is ‘`[Network Name] ; [Month of Creation] [Year of Creation]`’. The passphrases for the Stellar Mainnet, Testnet, and Futurenet are: - Mainnet: '`Public Global Stellar Network ; September 2015`' - Testnet: '`Test SDF Network ; September 2015`' - Futurenet: '`Test SDF Future Network ; October 2022`' Passphrases serve two main purposes: (1) used as the seed for the root account (master network key) at genesis and (2) used to build hashes of transactions, which are ultimately what is signed by each signer’s secret key in a transaction envelope; this allows you to verify that a transaction was intended for a specific network by its signers. Many SDKs have the passphrases hardcoded for Stellar's networks. If you’re running a private network, you’ll have to manually pass in a passphrase to be used whenever transaction hashes are generated. All of Stellar’s official SDKs allow you to use a network with a custom passphrase. ## Network IDs Each Stellar network also has a network ID, which is the SHA-256 hash of the network passphrase. The network ID is used in transaction signing and contract address generation and ensures that hashes and IDs generated are different on each network. The IDs for the Stellar Mainnet, Testnet, and Futurenet are: | Network | Network ID | | --- | --- | | Mainnet | `7ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a979` | | Testnet | `cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472` | | Futurenet | `a3a1c6a78286713e29be0e9785670fa838d13917cd8eaeb4a3579ff1debc7fd5` | ## What Testnet and Futurenet should and should not be used for ### Testnet and Futurenet are good for - Creating test accounts (with funding from Friendbot); - Developing applications and contracts and exploring tutorials on Stellar without the potential to lose any assets; - Testing existing applications against new releases or release candidates of Stellar Core, Horizon, and the Stellar RPC; - Performing data analysis on a smaller, non-trivial data set compared to the Mainnet. ### Testnet and Futurenet are bad for - Load and stress testing; - High availability test infrastructure- SDF does not guarantee Testnet availability; - Long-term storage of data on the network since the network resets periodically; - A testing infrastructure that requires more control over the test environment, such as: - The ability to control the data reset frequency; - The need to secure private or sensitive data (before launching on the Mainnet). You can always run your own test network for use cases that don’t work well with SDF’s Testnet. ## Moving your project from Testnet or Futurenet to production Mainnet, Testnet, and Futurenet each have their own unique passphrase, which is used to validate signatures on a given transaction. See above sections for the network passphrase for each network. For applications that don’t rely on the state of the network (such as specific accounts needing to exist), you move to production by changing the network passphrase and ensuring your Horizon instance is connected to Mainnet. If you’ve been running a Stellar Core or Horizon instance against the Testnet and want to switch to production, changing the passphrase will require both respective databases to be completely reinitialized. If you run your own RPC on Testnet or Futurenet, you may want to use an RPC service when you move to Mainnet. Check out the RPC service providers [here](../data/apis/rpc/providers.mdx). --- ## Resource Limits & Fees ## Where to find the current resource limits and fees ### [Stellar Lab](https://lab.stellar.org) The Stellar Lab's [Network Limits](https://lab.stellar.org/network-limits) page displays real-time network settings for [Testnet](https://lab.stellar.org/network-limits?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;), [Mainnet](https://lab.stellar.org/network-limits?$=network$id=mainnet&label=Mainnet&horizonUrl=https:////horizon.stellar.org&rpcUrl=https:////mainnet.sorobanrpc.com&passphrase=Public%20Global%20Stellar%20Network%20/;%20September%202015;;), and [Futurenet](https://lab.stellar.org/network-limits?$=network$id=futurenet&label=Futurenet&horizonUrl=https:////horizon-futurenet.stellar.org&rpcUrl=https:////rpc-futurenet.stellar.org&passphrase=Test%20SDF%20Future%20Network%20/;%20October%202022;;). The page offers both `table` and `JSON` display options for easy viewing. ### [Stellar CLI](https://github.com/stellar/stellar-cli) You can also query current network settings directly from the command line using: ```bash stellar network settings ``` For more details on using this command, see the [Stellar CLI documentation](https://developers.stellar.org/docs/tools/cli/stellar-cli#stellar-network-settings). --- ## Software Versions :::caution Release candidates are software releases that are also released to the [Testnet] test network. Software releases may occur between Testnet releases. If you're interacting with Testnet, the recommended software versions to use in development are provided below. Releases to Testnet may include network resets and network passphrase changes. ::: [testnet]: ./README.mdx ## Protocol 28 (Testnet, TBD) ### Software | Software | Version | | --- | --- | | XDR | `TBD` | | Rust XDR | `28.0.0` | | Smart Contract Host Environment | `28.0.1` | | Stellar Core | `TBD` | | Smart Contract Rust SDK | `TBD` | | Poseidon Rust SDK | `TBD` | | Stellar CLI | `TBD` | | Stellar RPC | `TBD` | | Stellar Horizon | `TBD` | | Stellar Galexie | `TBD` | | Stellar Quickstart | `TBD` | | Stellar JS Stellar Base | `N/A` (merged into the JS Stellar SDK) | | Stellar JS Stellar SDK | `v17.0.0-rc.1` | | Stellar Horizon Client & TxnBuild | `TBD` | | Stellar RPC Client | `TBD` | | Freighter | | | Laboratory | `N/A` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes New features in Adapter, Protocol 28: - Allow Validators to Vote to Drop the Transaction Set from the Current Ledger: [CAP-83](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0083.md) - Externally Managed Contract Executables: [CAP-85](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0085.md) - Host Functions for Sparse Symbol-Keyed Map Creation and Unpacking: [CAP-86](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0086.md) ## Protocol 27 (Mainnet, July 8, 2026) ### Software | Software | Version | | --- | --- | | XDR | `v27.0` | | Rust XDR | `27.0.0` | | Smart Contract Host Environment | `27.0.1` | | Stellar Core | `27.1.0` | | Smart Contract Rust SDK | `27.0.6` | | Poseidon Rust SDK | `27.0.0` | | Stellar CLI | `27.1.0` | | Stellar RPC | `v27.1.1` | | Stellar Horizon | `v27.0.1` | | Stellar Galexie | `v27.0.0` | | Stellar Quickstart | `docker pull stellar/quickstart` | | Stellar JS Stellar Base | `N/A` (merged into the JS Stellar SDK) | | Stellar JS Stellar SDK | `v16.2.0` | | Stellar Horizon Client & TxnBuild | `v0.6.1` | | Stellar RPC Client | `v27.0.0` | | Freighter | | | Laboratory | `N/A` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes New features in Zipper, Protocol 27: - Release Notes: [v27.1.0](https://github.com/stellar/stellar-core/releases/tag/v27.1.0) (recommended) / [v27.0.0](https://github.com/stellar/stellar-core/releases/tag/v27.0.0) (protocol activation) - [Protocol 27 Upgrade Guide](https://stellar.org/blog/foundation-news/stellar-zipper-protocol-27-upgrade-guide) - Authentication Delegation and Address-Bound Soroban Credentials: [CAP-71](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0071.md) ## Protocol 27 (Testnet, June 18, 2026) ### Software | Software | Version | | --- | --- | | XDR | `v27.0` | | Rust XDR | `27.0.0` | | Smart Contract Host Environment | `27.0.0` | | Stellar Core | `27.0.0` | | Smart Contract Rust SDK | `27.0.2` | | Poseidon Rust SDK | `27.0.0` | | Stellar CLI | `27.0.0` | | Stellar RPC | `v27.0.0` | | Stellar Horizon | `v27.0.0` | | Stellar Galexie | `v27.0.0` | | Stellar Quickstart | `docker pull stellar/quickstart` | | Stellar JS Stellar Base | `N/A` (merged into the JS Stellar SDK) | | Stellar JS Stellar SDK | `v16.0.0-rc.2` | | Stellar Horizon Client & TxnBuild | `v0.6.0` | | Stellar RPC Client | `v27.0.0` | | Freighter | | | Laboratory | `N/A` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes New features in Zipper, Protocol 27: - [Release Notes](https://github.com/stellar/stellar-core/releases/tag/v27.0.0) - [Protocol 27 Upgrade Guide](https://stellar.org/blog/foundation-news/stellar-zipper-protocol-27-upgrade-guide) - Authentication Delegation and Address-Bound Soroban Credentials: [CAP-71](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0071.md) ## Protocol 26 (Mainnet, May 6, 2026) ### Software | Software | Version | | --- | --- | | XDR | `v26.0` | | Rust XDR | `26.0.1` | | Smart Contract Host Environment | `26.0.0` | | Stellar Core | `26.1.0` | | Smart Contract Rust SDK | `26.0.1` | | Poseidon Rust SDK | `26.0.0` | | Stellar CLI | `26.1.0` | | Stellar RPC | `v26.0.0` | | Stellar Horizon | `v26.0.0` | | Stellar Galexie | `v26.1.0` | | Stellar Quickstart | `docker pull stellar/quickstart` | | Stellar JS Stellar Base | `v15.0.0` | | Stellar JS Stellar SDK | `v15.1.0` | | Stellar Horizon Client & TxnBuild | `v0.5.0` | | Stellar RPC Client | `v26.0.0` | | Freighter | | | Laboratory | `N/A` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes New features in Yardstick, Protocol 26: - Release Notes: [v26.1.0](https://github.com/stellar/stellar-core/releases/tag/v26.1.0) (recommended) / [v26.0.0](https://github.com/stellar/stellar-core/releases/tag/v26.0.0) (protocol activation) - [Protocol 26 Upgrade Guide](https://stellar.org/blog/foundation-news/stellar-yardstick-protocol-26-upgrade-guide) - Freeze Ledger Entries via Network Configuration: [CAP-77](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0077.md) - Allow SAC to Create G-Account Balances: [CAP-73](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0073.md) - Host Functions for Performing Limited TTL Extensions: [CAP-78](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0078.md) - Host Functions for Muxed Address Strkey Conversions: [CAP-79](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0079.md) - Host Functions for Efficient ZK BN254 Use Cases: [CAP-80](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0080.md) - Checked 256-bit Integer Arithmetic Host Functions: [CAP-82](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0082.md) ## Protocol 26 (Testnet, April 16, 2026) ### Software | Software | Version | | --- | --- | | XDR | `v26.0` | | Rust XDR | `26.0.1` | | Smart Contract Host Environment | `26.1.3` | | Stellar Core | `26.1.0` | | Smart Contract Rust SDK | `26.0.1` | | Poseidon Rust SDK | `26.0.0` | | Stellar CLI | `26.1.0` | | Stellar RPC | `v26.0.0` | | Stellar Horizon | `v26.0.0` | | Stellar Galexie | `v26.1.0` | | Stellar Quickstart | `docker pull stellar/quickstart` | | Stellar JS Stellar Base | `v15.0.0` | | Stellar JS Stellar SDK | `v15.1.0` | | Stellar Horizon Client & TxnBuild | `v0.5.0` | | Stellar RPC Client | `v26.0.0` | | Freighter | | | Laboratory | `N/A` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes New features in Yardstick, Protocol 26: - Release Notes: [v26.1.0](https://github.com/stellar/stellar-core/releases/tag/v26.1.0) (recommended) / [v26.0.0](https://github.com/stellar/stellar-core/releases/tag/v26.0.0) (protocol activation) - [Protocol 26 Upgrade Guide](https://stellar.org/blog/foundation-news/stellar-yardstick-protocol-26-upgrade-guide) - Freeze Ledger Entries via Network Configuration: [CAP-77](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0077.md) - Allow SAC to Create G-Account Balances: [CAP-73](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0073.md) - Host Functions for Performing Limited TTL Extensions: [CAP-78](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0078.md) - Host Functions for Muxed Address Strkey Conversions: [CAP-79](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0079.md) - Host Functions for Efficient ZK BN254 Use Cases: [CAP-80](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0080.md) - Checked 256-bit Integer Arithmetic Host Functions: [CAP-82](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0082.md) ## Protocol 25 (Mainnet, January 22, 2026) ### Software | Software | Version | | --- | --- | | XDR | `v25.0` | | Rust XDR | `25.0.0` | | Smart Contract Host Environment | `25.0.0` | | Stellar Core | `25.0.0` | | Smart Contract Rust SDK | `25.0.0` | | Poseidon Rust SDK | `25.0.0-rc.1` | | Stellar CLI | `25.0.0` | | Stellar RPC | `v25.0.0` | | Stellar Horizon | `v25.0.0` | | Stellar Galexie | `v25.1.0` | | Stellar Quickstart | `docker pull stellar/quickstart` | | Stellar JS Stellar Base | `v14.0.4` | | Stellar JS Stellar SDK | `v14.4.3` | | Stellar Horizon Client & TxnBuild | `v24.0.0` | | Stellar RPC Client | `v25.0.0` | | Freighter | | | Laboratory | `N/A` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes New features in X-Ray, Protocol 25: - [Release Notes](https://github.com/stellar/stellar-core/releases/tag/v25.0.0) - BN254 Elliptic Curve Operations: [CAP-74](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0074.md) - Poseidon/Poseidon2 Permutation Primitives: [CAP-75](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0075.md) ## Protocol 25 (Testnet, January 7, 2026) ### Software | Software | Version | | --- | --- | | XDR | `v25.0` | | Rust XDR | `25.0.0` | | Smart Contract Host Environment | `25.0.0` | | Stellar Core | `25.0.0` | | Smart Contract Rust SDK | `25.0.0` | | Poseidon Rust SDK | `25.0.0-rc.1` | | Stellar CLI | `25.0.0` | | Stellar RPC | `v25.0.0` | | Stellar Horizon | `v25.0.0` | | Stellar Galexie | `v25.1.0` | | Stellar Quickstart | `docker pull stellar/quickstart` | | Stellar JS Stellar Base | `v14.0.4` | | Stellar JS Stellar SDK | `v14.4.3` | | Stellar Horizon Client & TxnBuild | `v24.0.0` | | Stellar RPC Client | `v25.0.0` | | Freighter | | | Laboratory | `N/A` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes New features in X-Ray, Protocol 25: - [Release Notes](https://github.com/stellar/stellar-core/releases/tag/v25.0.0) - BN254 Elliptic Curve Operations: [CAP-74](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0074.md) - Poseidon/Poseidon2 Permutation Primitives: [CAP-75](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0075.md) ## Protocol 24 (Mainnet, October 22, 2025) ### Software | Software | Version | | --- | --- | | XDR | `v24.0` | | Rust XDR | `24.0.1` | | Smart Contract Host Environment | `24.0.0` | | Stellar Core | `24.0.0` | | Smart Contract Rust SDK | `23.0.3` | | Stellar CLI | `23.1.4` | | Stellar RPC | `v24.0.0` | | Stellar Horizon | `v24.0.0` | | Stellar Galexie | `v24.0.0` | | Stellar Quickstart | `docker pull stellar/quickstart` | | Stellar JS Stellar Base | `v14.0.0` | | Stellar JS Stellar SDK | `v14.3.0` | | Stellar Horizon Client & TxnBuild | `v24.0.0` | | Stellar RPC Client | `v24.0.0` | | Freighter | | | Laboratory | `N/A` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes Protocol 24 is a stability upgrade following Whisk (Protocol 23). Read more about the upgrade here: - [Release Notes](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0076.md) - [Stability Upgrade After Whisk](https://stellar.org/blog/developers/addressing-state-archival-inconsistencies-protocol-upgrade-vote-next-week) ## Protocol 24 (Testnet, October 21, 2025) ### Software | Software | Version | | --- | --- | | XDR | `v24.0` | | Rust XDR | `24.0.1` | | Smart Contract Host Environment | `23.0.0` | | Stellar Core | `24.0.0` | | Smart Contract Rust SDK | `23.0.3` | | Stellar CLI | `23.1.4` | | Stellar RPC | `v24.0.0` | | Stellar Horizon | `v24.0.0` | | Stellar Galexie | `v24.0.0` | | Stellar Quickstart | `docker pull stellar/quickstart` | | Stellar JS Stellar Base | `v14.0.0` | | Stellar JS Stellar SDK | `v14.3.0` | | Stellar Horizon Client & TxnBuild | `v24.0.0` | | Stellar RPC Client | `v24.0.0` | | Freighter | | | Laboratory | `N/A` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes Protocol 24 is a stability upgrade following Whisk (Protocol 23). Read more about the upgrade here: - [Stability Upgrade After Whisk](https://stellar.org/blog/developers/addressing-state-archival-inconsistencies-protocol-upgrade-vote-next-week) ## Whisk, Protocol 23 (Mainnet, September 3, 2025) ### Software | Software | Version | | --- | --- | | XDR | `v23.0` | | Rust XDR | `v23.0.0` | | Smart Contract Host Environment | `v23.0.2` | | Stellar Core | `v23.0.1` | | Smart Contract Rust SDK | `v23.0.2` | | Stellar CLI | `v23.1.1` | | Stellar RPC | `v23.0.2` | | Stellar Horizon | `v23.0.0` | | Stellar Galexie | `v23.0.0` | | Stellar Quickstart | `docker pull stellar/quickstart` | | Stellar JS Stellar Base | `v14.0.0` | | Stellar JS Stellar SDK | `v14.1.1` | | Stellar Horizon Client & TxnBuild | `v23.0.0` | | Rust Stellar RPC Client | `v23.0.0` | | Freighter | `v5.34.1` | | Laboratory | `N/A` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes New features in Whisk, Protocol 23: - [Release Notes](https://github.com/stellar/stellar-core/releases/tag/v23.0.1) - Unified Events: [CAP-67](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md) - State Archival: [CAP-62](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0062.md) and [CAP-66](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0066.md) ## Protocol 23 (Testnet, August 14, 2025) ### Software | Software | Version | | --- | --- | | XDR | `v23.0` | | Rust XDR | `23.0.0` | | Smart Contract Host Environment | `23.0.0` | | Stellar Core | `23.0.0` | | Smart Contract Rust SDK | `23.0.0-rc.2.4` | | Stellar CLI | `23.0.1` | | Stellar RPC | `v23.0.0` | | Stellar Horizon | `v23.0.0` | | Stellar Galexie | `v23.0.0` | | Stellar Quickstart | `docker pull stellar/quickstart` | | Stellar JS Stellar Base | `v14.0.0` | | Stellar JS Stellar SDK | `v14.0.0` | | Stellar Horizon Client & TxnBuild | `v23.0.0` | | Stellar RPC Client | `v23.0.0` | | Freighter | | | Laboratory | `N/A` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes New features in Whisk, Protocol 23: - [Release Notes](https://github.com/stellar/stellar-core/releases/tag/v23.0.0rc4) - Unified Events: [CAP-67](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md) - State Archival: [CAP-62](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0062.md) and [CAP-66](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0066.md) ## Whisk, Protocol 23 Release Candidate (Testnet only, July 17, 2025) ### Software | Software | Version | Package | | --- | --- | --- | | XDR | `v23.0` | `n/a` | | Rust XDR | `23.0.0-rc.2` | `rust crate: stellar-xdr:23.0.0-rc.2` | | Stellar Rust Host Environment | `23.0.0-rc.2` | `rust crate: soroban-env-host:23.0.0-rc.2.1` | | Stellar Core | `23.0.0rc4` | `debian: stellar-core:23.0.0-2587.rc4` | | Stellar Rust SDK | `23.0.0-rc.2.2` | `rust crate: soroban-sdk:23.0.0-rc.2.1` | | Stellar CLI | `23.0.0` | `rust crate: stellar-cli:23.0.0` | | Stellar RPC | `v23.0.0-rc.2` | `debian: stellar-rpc:23.0.0~rc2-127` | | Stellar Horizon | `v23.0.0-rc2` | `debian: stellar-horizon:23.0.0~rc2-508` | | Stellar Quickstart | `main` | `docker.io/stellar/quickstart:sha256:d4f752eece1e8780d19f4bd2726845996c413c0f4c4f57d1e4a9ff450442fc29` | | Stellar JS Stellar Base | `v14.0.0-rc.2` | `NPM: 14.0.0-rc.2` | | Stellar JS Stellar SDK | `v14.0.0-rc.3` | `NPM: 14.0.0-rc.3` | | Freighter | `n/a` | `n/a` | | Laboratory | `n/a` | `n/a` | | Futurenet Network Passphrase | `n/a` | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `n/a` | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `n/a` | `Public Global Stellar Network ; September 2015` | ### Release notes New features in Whisk, Protocol 23: - [Release Notes](https://github.com/stellar/stellar-core/releases/tag/v23.0.0rc4) - Unified Events: [CAP-67](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md) - State Archival: [CAP-62](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0062.md) and [CAP-66](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0066.md) ## Protocol 22 (Mainnet, December 5, 2024) ### Software | Software | Version | | --- | --- | | XDR | `v22.0` | | Rust XDR | `v22.0.0` | | Soroban Environment | `v22.1.2` | | Stellar Core | `v22.0.0` | | Soroban Rust SDK | `v22.0.3` | | Stellar CLI | `v22.0.1` | | Stellar RPC | `v22.1.0` | | Stellar Horizon | `v22.0.1` | | Stellar Quickstart | `docker.io/stellar/quickstart:v455-latest@sha256:bbd4cea64c5428381ac5ace7c380ed7c3b72f12488aeb1f1bf19c48e74244af8` | | Stellar JS Stellar Base | `v13.0.1` | | Stellar JS Stellar SDK | `v13.1.0` | | Freighter | | | Laboratory | | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes #### Core New features in Protocol 22: - Constructor support in Soroban: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0058.md. - Soroban host functions for BLS12-381: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0059.md. #### Soroban Rust SDK Key protocol-related changes: - Support for constructors - Support for BLS12-381 host functions #### Stellar CLI (Previously Soroban CLI) ## Protocol 21 (Mainnet, June 18, 2024) ### Software | Software | Version | | --- | --- | | XDR | `v21.1` | | Rust XDR | `v21.0.1` | | Soroban Environment | `v21.0.2` | | Stellar Core | `v21.0.0` | | Soroban Rust SDK | `21.0.1-preview.3` | | Stellar CLI | `v21.0.0` | | Soroban RPC | `v21.3.0` | | Stellar Horizon | `v2.30.0` | | Stellar Quickstart | `https://hub.docker.com/layers/stellar/quickstart/v426-latest-amd64/images/sha256-274395daab6fa8033b9213f152d56699358917fb01d7c7e95392a37fc00c9d01` | | Stellar JS Stellar Base | `v12.0.0-rc1` | | Stellar JS Stellar SDK | `v12.1.0` | | Freighter | | | Laboratory | | | Soroban React Payment dapp | `TBD` | | Soroban Mint Token dapp | `TBD` | | Soroban Swap Token dapp | `TBD` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes #### Core New features in Protocol 21: - Secp256r1 support in Soroban host: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0051.md - Soroban host function for extending TTL of contract instance and code separately: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0053.md - Use refined cost model for VM instantiation in order to reduce the VM instantiation metered costs: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0054.md - Intra-transaction VM module caching for the further Soroban cost reduction: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0055.md, https://github.com/stellar/stellar-protocol/blob/master/core/cap-0056.md #### Soroban Rust SDK Key protocol-related changes: - Support for secp256r1 signature verification - Support for extending TTL of contract instance and code separate from each other #### Stellar CLI (Previously Soroban CLI) :::note Soroban CLI has been renamed to Stellar CLI. ::: - Add stellar-cli crate alongside soroban-cli - Rename to stellar-cli - Install stellar and soroban CLIs when installing either - Update completions to use stellar instead of soroban - Update other references to stellar-cli - Add support for contract id alias name when deploying and invoking contracts - Extract alias logic into its own implementation - Embed examples contract list into source and remove from build script - Add cache sub commands and allow for proper transaction data logging - Update TS Bindings stellar-sdk dep to 12rc2 - Add no-build option for fee::Args - Do not auto add test account to keys - Output TransactionEnvelope instead of Transaction for --build-only - Ledger signing - Use safe unwrapping in option unwrap - Exclude host only functions from client - Add container log tailing cmd - Update network container start command to use updated enable flags - Add libudev-dev as dep - Bump versions of dependencies - Update to newest soroban-rpc and copy over old signing logic - Remove deprecated lab token command - Remove deprecated config command - Remove lab xdr command - Add xdr command to root ## Protocol 21 (Testnet only, May 20, 2024) ### Software | Software | Version | | --- | --- | | XDR | `v21.1` | | Rust XDR | `v21.0.1` | | Soroban Environment | `v21.0.2` | | Stellar Core | `v21.0.0` | | Soroban Rust SDK | `21.0.1-preview.3` | | Soroban CLI | `v21.0.0-rc.1` | | Soroban RPC | `v21.2.0` | | Stellar Horizon | `v2.30.0` | | Stellar Quickstart | `https://hub.docker.com/layers/stellar/quickstart/v426-latest-amd64/images/sha256-274395daab6fa8033b9213f152d56699358917fb01d7c7e95392a37fc00c9d01` | | Stellar JS Stellar Base | `v12.0.0-rc1` | | Stellar JS Stellar SDK | `v12.0.0-rc.3` | | Freighter | | | Laboratory | | | Soroban React Payment dapp | `TBD` | | Soroban Mint Token dapp | `TBD` | | Soroban Swap Token dapp | `TBD` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes #### Core This is the first stable Core release supporting protocol 21. New features in protocol 21: - Secp256r1 support in Soroban host: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0051.md - Soroban host function for extending TTL of contract instance and code separately: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0053.md - Use refined cost model for VM instantiation in order to reduce the VM instantiation metered costs: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0054.md - Intra-transaction VM module caching for the further Soroban cost reduction: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0055.md, https://github.com/stellar/stellar-protocol/blob/master/core/cap-0056.md #### Soroban Rust SDK This is the first version of the Soroban SDK that supports protocol 21. It is marked as 'preview' because contracts built with SDK v21 will only be compatible with the networks upgraded to protocol 21. Key protocol-related changes: - Support for secp256r1 signature verification - Support for extending TTL of contract instance and code separate from each other ## Protocol 21: Preview 1 (Testnet only, April 12, 2024) ### Software | Software | Version | | --- | --- | | XDR | `v21.1` | | Rust XDR | `v21.0.1` | | Soroban Environment | `v21.0.1` | | Stellar Core | `v21.0.0rc1` | | Soroban Rust SDK | `21.0.1-preview.1` | | Soroban CLI | `v21.0.0-preview.1` | | Soroban RPC | `v21.0.1` | | Stellar Horizon | `v2.30.0` | | Stellar Quickstart | `` | | Stellar JS Stellar Base | `v11.1.0` | | Stellar JS Stellar SDK | `v12.0.0-rc.1` | | Freighter | `` | | Laboratory | `` | | Soroban React Payment dapp | `TBD` | | Soroban Mint Token dapp | `TBD` | | Soroban Swap Token dapp | `TBD` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Release notes #### Core This is the first Core release supporting protocol 21. New features in protocol 21: - Secp256r1 support in Soroban host: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0051.md - Soroban host function for extending TTL of contract instance and code separately: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0053.md - Use refined cost model for VM instantiation in order to reduce the VM instantiation metered costs: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0054.md - Intra-transaction VM module caching for the further Soroban cost reduction: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0055.md, https://github.com/stellar/stellar-protocol/blob/master/core/cap-0056.md #### Soroban Rust SDK This is the first version of the Soroban SDK that supports protocol 21. It is marked as 'preview' because contracts built with SDK v21 will only be compatible with the networks upgraded to protocol 21. The API itself may be considered stable. Key changes: - Support for secp256r1 signature verification - Support for extending TTL of contract instance and code separate from each other ## Protocol 20: Soroban Phase 2 (March 19, 2024) ### Software | Software | Version | | --- | --- | | XDR | [3da6ebcbd8afa01d5c94dbc7f0475f4c00089420](https://github.com/stellar/rs-stellar-xdr/commit/3da6ebcbd8afa01d5c94dbc7f0475f4c00089420) | | Soroban Environment | `v20.2.2` | | Soroban Interface Version | `0` | | Soroban Resource Limits | Phase 2 Limits | | Soroban Resource Fees | Phase 2 Fees | | Stellar Core | `v20.3.0` | | Soroban Rust SDK | `v20.4.0` | | Soroban CLI | `v20.3.1` | | Soroban RPC | `v20.3.3` | | Stellar Horizon | `v2.28.3` | | Stellar Friendbot | `TBD` | | Stellar Quickstart | `docker.io/stellar/quickstart:latest@sha256:1a82b17a4fae853d24189dd25d4e6b774fa7a1b6356a993e618c6e9bd2f3e04c` | | Stellar JS Stellar Base | [`v11.0.0`](https://github.com/stellar/js-stellar-base/releases/tag/v11.0.0) | | Stellar JS Stellar SDK | [`v11.2.2`](https://github.com/stellar/js-stellar-sdk/releases/tag/v11.2.2) | | Freighter | `v5.17.0` | | Laboratory | `v4.1.0` | | Soroban React Payment dapp | `TBD` | | Soroban Mint Token dapp | `TBD` | | Soroban Swap Token dapp | `TBD` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Changelog #### Core - Remove use of C99 that looks like Cxx20 designated initializers - Reduce scan size in phase1 - Add simulate subcommand to network survey script - Continue to capture SCP messages for previous ledger in database - Rewrite state loading path on startup - Add support for debug-tx-set in dump-xdr - Bucket cleanup - Update phase1 settings - Fix compile error (Visual C++) - Update soroban settings files and utils - Add new throttling metrics - Adds CLI tool to print BucketList archival stats - Update denominators - Set key size to initial value - Update max_entries_to_archive to be 1000. - Restrict "prev" test to just the voting path, to allow catchup. - Strkey update - Add scripts/extract-wasms.sh - Bump overlay min version to 32 - Fix noisy eviction scan warnings - Early initialization of soroban metrics #### Soroban Rust SDK - Display String contents in Debug implementation - Add Bytes to_buffer and to_alloc_vec - Move the Env testutil internal types into a single type - Add option to disable test snapshots on Env - Bump version to 20.4.0 ## Protocol 20: Soroban Phase 1 (February 27, 2024) ### Software | Software | Version | | --- | --- | | XDR | [8b9d623ef40423a8462442b86997155f2c04d3a1](https://github.com/stellar/rs-stellar-xdr/commit/8b9d623ef40423a8462442b86997155f2c04d3a1) | | Soroban Environment | `v20.2.2` | | Soroban Interface Version | `0` | | Soroban Resource Limits | Phase 1 Limits | | Soroban Resource Fees | Phase 1 Fees | | Stellar Core | `v20.2.0` | | Soroban Rust SDK | `v20.3.2` | | Soroban CLI | `v20.3.1` | | Soroban RPC | `v20.3.3` | | Stellar Horizon | `v2.28.3` | | Stellar Friendbot | `TBD` | | Stellar Quickstart | `docker.io/stellar/quickstart:latest@sha256:1a82b17a4fae853d24189dd25d4e6b774fa7a1b6356a993e618c6e9bd2f3e04c` | | Stellar JS Stellar Base | [`v11.0.0`](https://github.com/stellar/js-stellar-base/releases/tag/v11.0.0) | | Stellar JS Stellar SDK | [`v11.2.2`](https://github.com/stellar/js-stellar-sdk/releases/tag/v11.2.2) | | Freighter | `v5.17.0` | | Laboratory | `v4.1.0` | | Soroban React Payment dapp | `TBD` | | Soroban Mint Token dapp | `TBD` | | Soroban Swap Token dapp | `TBD` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Changelog #### Soroban RPC - (tag: v20.3.3) Bump version to 20.3.3 - Update Dockerfile and Makefile to refer rpc instead of tools - Fix publish-dry-run failures - Revert "soroban-rpc: Remove publish-dry-run Workflow" - Perform fee padding in a larger bit width of integers - (tag: v20.3.2) Bump version to 20.3.2 - Merge pull request #68 from stellar/add-spec-tools-crate - Merge branch 'main' into add-spec-tools-crate - Change module from soroban-tool to rpc in go.mod - Add hardcoded WASM file and reference it in tests - Uncomment install_rust - Add prometheus hook to count different log levels - Remove build-test-wasms from makefile #### Soroban CLI - (tag: v20.3.1) Bump version to 20.3.1 - Update references in end-to-end tests to point to the latest releases - Bump version of soroban rpc and spec tools - Repoint soroban-spec-tools to the moved one in stellar/soroban-rpc - Rename Cargo.toml in the init template files - soroban-cli: Remove ALL RPC Related Code and Workflows - Use soroban-rpc crate from the RPC repo - fix: embed the init template files in the build - [Epic] Separating soroban-rpc to prepare for repo change - Soroban contract init followup - Merge pull request #1190 from stellar/release/v20.3.0 - Merge branch 'main' into release/v20.3.0 - Revert "[Epic] Separating soroban-rpc to prepare for repo change" - [Epic] Separating soroban-rpc to prepare for repo change ## Protocol 20: Soroban Phase 1 (Mainnet Edition) (February 5, 2024) ### Software | Software | Version | | --- | --- | | XDR | [8b9d623ef40423a8462442b86997155f2c04d3a1](https://github.com/stellar/rs-stellar-xdr/commit/8b9d623ef40423a8462442b86997155f2c04d3a1) | | Soroban Environment | `v20.2.2` | | Soroban Interface Version | `0` | | Soroban Resource Limits | Phase 0 Limits | | Soroban Resource Fees | Phase 0 Fees | | Stellar Core | `v20.2.0` | | Soroban Rust SDK | `v20.3.2` | | Soroban CLI | `v20.3.0` | | Soroban RPC | `v20.3.1` | | Stellar Horizon | `v2.28.3` | | Stellar Friendbot | `TBD` | | Stellar Quickstart | `docker.io/stellar/quickstart:latest@sha256:8d6f6520ad3842042bfe4e271f8b2324ec2f128564487abedd3876cea83af4f1` | | Stellar JS Stellar Base | [`v11.0.0`](https://github.com/stellar/js-stellar-base/releases/tag/v11.0.0) | | Stellar JS Stellar SDK | [`v11.2.2`](https://github.com/stellar/js-stellar-sdk/releases/tag/v11.2.2) | | Freighter | `5.16.0` | | Laboratory | `v4.1.0` | | Soroban React Payment dapp | `v3.0.0` | | Soroban Mint Token dapp | `v3.0.0` | | Soroban Swap Token dapp | `TBD` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | | Mainnet Network Passphrase | `Public Global Stellar Network ; September 2015` | ### Changelog #### XDR - Run CI for the msrv and latest rust version - Backfill changes to next for json rendering - Bump XDR - Bump version to 20.1.0 #### Soroban Environment - Allow small version-range wiggle room on curve25519-dalek to enable docs.rs nightly build - Bump version to 20.2.2 - Enable publish of soroban-simulation crate - Add a function to invoke host function 'end-to-end' in recording mode. - Bug 1283 asset code rendering - Use strkeys for contract IDs and addresses in diagnostic events. - Turn off wasm_reference_types in Wasmi - Prng tests - Remove ConversionError from ScVal/Val conversions - Tightening up metering in auth - Bump XDR to 20.1 - Allow negative fee1 kb low - Bump version to 20.2.0 - Cover various Symbol conversion code paths with various valid/invalid cases - Run CI for the msrv and latest rust version - Add protocol version method to invoke_contract - Enable VM execution in a WASM environment by guarding time track behind time feature - Add test for checking VM stack depth. - Migrate preflight computations from soroban-rpc - soroban-simulate: Misc fixes - Add CI job to run cargo-semver-checks - Tracing - Add some basic test coverage for e2e_invoke. - Add test vectors for ed25519 edge cases - Trace should not emit diagnostic errors - Bump wasmi to 0.31.1-soroban.20.0.1 - Bump version to 20.1.1 #### Soroban Rust SDK - Update soroban-env-\* - Bump version to 20.3.2 - Update extend_ttl docs - Bug 1076 conversion error flattening #### Soroban RPC - Migrate Soroban Tools to Soroban RPC - Use soroban-tools Crates - Pull in Recent Soroban RPC changes from soroban-tools - Mirror Last Remaining PRs from soroban-tools Repo - Add Workflow to Publish soroban-rpc Crate - added user agent config on ha archive pool - Update getTxn rpc with events data - Remove publish-dry-run Workflow - Use external soroban-simulation library for preflight computations - Store and serve the event transaction ID - Reduce event memory footprint - Add diagnostic events to sendTransaction response - Remove panics from internal codebase #### Soroban CLI - feat: soroban init command - Bump dependencies for pubnet release - Upgrade Ubuntu to 22.04 from 20.04 - Bump Go, Rust and Core dependencies - feat/cli: Move config commands to top level - bindings-ts: update to latest SDK & TypeScript, add CI test - TypeScript bindings have been updated to use the latest stellar-sdk - Support multi-auth workflows in typescript bindings - Replace cli xdr command with stellar-xdr cli - Update typescript bindings for latest versions - Warn about RC versions only when using pubnet ## Stable v20.1.0 (January 11, 2024) ### Software | Software | Version | | --- | --- | | XDR | [bb54e505f814386a3f45172e0b7e95b7badbe969](https://github.com/stellar/stellar-xdr/commit/bb54e505f814386a3f45172e0b7e95b7badbe969) | | Soroban Environment | `v20.1.0` | | Soroban Interface Version | `0` | | Stellar Core | `v20.1.0` | | Soroban Rust SDK | `v20.2.0` | | Soroban CLI | `v20.2.0` | | Soroban RPC | `v20.2.0` | | Stellar Horizon | `v2.27.0` | | Stellar Friendbot | `TBD` | | Stellar Quickstart | `docker.io/stellar/quickstart:soroban-dev@sha256:64b2d14b8a531c534560e287768846f538b2f063fc776aa9ca016c788e86c782` | | Stellar JS Stellar Base | [`v10.0.1`](https://github.com/stellar/js-stellar-base/releases/tag/v10.0.1) | | Stellar JS Stellar SDK | [`v11.2.0`](https://github.com/stellar/js-stellar-sdk/releases/tag/v11.2.0) | | Freighter | `5.9.0` | | Laboratory | `TBD` | | Soroban React Payment dapp | `TBD` | | Soroban Mint Token dapp | `TBD` | | Soroban Swap Token dapp | `TBD` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | ### Changelog #### Soroban Rust SDK - Fix bug with Timepoint/Duration as parameters/returns - Add storage update fns - Export functions for creating Timepoint/Duration - Allow `&Env` in contract fns ## Stable v20.0.0 (December 18, 2023) ### Software | Software | Version | | --- | --- | | XDR | [bb54e505f814386a3f45172e0b7e95b7badbe969](https://github.com/stellar/stellar-xdr/commit/bb54e505f814386a3f45172e0b7e95b7badbe969) | | Soroban Environment | `v20.0.0` | | Soroban Interface Version | `0` | | Stellar Core | `v20.0.1` | | Soroban Rust SDK | `v20.0.0` | | Soroban CLI | `v20.0.2` | | Soroban RPC | `v20.0.2` | | Stellar Horizon | `v2.27.0` | | Stellar Friendbot | `TBD` | | Stellar Quickstart | `docker.io/stellar/quickstart:testing@sha256:3c7947f65db493f2ab8ca639753130ba4916c57d000d4a1f01ec530e3423853b` | | Stellar JS Stellar Base | [`v10.0.0`](https://github.com/stellar/js-stellar-base/releases/tag/v10.0.0) | | Stellar JS Stellar SDK | [`v11.0.1`](https://github.com/stellar/js-stellar-sdk/releases/tag/v11.0.1) | | Stellar JS Soroban Client | [`v1.0.0`](https://github.com/stellar/js-soroban-client/releases/tag/v1.0.0) (deprecated, prefer the Stellar SDK) | | Freighter | `5.12.0` | | Laboratory | `4.11.0` | | Soroban React Payment dapp | `3.0.0` | | Soroban Mint Token dapp | `3.0.0` | | Soroban Swap Token dapp | `TBD` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | ### Changelog #### XDR - Update the docs for extendTTL/restore ops to match the threshold change #### Soroban Environment - Make RecordedAuthPayload consistently return None for invoker. - Expiration-related fixes - Turn dynamic borrow panics into HostErrors - Use Ed25519 verify_strict function rather than just verify, fix #857 - Misc int32 issues - Enforce object handle integrity when inserting into containers, fix #569 - Add testcase for out-of-order scmaps, fix #223. - Add a function to compute the rent fee. - Use host.err for auth error - Add a smoke test for recording auth for create contract host fn. - Stop treating storage errors as missing entries - Refactor authorization manager to only maintain mutable borrow on minimal amount of fields - Bump xdr - Add rent bumps to the SAC - Add is_admin function - Bump xdr - Add function to compute the write fee based on the ledger size. - Prohibit using disjoint signatures to cover the auth tree. - Enforce DepthLimiter in the Host to avoid stack overflow - Relative objects in wasm - Adapt to ResourceLimiter, replacing mem_fuel metering - Make del_contract_data no-op for removing non-existent instance storage key. - Add "tracy" feature to enable Tracy profiler, with some basic annotations - Update host to account for the XDR changes. - Update rust-version - Adapt to SCError change to be an enum, with ContractError(u32) - Make some host errors non-recoverable in try_call. - Fix panic-string-logging code path broken by recent dynamic-borrow fix. - Add soroban-bench-utils, add benchmark tests to measure metering accuracy - Bump env xdr and do the fee library changes corresponding to config changes - Bump env xdr - Remove event topic limits - Bump env xdr - Unify/fix expiration bump logic in host. - Add new tests for error escalation from contract calls. - Add a helper that invokes a host function 'end-to-end' - Add helpers for container bulk init; applies to auth metering - Switch some auth errors from Internal to InvalidInput. - Enable post-MVP WASM ops (sign-ext and mutable-globals), fix #968. - More token tests - Alloc example - Mop up some residual uses of format strings in errors (no longer supported) - Add wasm for upgrade write-bytes contract - Scale the linear cost model coefficient; improve model fitting - Error if bumping past max_entry_expiration and host function to retrieve max_entry_expiration - Make has checks to properly populate the storage map in recording mode - Clean up budget cost types - Add debug events to storage error reporting. - Switch to stable rust-analyzer in CI - Add some comments and tests to env-common/symbol.rs - Upgrade dalek crates to new stable versions. - Add an option to return an error when encountering non-root auth in recording mode. - Reject env.json if there are duplicate export names, fix #189 - Tighten signature of unchecked_visit_val_obj, fix #595 - Tighten dependencies further - Fix incorrect argument name - Bump env xdr - Tighten up Map and Vector and metering coverage - Store dummy instance for test contracts. - Update XDR to take change that removes SCSpecTypeSet - Fix comparison in Tag::is_object - Fix calibration due to delak change - Charge write fees for expiration entry bumps. - Reduce the expiration entry write size. - Refactor host to support the new expiration ledger approach. - Add lifetime threshold - Enable build workflow for merge groups - Remove key size from rent change computation. - Take change from txSOROBAN_RESOURCE_LIMIT_EXCEEDED to txSOROBAN_INVALID - Add ExpirationEntry support - Add git rev dep check to ci - Fix encode contract events metering - Trim deps - Reject vals with invalid tags, fix #1029 - Fix EXPIRATION_ENTRY_SIZE constant - Avoid iloop externalizing diagnostics for invalid references - Add "coverage" Makefile target for lcov.info, add a test that extends coverage - Fix asset-code rendering in native contract. - Fix rent changes extraction bug - Graydon code review - Jay code review - Bump xdr and use curr instead of next - Trivial xdr bump - Tighten wasm interface version checks, and do on upload. - Update wasmi to 0.31.0-soroban - Bump version to 20.0.0-rc1 - Add "next" feature to crates using xdr directly or indirectly - Move out-slice len adjustment from symbol_copy_to_slice to its caller. - Bump interface and regenerate wasms to handle symbol semantics change. - No-op rename: remove metered from metered_scan_slice_of_slices - Get rid of an unnecessary vector allocation in map_new_from_slices - Remove metered map iterator functions. - Small vec cleanup - Code cleanup around PRNG - Factor Tag::is_object to let Val::is_object delegate to it - Set LedgerEntry extensions on existing entries - Attach the auth in recording mode to any valid tracker. - Improve storage error reporting for token contract. - Env-common cleanups and dead code removal - More cleanups, comments and fixes from code review. - Make the 'unknown' wasmi error to be InvalidAction instead of InternalError - Error improvements - Small update for the default budget limits. - Calibration for PRNG and charge budget - Code review, cleanup, removal of dead/dangerous host functions - Add redundant "next" dependencies - Host code review, minor cleanups and code reorganization - Fix the instance storage update logic. - Feature: Expose rem_euclid functions on 256 bit numbers - Replace address bytes conversions with strkey conversions. - Use Address for call_account_contract_check_auth test helper. - More code review and cleanup of host.rs module - Budget fiddling - Bump env XDR - State expiration renames - Cost type cleanup, bug fix, improve calibration - Code review on fees, crypto, prng, dispatch - Storage code review - Setup bench framework to run experiments - Decorate error for nonce missing from the footprint. - Remove bad LEs - Add missing fixed-size metering in comparison.rs, clean up a bit - Properly dispatch test contracts based on the executable. - Tighten up metering for linear memory routines - conversion.rs code review fixes - Review fixes for declared_size and metered_clone - Gate recording auth behind the feature. - Fix test wasms for latest sdk - Cackle gate - Remove spendable_balance from SAC. - Extend upgrade contract from 30 to 60 days - Bug 1042 internal renames - Remove panic - Stop caching authorization tracker verification status. - Overhaul "free budget" and migrate non-metered code - Clarify (and add redundant defensive code) in check_val_integrity - Remove a panic and be more conservative about depth limit - Add conceptual overview comment to auth module - Fix fuzz-found panic in Prng::u64_in_inclusive_range - Fix fuzzer-found frame stack corruption in rollback if instance storage fails - Arbitrary-compatible expr generation and host fuzzing - Improve auth test coverage a bit - Rollback tests - Add a test that ensures that SAC reentry is not possible. - Remove vnext test wasms - Add fn to find out if Host can be finished - Add tests for more wasmi trap conditions - Storage tests - Add tests that try to build a deep host stack. - More events and diagnostics testing - Fix #1174 fuzzer-found deserialization of non-representable ScVal bug. - tighten up handling of debug mode - Update rust-version - Adapt to rs-stellar-xdr Limits change - Add tests that cover fees for each individual resource. - Cover non-existent Wasm in test for updating Wasm. - Adds loadgen test WASM - Add wasm test for excessive initial memory and table size - Initial cut at bug 872, observe side effects of tests - Remove unnecessary copy of value - Test update_contract_wasm with rollbacks. - Bug 1146 test invalid val bit patterns - Add @dmkozh as code owner of soroban-env-host - Hook envbase and vec slices - Update stellar-xdr - Don't block merges on rust-analyzer compat check - Observe most of the remainder of the testsuite - Update deps, fix #1200 - Test nested extend_ttl - Fix #1175 error code spoofing - Do not error if no authorizations - Add a contract that allows materializing arbitrarily large values. - Updated loadgen Wasm - Cap persistent extension to max - Error on issuer for mint, burn, clawback, and burn_from - Add a very simple hook for tracking top-level contract invocations. - Macro for testing host function dispatch with bad inputs - Check val integrity more - Adapt to Rust 1.74 - Improve test coverage for crypto functions. - Pub ContractInvocationEvent - Pass ref to host to contract invocation hook - Fix top contract invocation hook and auth interaction - XDR limits updates - Run user seeds to PRNGs through HMAC-SHA256 to unbias - Add size_hint methods to Arbitrary impls - Add tests for de-serializing deep XDR in deep call stack. - Generate combinational tests for linear memory functions - Add tests that grow containers until running out of memory budget. - Observe more things - Setting pre_release_version to zero in meta.rs in preparation of Soroban launch - Add Host::has_frame - Validated TTLentries - Budget subsystem code review - Rename extend host functions - Fuzz fixes - Cap number of args to wasm functions - Cargo version pinning - Pass --locked to publish as well - Tighten up saturating\_ math checks - Auth code review - Remove unused publish scripts/make targets - Write tests for unrecoverable errors with try_call - Fix the invoker contract auth rollback logic and cover it in the tests. - Re-calibration on x86 and update cost parameters - Add tests for invalid maps, oversized maps/vecs/bytes from various paths - Frame code review - Improve XDR conversion coverage - Add more error tests - Improve budget tracker - Validate assets when trying to create SAC instance. - Conversion tests - Update stellar-xdr - Change string_new_from_slice to use &[u8] instead of &str. - Update stellar-xdr - Initial SAC review - Derive debug, eq, ord on CostTracker for SDK - Noop auth fix - Test bad WASMs - Add time tracker - Bump wasmi and xdr to release versions - SAC final code review - Add fuzz target that runs wasmi on wasm-smith output - Remaining host code review - Bump version to 20.0.0 #### Soroban Rust SDK - Token events - Update rust-version - Update SDK to recent env - Add admin function - fix: use uppercase const name - Fix allocator - Add Arbitrary impl for Duration and Timepoint - Fix Budget::memory_bytes_cost - Bump env to c5607a2e9e296b2636b46dc910387aa3446b3e29 - Bump env - Update dalek, remove non-syn2 exemptions, tighten deps - Update map iterator to be index based - Remove ScSpecTypeSet support, which was already mostly dead. - Provide a way to allow non-root auth in recording mode. - Update env version in SDK. - Update SDK to support expiration entry rework. - Bump env for bump interface changes - Correct comment on String::copy_into_slice - Add doc comments about why symbol_short! - Add test vector for workspace setups where contract types live in a lib - Update rust-version - Enable build workflow for merge groups - re-enable linux arm64 builds - Fix/criadoperez - Text corrections - Update rust-version - bump env - Fix name of the Stellar Asset admin client - Add git rev dep check to ci - Remove authorized from standard token interface - Bump env - Adapt to removal of ConversionError from number type conversions. - Bump version to 20.0.0-rc1 - Implement deployer functions that return the deployed contract id. - Add Vec::to_vals - Expose PRNG functions - Expose secp256k1 and keccak256 in the SDK - Upgrade env to v20.0.0-rc2 - Elaborate more in comment on PRNG strengths and weaknesses - Bump version to 20.0.0-rc2 - More explicit PRNG-explaining comments - Adapt to minor env changes from code review - Adapt to recent changes in env - docs: Fixing a broken link in the SDK's lib.rs file - Fix typos in arbitrary docs - Change Storage::get to pub(crate) - Add fn to expose max expiration available - Update rust-version - fix(snapshot): set entry expiration info in set_ledger_info - Add SDK support for Address/StrKey conversions. - bump env and xdr for state exp rename - bump env - Feature: expose rem_euclid functions and add 128/256 bit conversions - Fix type of signature in auth - Add testutils for accessing all storage of a contract - Remove spendable balance - Fix nightly lints on unused internal exports - Generate all types of addresses when fuzzing. - Use deterministic randomness in arbitrary tests - Generate arbitrary containers with heterogeneous elements - Add SorobanArbitrary implementations for tuples. - Update env version - Fix docs about prng seed - Refactor Prng functions - Add prng generation for slices and arrays - Make easier to add shuffle to other types - Make Env::default and Env::from_snapshot configure the environment the same - Arbitrary testutils - Remove use of rand from generated addresses, nonces, salts, and issuer pks - Replace String::from_slice with from_str - Update soroban-env-\* deps - Encode network id as hex in ledger snapshots - Autosave a test snapshot file on every test exit - Allow bumping other instances - Update env - Make Option work with contract type - Rename token::Interface/Client to TokenInterface/Client - Don't write test snapshot if no thread name - Minor tidy and organization of test snapshots - Don't block merges on rust-analyzer compat check - Expand Env test to check that separate tests are written for multiple Envs - Do not record test snapshots for doc tests - Check that no diffs exist after test run due to uncommitted test snapshots - Add auth to Env snapshot - Update env and xdr - Snapshot all auths during a test not just last invokes - Update rust-version - Improvements to Arbitrary Options - Hide EnvBase and other internal types - Restore previous auth state in client - Make try\_ fns return smaller SDK error type - Remove nextest from Makefile - bump env - Renamed extend host function - Update env - Bump version to 20.0.0 #### Soroban RPC - increased preflight instruction fee padding to 3 million - Enforce enabling diagnostics events - Add getPreflight benchmark and test - Add write-through cache for config ledger entries - Cache all ledger entries queried from DB in read transaction - simulateTransaction automatically detects ledger entries which require restoring - Add heap profiling endpoints - Try to return diagnostic events on failure from simulateTransaction - Add generic panic handling - Limit request size to 10MB - getLedgerEntries can query multiple ledger entries at once - Stream ledgers on initialization - Set maximum number of keys to query for getLedgerEntries - Add support for new state expiration ledger entries - Ingest temp ledger entry evictions - Lower max http request size - Set base prng to zero in preflight and invoke - Exclude temporary expired entries from SnapshotSource in preflight - Include system events in fee calculation in preflight - Get expiration ledger sequence at source - Add a "Building from Source" README.md - Flatten the getEvents response structure - Enable debug by default for preflight - Optimize in-memory transaction store - Force On-Disk Mode - Fix datarace in bufferedResponseWriter.WriteOut - Fix upsert bug in DB cache - Fix ledger entry visibility bug - Fix unwrap() errors in libpreflight due to bugs in the Go/Rust interface - Fix simulation sequence number for bump/restore operations - Improve missing command line arguments message - Fix caching of GetLatestLedgerSequence - Ensure that the sim events are logged; improve format of main logs - Validate xdr payloads in soroban-rpc requests - Fix double-counting bug in preflight - Fix potential overflow - Fix multi-entry responses for getLedgerEntries method - Update jrpc2 version to enable application/json; charset=utf-8 - Unify all ledger sequence types to uint32 and stop stringyfying integers < 53-bits wide - Restore CORS support #### Soroban CLI - Allow fetching contract from network - Each generated contract method adds -file-path - Add two new output types for contract inspect - Add config identity fund to fund accounts on networks - Add Auth-next signing support - Add dotenv so directories can now set CLI args - Add key::Args and FullLedgerEntry/FullLedgerEntries - Remove sandbox - Add Assembled Transaction that handle preparing transaction post simulation - Make aliases visible in help doc - Fix --cost flag - Set the exit code to 1 in case of an error - Return an error once contract read is unable to read any entry - Wrap token no longer fails with valid inputs in sandbox mode - fund command now can accept a public strkey - Warn or Error When Deploying Contracts Compiled with RC Version of Soroban SDK - Print help if custom CLI is empty & replace unwrap where possible - Fix various typescript bindings - Make output consistent for all ways to get version ## Preview 11 (September 11, 2023): Testnet and Futurenet Edition ### Software | Software | Version | | --- | --- | | XDR | [9ac02641139e6717924fdad716f6e958d0168491](https://github.com/stellar/stellar-xdr/commit/9ac02641139e6717924fdad716f6e958d0168491) | | Soroban Environment | `v20.0.0-rc2` | | Soroban Interface Version | `57` | | Stellar Core | `20.0.0-1504.rc2.22088c1f2.focal` | | Soroban Rust SDK | `v20.0.0-rc2` | | Soroban CLI | `v20.0.0-rc4` | | Soroban RPC | `v20.0.0-rc4` | | Stellar Horizon | `2.27.0~rc2-384` | | Stellar Friendbot | `soroban-v0.0.2-alpha` | | Stellar Quickstart | `docker.io/stellar/quickstart:testing@sha256:0c756150e7b3c53603fe36bb932c4e7d7ceaef691906b2d3d952771ccc195559` | | Stellar JS Stellar Base | [`v10.0.0-beta.3`](https://github.com/stellar/js-stellar-base/releases/tag/v10.0.0-beta.3) | | Stellar JS Stellar SDK | [`v11.0.0-beta.5`](https://github.com/stellar/js-stellar-sdk/releases/tag/v11.0.0-beta.5) | | Stellar JS Soroban Client | [`v1.0.0-beta.3`](https://github.com/stellar/js-soroban-client/releases/tag/v1.0.0-beta.3) (deprecated, prefer the Stellar SDK) | | Freighter | `5.6.1` | | Laboratory | `2.12.0` | | Soroban React Payment dapp | `2.0.0` | | Soroban Mint Token dapp | `2.0.0` | | Soroban Swap Token dapp | `2.0.0` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | | Testnet Network Passphrase | `Test SDF Network ; September 2015` | :::note The Soroban RPC version for Preview 11 is presently in Stellar's unstable and testing apt repositories. Additionally, its Docker image release can be accessed at [docker.io/stellar/soroban-rpc:20.0.0-rc3-39](https://hub.docker.com/layers/stellar/soroban-rpc/20.0.0-rc3-39/images/sha256-227170869bca74998e1b6cc1e07deb3aca8d03e36154b3da700d6669e19d485b?context=explore). ::: ### Changelog #### XDR - Generate for entry expired error - Bump XDR - Generate XDR DepthLimiter and depth-limited Read/WriteXdr - Bump XDR - Make SCError into a union to allow user errors to be u32 - Update rust-version - Bump XDR - Bump XDR - Bump XDR - Regen to pick up ContractCostType changes - Bump XDR - Update to reflect removal of SCSpecTypeSet - Bump XDR - Take change from txSOROBAN_RESOURCE_LIMIT_EXCEEDED to txSOROBAN_INVALID - Run CI via in merge queue - Update rust-version - Bump curr and next - Bump XDR - Bump version to 20.0.0-rc1 #### Soroban Environment - Attach the auth in recording mode to any valid tracker when possible - Make RecordedAuthPayload consistently return None for invoker - Expiration-related fixes - Turn dynamic borrow panics into HostErrors - Use Ed25519 verify_strict function rather than just verify, fix #857 - Misc int32 issues - Enforce object handle integrity when inserting into containers, fix #569 - Add testcase for out-of-order scmaps, fix #223 - Add a function to compute the rent fee - Use host.err for auth error - Add a smoke test for recording auth for create contract host fn - Stop treating storage errors as missing entries - Refactor authorization manager to only maintain mutable borrow on minimal amount of fields - Bump xdr - Add rent bumps to the SAC - Add is_admin function - Bump xdr - Add function to compute the write fee based on the ledger size - Prohibit using disjoint signatures to cover the auth tree - Enforce DepthLimiter in the Host to avoid stack overflow - Relative objects in wasm - Adapt to ResourceLimiter, replacing mem_fuel metering - Make del_contract_data no-op for removing non-existent instance storage key - Add "tracy" feature to enable Tracy profiler, with some basic annotat… - Update host to account for the XDR changes - Update rust-version - Adapt to SCError change to be an enum, with ContractError(u32) - Make some host errors non-recoverable in try_call - Fix panic-string-logging code path broken by recent dynamic-borrow fix - Add soroban-bench-utils, add benchmark tests to measure metering accuracy - Bump env xdr and do the fee library changes corresponding to config changes - Bump env xdr - Remove event topic limits - Bump env xdr - Unify/fix expiration bump logic in host - Add new tests for error escalation from contract calls - Add a helper that invokes a host function 'end-to-end' - Add helpers for container bulk init; applies to auth metering - Switch some auth errors from Internal to InvalidInput - Enable post-MVP WASM ops (sign-ext and mutable-globals), fix #968 - More token tests - Alloc example - Mop up some residual uses of format strings in errors (no longer supported) - Add wasm for upgrade write-bytes contract - Scale the linear cost model coefficient; improve model fitting - Error if bumping past max_entry_expiration and and host function to retrieve max_entry_expiration - Make has checks to properly populate the storage map in recording mode - Clean up budget cost types - Add debug events to storage error reporting - Switch to stable rust-analyzer in CI - Add some comments and tests to env-common/symbol.rs - Upgrade dalek crates to new stable versions - Add an option to return an error when encountering non-root auth in recording mode - Reject env.json if there are duplicate export names, fix #189 - Tighten signature of unchecked_visit_val_obj, fix #595 - Tighten dependencies further - Fix incorrect argument name - Bump env xdr - Tighten up Map and Vector and metering coverage - Store dummy instance for test contracts - Update XDR to take change that removes SCSpecTypeSet - Fix comparison in Tag::is_object - Fix calibration due to delak change - Charge write fees for expiration entry bumps - Reduce the expiration entry write size - Refactor host to support the new expiration ledger approach - Add lifetime threshold - Enable build workflow for merge groups - Remove key size from rent change computation - Take change from txSOROBAN_RESOURCE_LIMIT_EXCEEDED to txSOROBAN_INVALID - Add ExpirationEntry support - Add git rev dep check to ci - Fix encode contract events metering - Trim deps - Reject vals with invalid tags, fix #1029 - Host: fix EXPIRATION_ENTRY_SIZE constant - Avoid iloop externalizing diagnostics for invalid references - Add "coverage" Makefile target for lcov.info, add a test that extends coverage - Fix asset-code rendering in native contract - Fix rent changes extraction bug - Graydon code review - Jay code review - Bump xdr and use curr instead of next - Trivial xdr bump - Tighten wasm interface version checks, and do on upload, Fix #1052 - Update wasmi to 0.31.0-soroban - Bump version to 20.0.0-rc1 #### Soroban Rust SDK - Implement deployer functions that return the deployed contract id - Add Vec::to_vals - Expose PRNG functions - Expose secp256k1 and keccak256 in the SDK - Upgrade env to v20.0.0-rc2 - Elaborate more in comment on PRNG strengths and weaknesses - Bump version to 20.0.0-rc2 - Token events - Update rust-version - Update SDK to recent env - Add admin function - fix: use uppercase const name - Fix allocator - Add Arbitrary impl for Duration and Timepoint - Fix Budget::memory_bytes_cost - Bump env to c5607a2e9e296b2636b46dc910387aa3446b3e29 - Bump env - Update dalek, remove non-syn2 exemptions, tighten deps - Update map iterator to be index based - Remove ScSpecTypeSet support, which was already mostly dead - Provide a way to allow non-root auth in recording mode - Update env version in SDK - Update SDK to support expiration entry rework - Bump env for bump interface changes - Correct comment on String::copy_into_slice - Add doc comments about why symbol_short! - Add test vector for workspace setups where contract types live in a lib - Update rust-version - Enable build workflow for merge groups - re-enable linux arm64 builds - Fix/criadoperez - Text corrections - Update rust-version - bump env - Fix name of the Stellar Asset admin client - Add git rev dep check to ci - Remove authorized from standard token interface - Bump env - Adapt to removal of ConversionError from number type conversions - Bump version to 20.0.0-rc1 #### Soroban RPC - List --network under RPC options - Enforce enabling diagnostics events - simulateTransaction will automatically detect ledger entries which require restoring - simulateTransaction will try to return diagnostic events on failure - getLedgerEntries can query multiple ledger entries at once - getLedgerEntries can set the maximum number of keys to query for - Temporary ledger entry evictions are ingested - Support StrKey format for contractIds field in getEvents request - Limit the execution duration of the jrpc requests - Performance fixes and improvements: add writethrough cache for config ledger entries and cache DB results better - Fix ledgerentry visibility bug - Fix simulation sequence number for bump/restore operations - Improve missing command line arguments message - Add missing config settings in ledger entry cache on reads - Fix caching of GetLatestLedgerSequence - Limit request size to 10MB - Limit number of concurrent requests - Improve HTTPRequestDurationLimiter by adding a recover handling - Stream ledgers on initialization - Increase instruction leeway to 20% in transaction simulation - Validate xdr payloads in soroban-rpc requests - Fix double-counting bug - Fix datarace in bufferedResponseWriter.WriteOut - Fix set_authorization_entries bug in transaction simulation - Restore CORS support #### Soroban CLI - Add multi-party authorization + signing support - Add two new output types for contract inspect - Add config identity fund to fund accounts on networks - Add restore/bump support with --wash_hash - Allow passing true and false to boolean types - Each generated contract method adds `-file-path` - Add dotenv so directories can now set CLI args - Generated TypeScript bindings have significantly improved - Make Wallet injectable - Correct generated README - Allow fetching contract from network - Removed --contract-name, added --overwrite - Require configuring network settings - Leveraging the latest soroban-client (v1.0.0-beta) - Make aliases visible in help doc - Ensure that the sim events are logged; improve format of main logs - Fix --cost flag - Set the exit code to 1 in case of an error - Return an error once contract read is unable to read any entry - wrap token no longer fails with valid inputs in sandbox mode ## Preview 10 (July 13, 2023) ### Software | Software | Version | | --- | --- | | XDR | [e372df9f677961aac04c5a4cc80a3667f310b29f](https://github.com/stellar/stellar-xdr/commit/e372df9f677961aac04c5a4cc80a3667f310b29f) | | Soroban Environment | `v0.0.17` | | Soroban Interface Version | `51` | | Stellar Core | `19.12.1-1406.b7d3a8f8d.focal~soroban` | | Soroban Rust SDK | `v0.9.2` | | Soroban CLI | `v0.9.1` | | Soroban RPC | `v0.9.2` | | Stellar Horizon | `stellar-horizon:2.26.1~soroban-373` | | Stellar Friendbot | `soroban-v0.0.2-alpha` | | Stellar Quickstart | `stellar/quickstart:soroban-dev@sha256:8a99332f834ca82e3ac1418143736af59b5288e792d1c4278d6c547c6ed8da3b` | | Stellar JS Stellar Base | `10.0.0-soroban.3` | | Stellar JS Soroban Client | `0.9.2` | | Freighter | `5.2.3` | | Laboratory | `2.11.0` | | Soroban React Payment dapp | `1.1.0` | | Soroban Mint Token dapp | `1.1.0` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | ### Changelog #### XDR - Remove TransactionResultV2 - Regenerate for overhaul of error codes - Bump xdr - Bump XDR - Update for state-expiration XDR changes - Update rust-version - Bump XDR - Regenerate for new crypto cost types - Bump XDR - Bump op - Generate for storage type removal - Drop ContractCostType::VerifyEcdsaSecp256k1Sig - Update next to pickup int256 cost types - Regenerate rename - Regenerate for typo - Bump xdr with contract instance storage changes - Regenerate for removal of ScVal::StorageType - Regenerate for RestoreFootprintOp - Generate rename - Update crate-git-revision #### Soroban Environment - Move a bunch of code out of host, reorg modules slightly. - Remove temp storage, fixes #758 - Bump xdr - Anti features - Err reform - Some further cleanups to diagnostic events - Bump xdr - Enforce AUTH_REVOCABLE on contract balances - Implement PRNG subsystem (round 2) - Remove incorrect debug_assert from ed25519_pub_key_from_bytes - Support Auth Next for host fns - Bump xdr - Update rust-version - Switch to wasmi native fuel metering - Fix get_authenticated_authorizations test utility. - Change authentication contract errors to be auth errors. - Improve VmInstantiation calibration - Update storage interface for state expiration - Update README.md - Implement contract expiration bumps - Calibrate wasmi fuel and tiered wasm instructions - Bump env XDR - Implement `TryFrom<&Error>` for ScVal - Provide host function for authorizing deep contract calls from current contract - Switch auth from autoincrement nonces to temporary random values. - Fix metering for internal events - Return strings for name/symbol and use strkey for issuer. - Remove Mergeable and rename Exclusive to Persistent - Add secp256k1 and keccak256 host functions - Add host functions for returning contract ids - Fix comparisons - Rename rawval to val - Enforce readonly footprint on bump - Add host functions for `{u,i}256` arithmetics - More raw to val - Support Timepoint, Duration - Bump xdr - Check map order when rebuilding from exact iter - Updates for contract instance related XDR changes. - Change StorageType to Just Be An Enum - Bump xdr for RestoreFootprintOp - Bump xdr for rename - Add host function for bumping contract instance - Implement contract instance storage in Soroban host. - Change `[IU]256` arithmetic host functions to take *Val not *Object. - Replace increase and decrease functions with approve - Expose bumps - Fix calibration tests - Change `[IU]256` `[from,to]_bytes` functions to work with Val - Bump wasmi version #### Soroban Rust SDK - Fix storage comment - Noop in instance bump for tests #### Soroban RPC - Update js-soroban-client dependency from 0.9.0 to 0.9.1 #### Soroban CLI - Update js-soroban-client dependency from 0.9.0 to 0.9.1 ## Preview 9 (May 24th, 2023) ### Software | Software | Version | | --- | --- | | XDR | [2f16687fdf6f4bcfb56805e2035f69997f4b34c4](https://github.com/stellar/stellar-xdr/commit/2f16687fdf6f4bcfb56805e2035f69997f4b34c4) | | Soroban Environment | `v0.0.16` | | Soroban Interface Version | `37` | | Stellar Core | `19.10.1-1310.6649f5173.focal~soroban` | | Soroban Rust SDK | `v0.8.4` | | Soroban CLI | `v0.8.0` | | Soroban RPC | `v0.8.0` | | Stellar Horizon | `stellar-horizon:2.25.1~soroban-346` | | Stellar Friendbot | `soroban-v0.0.2-alpha` | | Stellar Quickstart | `stellar/quickstart:soroban-dev@sha256:57e8ab498bfa14c65595fbb01cb94b1cdee9637ef2e6634e59d54f6958c05bdb` | | Stellar JS Stellar Base | `9.0.0-soroban.1` | | Stellar JS Soroban Client | `0.7.0` | | Freighter | `5.0.1` | | Laboratory | `2.10.0` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | ### Changelog #### XDR See https://github.com/stellar/rs-stellar-xdr/releases v0.0.16 for more details. #### Soroban Environment See https://github.com/stellar/rs-soroban-env/releases v0.0.16 for more details. #### Soroban Rust SDK See https://github.com/stellar/rs-soroban-sdk/releases v0.8.4 for more details. #### Soroban RPC See https://github.com/stellar/soroban-tools/releases v0.8.0 for more details. #### Soroban CLI See https://github.com/stellar/soroban-tools/releases v0.8.0 for more details. ## Preview 8 (April 4th, 2023) ### Software | Software | Version | | --- | --- | | XDR | [7356dc237ee0db5626561c129fb3fa4beaabbac6](https://github.com/stellar/stellar-xdr/commit/7356dc237ee0db5626561c129fb3fa4beaabbac6) | | Soroban Environment | `v0.0.15` | | Soroban Interface Version | `32` | | Stellar Core | `19.8.1-1246.064a2787a.focal~soroban` | | Soroban Rust SDK | `v0.7.0` | | Soroban CLI | `v0.7.0` | | Soroban RPC | `v0.7.0` | | Stellar Horizon | `stellar-horizon:2.24.62~soroban-338` | | Stellar Friendbot | `soroban-v0.0.2-alpha` | | Stellar Quickstart | `stellar/quickstart:soroban-dev@sha256:a057ec6f06c6702c005693f8265ed1261e901b153a754e97cf18b0962257e872` | | Stellar JS Stellar Base | `8.2.2-soroban.12` | | Stellar JS Soroban Client | `v0.5.0` | | Freighter | `v2.12.2` | | Laboratory | `v2.8.0` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | ### Breaking changes note This release includes a major overhaul of the value representation at the XDR level, which potentially breaks contracts that rely on the old type definitions, here is a summary of the changes: - Removed the object/value split at the XDR level - Expanded size of value tag from 8 to 128 cases - Removed types: `U63`, `Static`, `Bitset` - Added `U256`, `I256` and special case u64 subtypes `Timepoint` and `Duration` - Added `String` which is a `Bytes`-like object but displays as text - Enhanced `Symbol` to have a maximum size of 32 bytes (no more 10-char limit) SDK support has also been added/updated for these types. See [xdr](https://github.com/stellar/stellar-xdr/pull/70), [env](https://github.com/stellar/rs-soroban-env/pull/682) and [sdk](https://github.com/stellar/rs-soroban-sdk/pull/879) changes for more details. ### Changelog #### XDR - Make types wrapping Strings no longer aliases - Value representation overhaul - Diagnostic events See https://github.com/stellar/rs-stellar-xdr/releases v0.0.15 for more details. #### Soroban Environment - Remove the unnecessary error events that appeared in non-error scenarios. - Reform metering for value cloning and memory allocation. - Provide more test coverage for Auth Next. - Check issuer clawback flag when new contract balance is created. - Add a test that covers out-of-order require_auth calls. - Token name in events. - Adapt Storage and SnapshotSource to use Rc only. - Add host functions to convert addresses to/from account/contract ids. - Value and object representation overhaul. - Move InvokerType from common env to internal host implementation. - fix thinko in vec_new_from_linear_memory. - Improve safety of custom account contracts. - Split out DeclaredSizeForMetering from MeteredClone and use it for Compare. - Structured debug events. - Fix an i128 conversion bug. - Use new bulk memory ops for tuples. - Host budget metering, cost model, calibration changes. - Increase memory budget to 50MB. See https://github.com/stellar/rs-soroban-env/releases v0.0.15 for more details. #### Soroban Rust SDK - Add docs in specs for types. - Rename Serialize/Deserialize to To/FromXdr. - Use the updated source_account() interface. - Update storage docs. - Expose reset budget functions to testutils. - Adapt storage and snapshot interface to env changes. - Add utils for converting from/to contract/account ids. - Value and object representation overhaul. - Add a getter for address of a generated contract client. - Use String for log_fmt_values. - Add a utility to call \_\_check_auth in tests. - Add more support for the new types in soroban-spec. - Use vector bulk-init in Vec. - Bump env and update for event changes. - fix(macro): add new XDR types to parser. - Update budget test. - bump env and update budget print. See https://github.com/stellar/rs-soroban-sdk/releases v0.7.0 for more details. #### Soroban RPC - Set a maximum ledger latency in /health method. - Add resultMetaXdr and envelopeXdr back to getTransaction() response. - Support for new `diagnostic` contract events. - soroban-rpc: Add filtering support for diagnostic events. - soroban-rpc: Ingest diagnostic events. - Remove 'soroban serve' subcommand. - Limit preflight-computation concurrency through a worker pool. - Miscellaneous fixes. - General improvements to compilation and testing. - Add contract events to simulateTransaction's response. See https://github.com/stellar/soroban-tools/releases v0.7.0 for more details. #### Soroban CLI - Improved documentation, and `--help` text - feat: auto-generate comprehensive CLI docs. - When using `invoke`, contract function names now come after the `--`, like `soroban invoke --id -- hello --to "world"` - Improvements to identity management - fix: allow using ENV to add identity and pass secret. - Replaced `--identity`, `--secret-key`, and `--account` flags. Replace with single `--source-account`, which understands identities, and secret keys. - feat: add better docs for UDT and add examples for each arg. - Added `soroban --version` - Better errors, replacing `Unknown` - Refactor, so that the cli can be imported via the new `soroban-test` crate. This makes testing and using soroban-cli as a library easier. - feat: reorg into a proper library. - feat: add new soroban-test crate. - Update to Clap v4. - fix(CLI): string/u/i256 XDR parsing. - Support for new `diagnostic` type events See https://github.com/stellar/soroban-tools/releases v0.7.0 for more details. ## Preview 7 (February 16th, 2023) ### Software | Software | Version | | --- | --- | | XDR | [df18148747e807618acf4639db41c4fd6f0be9fc](https://github.com/stellar/stellar-xdr/commit/df18148747e807618acf4639db41c4fd6f0be9fc) | | Soroban Environment | `v0.0.14` | | Soroban Interface Version | `29` | | Stellar Core | `19.7.1-1204.871accefc.focal~soroban` | | Soroban Rust SDK | `v0.6.0` | | Soroban CLI | `v0.6.0` | | Soroban RPC | `0.6.1-13` | | Stellar Horizon | `stellar-horizon:2.24.61~soroban-335` | | Stellar Friendbot | `soroban-v0.0.2-alpha` | | Stellar Quickstart | `stellar/quickstart:soroban-dev@sha256:81c23da078c90d0ba220f8fc93414d0ea44608adc616988930529c58df278739` | | Stellar JS Stellar Base | `8.2.2-soroban.11` | | Stellar JS Soroban Client | `v0.4.0` | | Freighter | `v2.10.0` | | Laboratory | `v2.7.0` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | ### Breaking changes note This release comes with a revamp of authorization approach that is breaking for most of the contracts that did any sort of auth logic or used tokens. [example](../build/smart-contracts/example-contracts/auth.mdx) and [authorization overview](../learn/fundamentals/contract-development/authorization.mdx) for more details. ### Changelog #### XDR - Update Rust XDR for Auth Next. See https://github.com/stellar/rs-stellar-xdr/releases v0.0.13 and v0.0.14 for more details. #### Soroban Environment - Allow for a custom budget outside tests - Restructure Event to be cheap to clone and allow it to be rolled back - Add auth_required support for non-account balances - Reform TryFromVal to reduce number of impls - Fix override of panic hook during cross-contract testing - Add EnvBase::Error, remove CheckedEnv, make Env methods return Error - Initial Auth Next implementation in Soroban Host - Add 13-tuple conversions - Do not include Status as the first arg of a DebugEvent - Add a simpler version of require_auth - Use auth specific errors - Emulate classic account authentication in recording auth mode. - Change the interface for the auth testing utility. See https://github.com/stellar/rs-soroban-env/releases v0.0.13 and v0.0.14 for more details. #### Soroban Rust SDK - Support variants with multiple fields in UDTs - Error on UDT enums with 0-element tuple variants - Allow xlm balance updates - Fix vec insert - Adapt SDK to changes to `{Try,From,Into}Val` in env crates - Fix vec pop_front - Use better storage error in tests - Adapt to introduction of Env::Error - Improve compiler errors for UDTs - Allow same named types and functions - Make soroban_token_spec::spec_xdr easier to keep updated - Remove build-optimized makefile target - Auth Next changes in SDK - Add docs to contract spec entries - SDK support for simplified require_auth See https://github.com/stellar/rs-soroban-sdk/releases v0.5.0 and v0.6.0 for more details. #### Soroban RPC - Configure default limit, update cursor / startLedger validation, and include latest ledger for getEvents - Add support for AuthNext - Fix rollback error in logs - Add getNetwork command - Implement event storage - Implement ledger entry storage - Refactor db and ingestion packages, add ingestion of LedgerCloseMeta - Implement simulateTransaction using rust instead of preflight - Simplify topic matching for events search See https://github.com/stellar/soroban-tools/releases v0.5.0 v0.6.0 for more details. #### Soroban CLI - Add option for running contract with unlimited budget - Add support for AuthNext - Add config command - Add getNetwork support - Reorganize CLI commands See https://github.com/stellar/soroban-tools/releases v0.5.0 v0.6.0 for more details. ## Preview 6 (January 9th, 2023) ### Software | Software | Version | | --- | --- | | XDR | [026c9cd074bdb28ddde8ee52f2a4502d9e518a09](https://github.com/stellar/stellar-xdr/tree/026c9cd074bdb28ddde8ee52f2a4502d9e518a09) | | Soroban Environment | `v0.0.12` | | Soroban Interface Version | `27` | | Stellar Core | `19.6.1-1158.c0ad35aa1.focal~soroban` | | Soroban Rust SDK | `v0.4.2` | | Soroban CLI | `v0.5.0` | | Soroban RPC | `0.4.0-10` | | Stellar Horizon | `stellar-horizon:2.22.0~soroban-323` | | Stellar Friendbot | `soroban-v0.0.2-alpha` | | Stellar Quickstart (amd64) | `stellar/quickstart:soroban-dev@sha256:c4429def497ed78ca99ae40c8e2522ec932081b4428df992900b5bc8d53bd642` | | Stellar Quickstart (arm64) | `stellar/quickstart:soroban-dev@sha256:37205510329845f5fe533bb7c4c182d8f35b3a3515f0a6729889067663e1ec97` | | Stellar JS Stellar Base | `8.0.1-soroban.6` | | Stellar JS Soroban Client | `v0.3.0` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | ### Changelog #### Soroban Environment - Wasm instruction level calibration - Replace `im` containers with `Vec` - Remove EnvVal - Fix first/last_index_of functions to use deep object comparison - Export type aliases for Storage and Footprint - Add env.json - Remove built-in soroban token - Update rust-version - Use single balances in the Stellar Asset Contract - Remove unnecessary i128 clone See https://github.com/stellar/rs-soroban-env/releases v0.0.12 for more details. #### Soroban Rust SDK - Fix contractimpl for empty impl blocks - bump env and fix token interface - Remove init from the token interface - Update rust-version - Require only a borrow of Host when updating ledger snapshot - Fix doc comments and clippy warnings on ledger snapshot - Add LedgerSnapshot::update(Host) - undo token deploy revert - Revert deploy and update env - Remove token deploy in anticipation of removal of the soroban only built-in token - Make ledger snapshot write file create dir path - Make errors explicit in ledger snapshot functions - Update env to include delete-im, remove-EnvVal changes See https://github.com/stellar/rs-soroban-sdk/releases v0.4.0, v0.4.1, v0.4.2 for more details. #### Soroban RPC - Add GitHub linting for GO code See https://github.com/stellar/soroban-tools/releases v0.4.0 for more details. #### Soroban CLI - Update rust version - StrValError --> Error and implemented using thiserror - Use soroban-ledger-snapshot for managing ledger.json - Use LedgerSnapshot::update to update snapshot instead of unpacking the host and updating ledger info and entries separately. - Add events subcommand for local and remote event viewing - Deprecate token create command See https://github.com/stellar/soroban-tools/releases v0.4.0 for more details. ## Preview 5 (December 8th, 2022) ### Software | Software | Version | | --- | --- | | XDR | [026c9cd074bdb28ddde8ee52f2a4502d9e518a09](https://github.com/stellar/stellar-xdr/tree/026c9cd074bdb28ddde8ee52f2a4502d9e518a09) | | Soroban Environment | `v0.0.11` | | Soroban Interface Version | `26` | | Stellar Core | `stellar-core_19.5.1-1137.b3a6bc281.focal~soroban` | | Soroban Rust SDK | `v0.3.2` | | Soroban CLI | `v0.3.3` | | Soroban RPC | `0.3.1-32` | | Stellar Horizon | `stellar-horizon:2.22.0~soroban-318` | | Stellar Friendbot | `soroban-v0.0.2-alpha` | | Stellar Quickstart | `stellar/quickstart:soroban-dev@sha256:8046391718f8e58b2b88b9c379abda3587bb874689fa09b2ed4871a764ebda27` | | Stellar JS Stellar Base | `8.0.1-soroban.5` | | Stellar JS Soroban Client | `v0.2.0` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | ### Changelog #### XDR - Remove BigInt from ScVal - Add u128 and i128 to ScVal - Change the structure of events in meta - Change transaction operation structure for Soroban contract deployments and invocations See https://github.com/stellar/stellar-xdr/compare/48d5e17ae63bba0aa9725cd9d18d7438f44c07b1...026c9cd074bdb28ddde8ee52f2a4502d9e518a09 for more details. #### Soroban Environment - Upgrade crate-git-revision to 0.0.4 (contribution by [@brson]) - Add Host::with_artificial_test_contract_frame - Restructure benchmark framework, add calibration code for all CostTypes - Disable budget costs for object cmp - Env changes to decouple contract instance from source - Remove BigInt, switch everything to u128 and i128 See https://github.com/stellar/rs-soroban-env/releases v0.0.10, v0.0.11 for more details. #### Soroban Rust SDK - Rename data to storage by @leighmcculloch in #786 - Add ability to get current Budget from env in tests by @leighmcculloch in #789 - Add Env::as_contract for testutils by @leighmcculloch in #761 - Update contract deployment to match the Env changes by @dmkozh in #766 - Make contract_id public in contract clients. by @dmkozh in #768 - Remove BigInt by @sisuresh in #770 - Add soroban-ledger-snapshot - Change gen JSON output from stream to array (contribution by [@vinamogit]) - Contributions from [@vinamogit] See https://github.com/stellar/rs-soroban-sdk/releases v0.3.0, v0.3.1, v0.3.2 for more details. #### Soroban RPC - Add soroban-rpc version subcommand - Add a new getLedgerEntry jsonrpc method, deprecating and replacing getContractData allowing an application to fetch any ledger entry - Added new getEvents method currently backed by horizon See https://github.com/stellar/soroban-tools/releases v0.3.0, v0.3.1 for more details. #### Soroban CLI - Fix apt-get install in publish workflow - Added type description to errors when using --arg (contribution by [@waldmatias]) - Additional CLI support for the contract deployment changes - Adds support for `soroban contract deploy --wasm-hash`, as well as `soroban contract install --wasm` - Add xdr and env version to version subcommand output - Fix that the footpoint was not set correctly when deploying the wrapped token contract (contribution by [@overcat]) - Contributions from [@waldmatias], [@willemneal], [@overcat], [@brson] See https://github.com/stellar/soroban-tools/releases v0.3.0, v0.3.1, 0.3.3 for more details. ## Preview 4 (November 15th, 2022) ### Software | Software | Version | | --- | --- | | XDR | https://github.com/stellar/stellar-xdr-next/tree/48d5e17ae63bba0aa9725cd9d18d7438f44c07b1 | | Soroban Environment | `v0.0.9` | | Soroban Interface Version | `23` | | Stellar Core | `19.5.1-1111.eba1d3de9.focal~soroban` | | Soroban Rust SDK | `v0.2.1` | | Soroban CLI | `v0.2.1` | | Soroban RPC | `0.3.1-32` | | Stellar Horizon | `2.22.0~soroban-304` | | Stellar Quickstart | `stellar/quickstart:soroban-dev@sha256:0993d3350148af6ffeab5dc8f0b835236b28dade6dcae77ff8a09317162f768d` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | ### Changelog #### XDR - Trivial whitespace changes #### Soroban Environment - Vm tuning - Add token events - Catch panics from native contracts in try_call - Improved built-in token error reporting - Add missing conversion from Status->ScStatus for the ContractError variant - Capture user panic-strings in native builds, avoid spurious NoContractRunning error - Few small fixes to error debug events See https://github.com/stellar/rs-soroban-env/releases v0.0.7, v0.0.8, v0.0.9 for more details. #### Soroban Rust SDK - Add Logger::print in testutils - Add conversion from Address to Identifier - Remove deprecated functions - Remove panic-catching and fix tests that use newly-working native try_call - Reintroduce an optimized aborting unwrap - Add assert_with_error! macro - Rename panic_error! to panic_with_error! See https://github.com/stellar/rs-soroban-sdk/releases v0.2.0, v0.2.1 for more details. #### Soroban RPC - Initial Release #### Soroban CLI - Strings and symbols are rendered as text in JSON output - Bytes are rendered as hex in JSON output - Accounts in invocations are created in sandbox - Add optimize sub-command that optimizes contracts - Fix the bin name in the completion command - Fix jsonrpc compliance issue See https://github.com/stellar/soroban-cli/releases v0.2.0, v0.2.1 for more details. ## Preview 3 (October 11th, 2022) ### Software | Software | Version | | --- | --- | | XDR | https://github.com/stellar/stellar-xdr-next/tree/161e2e5b64425a49f9ccfef7f732ae742ed5eec4 | | Soroban Environment | `v0.0.6` | | Soroban Interface Version | `23` | | Stellar Core | `19.4.1-1097.4e813f20e.focal~soroban` | | Soroban Rust SDK | `v0.1.1` | | Soroban CLI | `v0.1.2` | | Soroban RPC | ??? | | Stellar Horizon | `2.22.0~soroban-304` | | Stellar Quickstart | `stellar/quickstart:soroban-dev@sha256:e58d83f92a61f43406087f488dd1cba110a92646dca85f14b3a416163609e853` | | Futurenet Network Passphrase | `Test SDF Future Network ; October 2022` | ### Changelog See https://stellar.org/blog/soroban-a-new-smart-contract-standard. ## Preview 2 (September 13th, 2022) See https://stellar.org/blog/developers/soroban-preview-release-2. ## Preview 1 (August 1st, 2022) See https://stellar.org/blog/project-jump-cannon-soroban-preview-release. [@waldmatias]: https://github.com/waldmatias [@willemneal]: https://github.com/willemneal [@overcat]: https://github.com/overcat [@brson]: https://github.com/brson [@vinamogit]: https://github.com/vinamogit --- ## SDF Platforms SDF has open-sourced some "platforms" that make it easier to accomplish certain things on the network. ## Anchor Platform The Anchor Platform is a set of tools and APIs that enable developers and businesses to build their own on and off-ramp services for the Stellar network. It provides a standardized interface, including the implementation of several Stellar Ecosystem Proposals (SEPs), to make it easy for businesses to integrate with Stellar-based wallets and exchanges. [Learn more about the Anchor Platform API here!](./anchor-platform/README.mdx) ## Stellar Disbursement Platform The Stellar Disbursement Platform (SDP) enables organizations to disburse bulk payments to recipients using Stellar. [Learn more about the Stellar Disbursement Platform API here!](./stellar-disbursement-platform/README.mdx) --- ## The Anchor Platform: Build and Manage On/Off-Ramps on the Stellar Network # Anchor Platform The Anchor Platform provides a set of tools and APIs for building on and off-ramp services on the Stellar network. With standardized interfaces and full implementations of key Stellar Ecosystem Proposals (SEPs), it simplifies integration with Stellar-based wallets and exchanges, enabling you to focus on your core business logic rather than protocol implementation details. ## Supported SEPs The Anchor Platform implements the following Stellar Ecosystem Proposals: - **[SEP-1](sep-guide/sep1/README.mdx)** — Stellar.toml file serving for service discovery - **[SEP-6](sep-guide/sep6/README.mdx)** — Deposit and withdrawal operations - **[SEP-10](sep-guide/sep10/README.mdx)** — Web authentication using challenge/response transactions - **SEP-12** — Customer KYC/AML data management - **[SEP-24](sep-guide/sep24/README.mdx)** — Interactive deposit and withdrawal flows - **[SEP-31](sep-guide/sep31/integration.mdx)** — Cross-border payment processing (receive only) - **SEP-38** — Price quotes and exchange rate services - **[SEP-45](sep-guide/sep45/README.mdx)** — Web authentication using challenge/responses for contract accounts ## Key Features - **Complete SEP implementations** — Full support for deposit, withdrawal, and payment processing workflows - **Authentication & authorization** — SEP-10 and SEP-45 support for both traditional and smart contract accounts - **Customer management** — SEP-12 integration for KYC/AML compliance and customer data handling - **Transaction processing** — Comprehensive transaction lifecycle management with status tracking and webhook callbacks - **Quote & exchange services** — SEP-38 integration for price discovery and exchange rate calculations - **Multi-asset support** — Flexible configuration for multiple assets with various deposit and withdrawal methods - **Smart contract support** — Native support for Stellar contract accounts (C-accounts) via SEP-45 ## Documentation Links - **[Architecture](./admin-guide/architecture.mdx)** — System architecture and component overview - **[Getting Started](./admin-guide/getting-started.mdx)** — Initial setup and deployment instructions - **[Event Handling](./admin-guide/events/README.mdx)** — Event delivery, webhooks, and integration patterns - **[SEP Guides](./sep-guide/README.mdx)** — Implementation guides for Stellar Ecosystem Proposals - **[API Reference](./api-reference/README.mdx)** — Complete API documentation and reference ## Additional Resources The documentation for the Anchor Platform is a work in progress. Developers are welcome to dive into the code and existing documentation on the [GitHub repository](https://github.com/stellar/java-stellar-anchor-sdk). [sep-1]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md [sep-6]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md [sep-10]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md [sep-12]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md [sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md [sep-31]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md [sep-38]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md [sep-45]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0045.md [anchor-platform-github]: https://github.com/stellar/java-stellar-anchor-sdk --- ## Admin Guide(3) All you need to know about setting up, running, and using the Anchor Platform. --- ## Architecture(Admin-guide) Before starting with the Anchor Platform, let's get familiar with the architecture. This section will describe the components involved and how they interact. ### Fundamental Architecture The following architectural components are required for all deployments of the Anchor Platform. ![fundamental anchor platform architecture](/assets/ap/anchor-platform-architecture-1.png) #### Client The client is an application, such as a wallet or remittance sender, that acts on behalf of a user and makes requests to the system. Clients make requests to the SEP server component of the Anchor Platform using sets of standards called [SEPs][seps] (Stellar Ecosystem Proposals). #### SEP Server The SEP server is a client-facing server and therefore needs to be accessible from an external network. The SEP server processes user requests and manages the state of transactions they initiate. When the SEP server needs to provide information it doesn't have to the client, such as the exchange rate for an asset pair or the KYC status of a customer, it makes synchronous [callback][callback-api] requests to the business server and returns the information in a SEP-compliant format. :::note The SEP server will never store any sensitive information, such as KYC (PII), in the database. ::: #### Business Server The business server is a service that you (the business) must implement to connect the Anchor Platform with your internal systems. The business server responds to callback requests sent by the SEP server, such as requests for a quote, receives events sent by event service, such as notification of a received payment to your Stellar account, and provides updates to the platform server when off-chain events occur, such as the initiation of a bank transfer to a customer. #### Platform Server The platform server is an internal component. It should be hosted in a private network and should not be accessible from the Internet. This server enables the business to fetch and update the state of transactions using its [API][platform-api]. #### Database The Anchor Platform uses a PostgreSQL database to store Stellar events and entities. It is primary used to store transactions. #### Kafka Kafka is used as the messaging backbone for the Anchor Platform, facilitating communication and event-driven interactions between different components. It allows components like the SEP server, business server, and event service to publish and subscribe to transaction and payment events in a reliable and scalable manner. ### Complete Architecture In addition to the components described above, the Anchor Platform includes several other components that offer additional functionality. Your business can chose to which of the additional components to use, but the diagram below visualizes the architecture of the system if all components are utilized. [![complete anchor platform architecture](/assets/ap/anchor-platform-architecture-2.png)](/assets/ap/anchor-platform-architecture-2.png) #### Event Service The event service enables the Anchor Platform to send HTTP webhooks to registered clients and your business server when the state of transactions change, removing the need for clients and/or your business server to poll the Anchor Platform's APIs. It works by reading events from published to a Kafka topic by the other Anchor Platform components. [Read more][events] about using the event service. #### Payment Observer The Payment Observer monitors the Stellar blockchain using Stellar RPC or Horizon, automatically detects payments related to the business, and updates the corresponding transactions in the Anchor Platform's database. If you also use the [events] service, payments to your accounts will trigger a HTTP callback made to your business server. If you already have a solution for monitor payments to your Stellar accounts, such as an integration with your exchange, Horizon, or RPC, then this component is not required, although your business server will need to notify the Anchor Platform when a payment associated with an Anchor Platform transaction was made to your one of your Stellar accounts via the [Platform API][platform-api]. [seps]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/README.md [platform-api]: ../api-reference/platform/transactions/README.mdx [callback-api]: ../api-reference/callbacks/README.mdx [events]: ./events/README.mdx --- ## Assets and Wallet Clients This guide covers how to configure assets and wallet clients in the Anchor Platform. ## Assets Configuration Assets define the tokens and currencies that your Anchor Platform supports for deposits and withdrawals. In the "Getting Started" guide, assets are configured in the `config/assets.yaml` file by the `ap_start.sh`. For the complete list of fields and defaults, see the [asset configuration reference](https://github.com/stellar/anchor-platform/blob/develop/core/src/main/resources/config/anchor-asset-default-values.yaml). ### Example Asset Configuration ```yaml items: - id: stellar:native distribution_account: "G...DIST" significant_decimals: 7 sep6: enabled: true deposit: enabled: true min_amount: 0 max_amount: 10 methods: - SEPA - SWIFT withdraw: enabled: true min_amount: 0 max_amount: 10 methods: - bank_account - cash sep24: enabled: true sep31: enabled: true sep38: enabled: true exchangeable_assets: - iso4217:USD ``` ### Field Explanations - **`id`** (Required) - The asset identifier in the format `SCHEMA:CODE:(ISSUER)` or `stellar:native` representing XLM, the native Stellar asset. For example, `stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5` represents Circle USD. For fiat currencies, use `iso4217:CODE` (e.g., `iso4217:USD`, `iso4217:CAD`). - **`distribution_account`** - The Stellar account address that holds the asset for distribution. Not needed for fiat assets (`iso4217`). - **`significant_decimals`** - The number of decimal places the asset supports. - **`sep6`** - Enables SEP-6 and configures deposit/withdraw limits and methods per asset. - **`sep24`** - Enables SEP-24 interactive flows and their per-asset settings. - **`sep31`** - Enables SEP-31 cross-border payment support for the asset. - **`sep38`** - Enables SEP-38 quotes and `exchangeable_assets` for supported currency pairs. ## Wallet Clients Configuration Wallet clients are the wallet apps that connect to your Anchor Platform to perform transactions on behalf of users. For the full list of client fields and defaults, see the [client configuration reference](https://github.com/stellar/anchor-platform/blob/main/platform/src/main/resources/config/anchor-client-default-values.yaml). ### Example Client Configuration ```yaml items: - name: "referenceCustodial" type: custodial signing_keys: - GDJLB...KLTG callback_urls: sep6: https://client.example.com/callbacks/sep6 sep24: https://client.example.com/callbacks/sep24 sep31: https://client.example.com/callbacks/sep31 sep12: https://client.example.com/callbacks/sep12 - name: "reference" type: noncustodial domains: - wallet-server:8092 - client.example.com callback_urls: ... ``` ### Field Explanations **Custodial Client Configuration:** - **`name`** (Required) - A unique identifier for the client. - **`type: custodial`** (Required) - The type of the client. Must be set to `custodial` for custodial clients. - **`signing_keys`** (Required) - A list of Stellar public keys used for client SEP-10 authentication. The anchor uses these keys to verify that requests are coming from the authorized client. - **`callback_urls`** (Optional) - URLs to which the service can send callbacks for different SEP types. - **`sep6`** - Callback URL for SEP-6 (deposit/withdrawal) transaction status updates - **`sep24`** - Callback URL for SEP-24 (interactive deposit/withdrawal) transaction status updates - **`sep31`** - Callback URL for SEP-31 (cross-border payments) transaction status updates - **`sep12`** - Callback URL for SEP-12 (KYC) customer information updates **Noncustodial Client Configuration:** - **`name`** (Required) - A unique identifier for the client, similar to custodial clients. Typically a name that clearly represents the client entity. - **`type: noncustodial`** (Required) - The type of the client. Must be set to `noncustodial` for noncustodial clients. Noncustodial clients allow users to control their own private keys, and the wallet acts as an interface to user-controlled accounts. - **`domains`** (Required) - A list of domains associated with the client, used to verify the client's identity. - **`callback_urls`** (Optional) - URLs to which the service can send callbacks for different SEP types. Works the same way as for custodial clients (see above). --- ## dev.env Using the Payment Observer allows you to delegate this step to the Anchor Platform. To enable the Payment Observer, use the `--stellar-observer` flag in the command section of the [compose file](../../getting-started.mdx#configuration). The Payment Observer will track all transactions sent to the distribution account. When the transaction with the expected memo is detected in the network, the status will automatically change to `pending_anchor` and event will be the emitted (if Kafka is used). In order to update the transaction's statuses, the observer makes corresponding JSON-RPC requests to the platform. It should use the following URL. ```bash # dev.env PLATFORM_API_BASE_URL=http://platform-server:8085 ``` :::caution The Payment Observer won't validate the amounts. It's your responsibility to verify that the amount sent by the user is correct. ::: :::info If you already have a system that monitors payments, make sure that the logic of the system matches the description below: First, wait for the transaction to be included in the ledger (using an SDK). This transaction must have the expected memo and destination address (distribution account). Once this transaction has been detected and verified, notify the user that the funds have been received using the [notify_onchain_funds_received](#funds-received-1) JSON-RPC request. ::: --- ## Error | Error code | Meaning | | :--------- | :------------------------------------------- | | -32600 | The JSON sent is not a valid Request object | | -32601 | The method does not exist / is not available | | -32602 | Invalid method parameter(s) | | -32603 | Internal JSON-RPC error | :::tip We will also reference a `$transaction_id` variable. This is an identification of transaction that is being returned from the Anchor Platform on an withdrawal or deposit start request. You can obtain the transaction ID by connecting the test wallet to your local Anchor Platform instance. ::: --- ## Request The Request object must contain the following attributes: - ATTRIBUTE - DATA TYPE - DESCRIPTION - jsonrpc - string - A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0" - method - string - A String containing the name of the method to be invoked. List of available methods you can see in [JSON-RPC Methods][json-rpc-methods] - params - object - A Structured value that holds the parameter values, corresponding to method call, to be used during the invocation of the method - id - string - An identifier established by the client. The Server will reply with the same value in the Response object :::tip It's possible to provide multiple updates in a single JSON-RPC request (by placing multiple JSON-RPC request objects). When an update is done in this way, all updates will be done sequentially. Most importantly, each JSON-RPC request is not atomic. If one update fails, all previous updates WILL be applied and all subsequent updates WILL be processed and applied as well. ::: --- ## Response The Response is expressed as a single JSON Object, with the following attributes: - ATTRIBUTE - DATA TYPE - DESCRIPTION - jsonrpc - string - A String specifying the version of the JSON-RPC protocol. It's set to "2.0" - result - object - A Structured value that holds the updated transaction details - id - string - An identifier sent by the client - error - object - A Structured value that holds the error details - id - string - Unique id of the transaction for which an error occurred - code - number - A number that indicates the error type that occurred. Please see a list of [error codes](#error-codes) below - message - string - A String providing a short description of the error - data - string - A primitive or structured value that contains additional information about the error --- ## call-json-rpc.sh Before making JSON-RPC requests, let's first create a template for making a request to the Anchor Platform. ```bash # call-json-rpc.sh #!/usr/bin/env bash curl localhost:8085 \ -X POST \ -H 'Content-Type: application/json' \ --data "@$1" ``` This small script will make a JSON-RPC request to the Anchor Platform hosted on the default port (8085). JSON transaction data stored in the provided file will be used as body (requests must be an array). --- ## dev.env(Security) To enable API key authentication, modify your `dev.env` file: ```bash # dev.env PLATFORM_API_AUTH_TYPE=api_key # Will be used as API key SECRET_PLATFORM_API_AUTH_SECRET="your API key that business server will use" ``` Once enabled, all requests must include a valid `X-Api-Key` header, set to the configured API key. --- ## dev.env(3) To enable JWT authentication, modify your `dev.env` file: ```bash # dev.env PLATFORM_API_AUTH_TYPE=jwt # Will be used to sign the JWT token SECRET_PLATFORM_API_AUTH_SECRET="your secret that business server will use" ``` Anchor Platform uses the HMAC SHA-256 (HS256) algorithm to sign JWT tokens. Ensure that `SECRET_PLATFORM_API_AUTH_SECRET` is at least 32 characters long for security. Once enabled, all requests must include a valid `Authorization` header with the format `Bearer `. --- ## Security :::caution By default, the Platform API's endpoints such as `GET /transactions` and `GET /transactions/:id` are not protected, and are accessible by anyone who has access to the server, including wallet applications. ::: :::info It's recommended to keep Platform server accessible only from the private network. However, you may want to add additional layer of protection via securing the API. ::: --- ## Event Handling Receive transaction updates through HTTP webhook events. --- ## Delivery Guarantees Depending on the messaging system you use, there will be different delivery guarantees. the event service uses Kafka as the messaging system, so the delivery guarantees will depend on the producer configuration and the broker configuration that you use. Depending on the number of partitions configured for the `TRANSACTION` topic, the events may be delivered out of order. :::caution Any transaction logic that depends on the order should use the transaction `status` and the `updated_at` fields to determine the order of the events. ::: Next subsections will describe the delivery guarantees from the client and the business server perspective. ### Client Delivery Guarantees For each client, the event service will attempt to deliver each event up to three times with an exponential backoff. If the event is not delivered after three attempts due to HTTP 4xx or 5xx errors, the event will be skipped. If the client is not reachable after three attempts, the event service will no longer attempt to deliver any events to that client. ### Business Server Delivery Guarantees The event service will attempt to deliver each event to the businesss server up to three times with an exponential backoff. If the event is not delivered after three attempts due to HTTP 4xx or 5xx errors, the event will be skipped. If the business server is not reachable after three attempts, the event service will no longer attempt to deliver any events to the business server. :::note The business server delivery guarantees are the same as the client delivery guarantees. In the future, the event service will skip the events that are not delivered to clients that are not reachable. ::: --- ## Getting Started(Events) Anchor Platform provides an event service that sends HTTP webhook notifications to: **Business Servers** - Transaction status changes - Quote updates - Customer KYC status changes Event schemas for business servers are defined in the [API reference](../../api-reference/callbacks/post-event.api.mdx). **Client Applications** - Transaction status changes affecting their users - Customer KYC status changes affecting their users _Event schemas for client applications are defined in their respective SEPs:_ - [SEP-6 Transaction Events](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md#single-historical-transaction) - [SEP-12 Customer Events](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md#response) - [SEP-24 Transaction Events](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#single-historical-transaction) - [SEP-31 Transaction Events](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md#get-transaction) This eliminates the need for business servers and client applications to continuously poll the APIs for updates. --- ## Integration This guide will walk you through integrating with the event service to start receiving events. The event service currently only supports Apache Kafka as the backend message broker. It assumes familiarity with Kafka and will not cover how to set up a Kafka cluster. ## Requirements Anchor Platform will send events to the `TRANSACTION` Kafka topic. The event service will consume events from this topic and send them to the appropriate endpoints. ## Configuration First, the event service's Kafka producer need to be configured using the `event.queue` section of the configuration file or setting the environment variables. The following is the set of required environment variables needed to configure the event service's Kafka producer: ```bash # dev.env EVENTS_ENABLED=true EVENTS_QUEUE_TYPE=kafka EVENTS_QUEUE_KAFKA_BOOTSTRAP_SERVER=localhost:9092 ``` ```yaml # dev.services.yaml events: enabled: true queue: type: kafka kafka: bootstrap_server: localhost:9092 ``` Anchor Platform allows a subset of the Kafka producer's client configuration to be set. See the [default values file][default-values-file] for more information what is available. For more information on the Kafka producer's client configuration, see the [Kafka documentation](https://kafka.apache.org/documentation/#producerconfigs). Next, the event processor needs to be configured in the `event_processor` section of the Anchor Platform Configuration file or setting the environment variables. ```bash # dev.env EVENT_PROCESSOR_CLIENT_STATUS_CALLBACK_ENABLED=true EVENT_PROCESSOR_CALLBACK_API_REQUEST_ENABLED=true ``` ```yaml # dev.services.yaml event_processor: client_status_callback: enabled: true callback_api_request: enabled: true ``` This will enable the event processor to start processing events from `TRANSACTION` topic. In this example, the event processor will send events to client and business server callback endpoints. ## Receiving Events The event service can be used to send events to client and business server callback endpoints. The event service will send events to these endpoints as HTTP POST requests with the event data in the request body. ### As a Client Application Client applications can receive updates about their users' transactions and customer information. The schema of the event data will depend on the type of event being sent. To receive events as a client application, you will need to expose callback URLs that the event service can send events to. The event service will send a POST request to this endpoint with the event data in the request body. The schema of the event data will depend on the type of event being sent. Anchor Platform allows unique endpoints to be configured by event type. Anchor Platform will only send events to clients listed in the client configuration. See the [client configuration documentation][clients-config] for more information. #### Callback Signing Anchor Platform signs the callback requests it sends to client applications. The signature is included in the `Signature` header of the request. The callback URL signature specification can be found in the corresponding SEP protocol specifications. - [SEP-6](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md#url-callback-signature) - [SEP-12](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md#callback-post-request) - [SEP-24](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#url-callback-signature) - [SEP-31](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md#url-callback-signature) ### As a Business Server In addition to SEP transaction status updates, business servers can receive events about SEP-31 quote creation or SEP-12 customer information updates. The schema of the event data will depend on the type of event being sent. Visit the [Event API documentation](../../api-reference/callbacks/post-event.api.mdx) for more information about the schema of the event data. To receive events as a business server, you will need to expose a callback URL that the event service can send events to. The event service will send a POST request to this endpoint with the event data in the request body. #### Configuration The event service's callback API can be configured using the `callback_api` section of the Anchor Platform configuration file or setting the environment variables. The following is an example of how to configure the event service's callback API with JWT authentication: ```bash # dev.env # note `/callback` will not be used for event callbacks # instead events will be sent to `http://localhost:8081/event` # all other callbacks (rates, customer, etc.) will use the provided `/callback` root path CALLBACK_API_BASE_URL=http://localhost:8081/callback CALLBACK_API_AUTH_TYPE=jwt CALLBACK_API_AUTH_JWT_EXPIRATION_MILLISECONDS=30000 CALLBACK_API_AUTH_JWT_HTTP_HEADER=Authorization SECRET_CALLBACK_API_AUTH_SECRET="a secret for signing jwts" ``` ```yaml # dev.services.yaml callback_api: base_url: http://localhost:8081/callback auth: type: jwt jwt: expiration_milliseconds: 30000 http_header: Authorization ``` The following is an example of how to configure the event service's callback API with API key authentication: ```bash # dev.env CALLBACK_API_BASE_URL=http://localhost:8081/callback CALLBACK_API_AUTH_TYPE=api_key CALLBACK_API_AUTH_API_KEY_HTTP_HEADER=X-Api-Key SECRET_CALLBACK_API_AUTH_SECRET="your API key" ``` ```yaml # dev.services.yaml callback_api: base_url: http://localhost:8081/callback auth: type: api_key api_key: http_header: X-Api-Key ``` This configures the event service's callback API that will be used to send events to client and business server callback endpoints. The following are the supported configuration options: - `base_url`: The base URL of the business server's callback endpoint. - `secret`: The secret to be used when sending events to the business server's callback endpoint. This is used to sign the request body when JWT authentication is enabled and it is the API key when API key authentication is enabled. - `auth`: The authentication method to be used when sending events to the business server's callback endpoint. The following are the supported authentication methods: - `JWT`: The event service will send a JSON Web Token (JWT) in the `Authorization` header of the request. The following are the supported configuration options: - `expiration_milliseconds`: The expiration time of the JWT in milliseconds. - `http_header`: The header in which the JWT will be sent. - `API_KEY`: The event service will send an API key in the `Authorization` header of the request. The following are the supported configuration options: - `http_header`: The header in which the API key will be sent. [default-values-file]: https://github.com/stellar/java-stellar-anchor-sdk/blob/develop/platform/src/main/resources/config/anchor-config-default-values.yaml [clients-config]: ../../sep-guide/sep10/README.mdx#config-with-client-attribution --- ## Getting Started(Admin-guide) This guide will help you quickly get the Anchor Platform (TESTNET only) with a business reference running locally using Docker Compose. Please note that this is not meant to be run in production. ## Prerequisites - [Docker](https://www.docker.com/get-started) and Docker Compose installed - [Stellar CLI](https://github.com/stellar/stellar-cli) installed ## Quick Start ### 1. Clone the Anchor Platform repository ```bash git clone https://github.com/stellar/anchor-platform ``` ### 2. Navigate to the quick-run directory ```bash cd anchor-platform/quick-run ``` ### 3. Start all services ```bash ./ap_start.sh ``` ### 4. Verify the platform is running Wait a few moments for services to initialize, then verify the platform is responding: ```bash curl http://localhost:8080/.well-known/stellar.toml ``` You should see the Stellar TOML configuration file returned. ### 5. Check service status ```bash docker-compose ps ``` All services should show as "Up" in the status column. ## What's Included The `quick-run` setup includes: - **docker-compose.yaml** - Complete service definitions with all dependencies - **dev.env** - Pre-configured environment variables - **config/** - Required configuration files: - `assets.yaml` - Asset definitions - `clients.yaml` - Client configurations - `reference-config.yaml` - Reference server settings - `stellar.localhost.toml` - SEP-1 TOML file :::tip For more information about configuring assets and client wallets, see the [Assets and Client Wallets](./assets-and-client-wallets.mdx) guide. ::: ## Testing with Stellar Demo Wallet You can test the Anchor Platform using the [Stellar Demo Wallet](https://demo-wallet.stellar.org): 1. Open the [Stellar Demo Wallet](https://demo-wallet.stellar.org) in your browser. 2. Click on **"Generate keypair for new account (testnet only)"** button. 3. Click on the **"Create Account"** button next to the PUBLIC key. 4. You should now see `XLM` available under the **Balances** section, indicating your account is loaded. 5. Click on **"Add home domain"** and enter the following URL: ``` http://localhost:8080 ``` This connects the demo wallet to your local Anchor Platform instance running on port 8080. 6. You should now be able to perform SEP transactions (deposits, withdrawals, etc.) with your local Anchor Platform instance. ## Customizing Configuration To modify settings: 1. Edit `dev.env` for environment variables 2. Edit files in `config/` for service-specific configurations 3. Restart services: `docker-compose restart` ## Stopping Services To stop all services: ```bash docker-compose down ``` ## How to implement your business callback server Once you have the Anchor Platform running, you can replace the reference server with your own business callback server implementation. This allows you to implement your own business logic for handling deposits, withdrawals, and other anchor operations. ### 1. Shutdown the reference business server After the Anchor Platform is started and running, stop the reference server service: ```bash docker-compose stop reference-server ``` This stops the reference server while keeping the Anchor Platform and other services running. The platform will continue to operate, but it will no longer receive callbacks from the reference server. ### 2. Implement and run your own reference server Implement your own callback server that implements the Anchor Platform callback API. You can use the [kotlin-reference-server](https://github.com/stellar/anchor-platform/tree/develop/kotlin-reference-server) as a reference implementation to understand the required endpoints and data structures. Ensure your callback server is accessible at `http://localhost:8091` (or the configured endpoint) and that the Anchor Platform can reach it. Your server should: - Listen on port **8091** (or configure the platform to use a different port) - Implement the required callback endpoints as specified in the Anchor Platform API documentation - Handle business logic for: - Fee calculation - Exchange rate determination - Transaction status updates - Customer information (KYC) management - Deposit and withdrawal processing ### 3. Test your integration using the demo wallet Follow the steps described in the [Testing with Stellar Demo Wallet](#testing-with-stellar-demo-wallet) section above, and verify that your server correctly handles deposit, withdrawal, and other SEP operations initiated from the wallet. --- ## API Reference(3) View all Anchor Platform API information. --- ## Callbacks Server The Anchor Platform provides several callback functionalities for your business server. | | | | ------ | --------------------------------------- | | GET | [/customer](./get-customer.api.mdx) | | PUT | [/customer](./put-customer.api.mdx) | | DELETE | [/customer/:id](./del-customer.api.mdx) | | POST | [/event](./post-event.api.mdx) | | GET | [/rate](./get-rates.api.mdx) | --- ## Delete Customer Data The request for this endpoint is identical to the [`DELETE /customer`](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md#customer-delete) request defined in SEP-12. Delete the customer's data or queue the customers data for deletion. Request --- ## Retrieve Customer's Info The request and response for this endpoint is identical to the [`GET /customer`](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md#customer-get) request and response defined in SEP-12. This endpoint allows clients to: 1. Fetch the fields the server requires in order to register a new customer via a SEP-12 [`PUT /customer`](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md#customer-put) request If the server does not have a customer registered for the parameters sent in the request, it should return the fields required in the response. The same response should be returned when no parameters are sent. 2. Check the status of a customer that may already be registered This allows clients to check whether the customers information was accepted, rejected, or still needs more info. If the server still needs more info, or the server needs updated information, it should return the fields required. Request --- ## Retrieve Rates Transactions that involve two non-equivalent on & off-chain assets (such as USDC on Stellar and fiat EUR) must use exchange rates that are communicated to the client application requesting the transaction. When clients make requests to the Platform for these exchange rates, the Platform sends this request to the anchor to fetch it. Rates can be [indicative](https://www.investopedia.com/terms/i/indicativequote.asp) or [firm](https://www.investopedia.com/terms/f/firmquote.asp). The anchor must provide an ID and expiration if the client requests a firm rate. Anchors can provide discounted rates specific client applications. The Platform includes the `client_id` parameter for this reason. Either `sell_amount` or `buy_amount` will be included in requests as parameters, but never both. In the same way, either `sell_delivery_method` and `buy_delivery_method` may be included in requests, but never both, since either `sell_asset` or `buy_asset` is a Stellar asset. Upon receiving the response, the Anchor Platform will validate the amount and price of the response. If the validation fails, the Platform will respond to the client application's request with a HTTP status code of `502 Bad Gateway`. The `sell_amount`, `buy_amount`, `price`, and `fee` are validated as follows: - if `rate.fee` exists, - `rate.fee.asset` must have a positive value of `significant_decimals` defined in the asset configuration. - `rate.fee.total` must equal to the sum of `rate.fee.details.amount`. - if the `rate.fee.asset == rate.sell_asset`, `sell_amount ~= price * buy_amount + fee` must hold true. - if the `rate.fee.asset == rate.buy_asset`, `sell_amount ~= price * (buy_amount + fee)` must hold true. - if `rate.fee` does not exist, `sell_amount ~= price * buy_amount` must hold true. The `~=` is defined as equality within rounding error. The rounding error is defined as `10^(-significant_decimals)` Request --- ## Receive an Event Receive a JSON object representing an event. Request --- ## Create or Update Customer Info **The Anchor Platform does not persist any customer KYC data.** The request and response for this endpoint are identical to the [`PUT /customer`](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md#customer-put) request and response defined in SEP-12. Client applications make requests with the following request body, which is forwarded to the anchor. Anchors must validate and persist the data passed, and return the customer's `id`. Requests containing only string fields will be forwarded to the anchor as with the `application/json` content type. Requests containing binary fields will be forwarded to the anchor as with the `multipart/form-data` content type. Request --- ## Platform Server Data on the Anchor Platform is available through two different APIs: A REST API and a JSON-RPC API. Each of these APIs has associated documentation here. | | | | ------------------------------------- | --- | | [REST API](./transactions/README.mdx) | | | [JSON-RPC API](./rpc/README.mdx) | | --- ## JSON-RPC API Interact with your Anchor Platform instance through the use of lightweight, easy-to-use RPC requests. --- ## JSON-RPC Methods This section lists the Anchor Platform JSON-RPC API methods that should be called by Stellar clients to update status of the transaction. The OpenRPC Specification for JSON-RPC API is available [here](https://playground.open-rpc.org/?schemaUrl=https://raw.githubusercontent.com/stellar/stellar-docs/main/static/assets/rpc-methods/open-rpc.json). Postman collection is available [here](https://documenter.getpostman.com/view/9257637/2s9Y5U1kra) | | | | --- | --- | | | [do_stellar_payment](./do_stellar_payment.mdx) | | | [do_stellar_refund](./do_stellar_refund.mdx) | | | [get_transaction](./get_transaction.mdx) | | | [get_transactions](./get_transactions.mdx) | | | [notify_amounts_updated](./notify_amounts_updated.mdx) | | | [notify_customer_info_updated](./notify_customer_info_updated.mdx) | | | [notify_interactive_flow_completed](./notify_interactive_flow_completed.mdx) | | | [notify_offchain_funds_available](./notify_offchain_funds_available.mdx) | | | [notify_offchain_funds_pending](./notify_offchain_funds_pending.mdx) | | | [notify_offchain_funds_received](./notify_offchain_funds_received.mdx) | | | [notify_offchain_funds_sent](./notify_offchain_funds_sent.mdx) | | | [notify_onchain_funds_received](./notify_onchain_funds_received.mdx) | | | [notify_onchain_funds_sent](./notify_onchain_funds_sent.mdx) | | | [notify_refund_pending](./notify_refund_pending.mdx) | | | [notify_refund_sent](./notify_refund_sent.mdx) | | | [notify_transaction_error](./notify_transaction_error.mdx) | | | [notify_transaction_expired](./notify_transaction_expired.mdx) | | | [notify_transaction_on_hold](./notify_transaction_on_hold.mdx) | | | [notify_transaction_recovery](./notify_transaction_recovery.mdx) | | | [notify_trust_set](./notify_trust_set.mdx) | | | [request_offchain_funds](./request_offchain_funds.mdx) | | | [request_onchain_funds](./request_onchain_funds.mdx) | | | [request_trust](./request_trust.mdx) | --- ## do_stellar_payment meth.name === "do_stellar_payment")[0] } /> --- ## do_stellar_refund meth.name === "do_stellar_refund")[0] } /> --- ## get_transaction meth.name === "get_transaction")[0]} /> --- ## get_transactions meth.name === "get_transactions")[0]} /> --- ## notify_amounts_updated meth.name === "notify_amounts_updated")[0] } /> --- ## notify_customer_info_updated meth.name === "notify_customer_info_updated", )[0] } /> --- ## notify_interactive_flow_completed meth.name === "notify_interactive_flow_completed", )[0] } /> --- ## notify_offchain_funds_available meth.name === "notify_offchain_funds_available", )[0] } /> --- ## notify_offchain_funds_pending meth.name === "notify_offchain_funds_pending", )[0] } /> --- ## notify_offchain_funds_received meth.name === "notify_offchain_funds_received", )[0] } /> --- ## notify_offchain_funds_sent meth.name === "notify_offchain_funds_sent", )[0] } /> --- ## notify_onchain_funds_received meth.name === "notify_onchain_funds_received", )[0] } /> --- ## notify_onchain_funds_sent meth.name === "notify_onchain_funds_sent", )[0] } /> --- ## notify_refund_pending meth.name === "notify_refund_pending")[0] } /> --- ## notify_refund_sent meth.name === "notify_refund_sent")[0] } /> --- ## notify_transaction_error meth.name === "notify_transaction_error", )[0] } /> --- ## notify_transaction_expired meth.name === "notify_transaction_expired", )[0] } /> --- ## notify_transaction_on_hold meth.name === "notify_transaction_on_hold", )[0] } /> --- ## notify_transaction_recovery meth.name === "notify_transaction_recovery", )[0] } /> --- ## notify_trust_set meth.name === "notify_trust_set")[0]} /> --- ## request_offchain_funds meth.name === "request_offchain_funds")[0] } /> --- ## request_onchain_funds meth.name === "request_onchain_funds")[0] } /> --- ## request_trust meth.name === "request_trust")[0]} /> --- ## Overview(Rpc) JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol. It's simple and easy to use, as it uses a single HTTP endpoint and a JSON object that contains the method name and parameters. It is transport agnostic in that the concepts can be used within the same process, over sockets, over http, or in many various message passing environments. It uses [JSON](http://www.json.org) ([RFC 4627](http://www.ietf.org/rfc/rfc4627.txt)) as data format. :::note All member names exchanged between the Client and the Server that are considered for matching of any kind should be considered to be case-sensitive. ::: You can read more about JSON-RPC protocol [here](https://www.jsonrpc.org/specification). --- ## Transactions(3) Transactions are representations of a SEP transaction. It holds information about the protocol being used, and all necessary information passed by an external party (such as wallet or an anchor). Should not be confused with stellar [transactions](../../../../../learn/glossary.mdx#transaction). | | | | --- | -------------------------------------------- | | GET | [/transactions/:id](get-transaction.api.mdx) | | GET | [/transactions/](./get-transactions.api.mdx) | --- ## Retrieve a Transaction(Transactions) Provides the information necessary for the business to determine the state of the transaction identified by `id`, decide if any action must be taken to continue processing the transaction, and act on the decision. Request --- ## Retrieve a List of Transactions Allows to query list of transactions for desired SEP. This api supports pagination, and it's possible (and recommended) to make multiple requests to query transactions. The last page is reached when the number of elements returned by the endpoint is smaller than provided `page_size`. Request --- ## SEP Guides Guides for implementing Stellar Ecosystem Proposals (SEPs) with the Anchor Platform. --- ## Stellar Info File (SEP-1) ## Overview SEP-1 (Stellar Info File) allows wallets and other Stellar applications to discover information about your anchor service. By hosting a `stellar.toml` file at `/.well-known/stellar.toml`, you enable applications to automatically find: - Your organization's information - Supported assets and currencies - Authentication endpoints (SEP-10) - SEP endpoints for SEP-6, SEP-24, SEP-31, SEP-38, SEP-45 For details, please refer to the [SEP-1 specification][sep-1]. ## Creating Your stellar.toml File Create a `stellar.toml` file with your service information. Here's a minimal example to get started: ```toml # dev.stellar.toml ACCOUNTS = ["GD...G"] # Your distribution account public keys SIGNING_KEY = "GD...G" # Your signing key (public key) for SEP-10 authentication NETWORK_PASSPHRASE = "Test SDF Network ; September 2015" # Use "Public Global Stellar Network ; September 2015" for mainnet [DOCUMENTATION] ORG_NAME = "Your organization" ORG_URL = "https://your-website.com" ORG_DESCRIPTION = "A description of your organization" ``` :::tip For a complete list of all available `stellar.toml` attributes, see the [SEP-1 specification][sep-1]. You'll need to add additional sections like `[[CURRENCIES]]`, `TRANSFER_SERVER`, `TRANSFER_SERVER_SEP0024`, `WEB_AUTH_ENDPOINT`, `WEB_AUTH_FOR_CONTRACTS_ENDPOINT`, `DIRECT_PAYMENT_SERVER` etc., as you configure the supported assets and other SEPs. ::: :::important **Production vs. Development**: You'll need separate `stellar.toml` files for testnet and mainnet: - **Testnet**: Use `NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"` - **Mainnet**: Use `NETWORK_PASSPHRASE = "Public Global Stellar Network ; September 2015"` Make sure your production file includes your actual Mainnet distribution accounts, signing keys, and production service URLs. ::: ## Configuration To enable SEP-1, you need to configure how the Anchor Platform should access your `stellar.toml` file. The platform supports three methods: | Type | Use Case | Description | | --- | --- | --- | | `file` | **Recommended for most cases** | Read from a local file on the server | | `string` | Quick testing or simple configs | Provide the TOML content directly in the config | | `url` | External hosting | Fetch from a remote URL (useful for dynamic content) | ### Environment Variables Configure SEP-1 using the following environment variables: - `SEP1_ENABLED`: Set to `true` to enable SEP-1 - `SEP1_TOML_TYPE`: One of `file`, `string`, or `url` - `SEP1_TOML_VALUE`: The value depends on the type (see examples below) ### Method 1: File (Recommended) Best for: Production deployments where you manage the file on disk. ```bash # dev.env SEP1_ENABLED=true SEP1_TOML_TYPE=file SEP1_TOML_VALUE=/path/to/your/stellar.toml ``` :::tip When using Docker, mount your `stellar.toml` file as a volume and reference the path inside the container. For example: ```yaml # docker-compose.yaml volumes: - ./config/stellar.toml:/config/stellar.toml:ro ``` Then set `SEP1_TOML_VALUE=/config/stellar.toml` in your environment. ::: ### Method 2: String Best for: Quick testing, development, or when managing config via environment variables. ```bash # dev.env SEP1_ENABLED=true SEP1_TOML_TYPE=string SEP1_TOML_VALUE="ACCOUNTS = [\"GD...G\"] SIGNING_KEY = \"GD...G\" NETWORK_PASSPHRASE = \"Test SDF Network ; September 2015\" [DOCUMENTATION] ORG_NAME = \"Your organization\" ORG_URL = \"https://your-website.com\"" ``` :::caution When using the `string` type, ensure your TOML content is properly escaped for your environment file format. For complex configurations, the `file` type is easier to manage. ::: ### Method 3: URL Best for: Dynamic content or when hosting the file externally. ```bash # dev.env SEP1_ENABLED=true SEP1_TOML_TYPE=url SEP1_TOML_VALUE=https://example.com/stellar.toml ``` :::note When using the `url` type, the Anchor Platform will fetch the file on each request. Make sure the URL is accessible from your Anchor Platform server and returns valid TOML content. ::: ## Accessing Your stellar.toml File Once configured and enabled, the Anchor Platform automatically serves your `stellar.toml` file at the standard SEP-1 endpoint: - **`/.well-known/stellar.toml`** - The primary endpoint (served with `Content-Type: text/plain`) - **`/`** - Redirects to `/.well-known/stellar.toml` when SEP-1 is enabled ### Testing Your Configuration After starting the Anchor Platform, verify your configuration: ```bash # Test the endpoint curl http://localhost:8080/.well-known/stellar.toml # Or test the redirect curl -L http://localhost:8080/ ``` You should see your `stellar.toml` content returned as plain text. ### Alternative: External Hosting You can also host your `stellar.toml` file using a static file server like [nginx] or a CDN. If you choose this approach: 1. Host the file at `https://your-domain.com/.well-known/stellar.toml` 2. Ensure it's publicly accessible 3. Make sure your `stellar.toml` file includes the correct URLs pointing to your Anchor Platform endpoints The Anchor Platform's SEP-1 service is optional if you're hosting the file externally, but it provides a convenient way to manage everything in one place. [sep-1]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md [sep-9]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md [sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md [sep-38]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md [sep24-get-info]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#info [anchor-platform-image]: https://hub.docker.com/r/stellar/anchor-platform [docker-compose]: https://docs.docker.com/compose/ [minikube]: https://minikube.sigs.k8s.io/docs/ [kubernetes]: https://kubernetes.io/ [nginx]: https://www.nginx.com/ [ap-default-values]: https://github.com/stellar/java-stellar-anchor-sdk/blob/develop/platform/src/main/resources/config/anchor-config-default-values.yaml [stellar-demo-wallet]: https://demo-wallet.stellar.org [stellar-lab]: https://lab.stellar.org/ [postgresql]: https://www.postgresql.org/ [aurora-postgresql]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.AuroraPostgreSQL.html [h2]: https://www.h2database.com/html/main.html [sqlite]: https://www.sqlite.org/index.html [flyway]: https://documentation.red-gate.com/fd/redgate-flyway-documentation-138346877.html [sep-24-ref-ui]: https://github.com/stellar/sep24-reference-ui [sep-24-ref]: https://github.com/stellar/java-stellar-anchor-sdk/tree/develop/kotlin-reference-server --- ## Stellar Authentication (SEP-10) ## Overview SEP-10 (Stellar Web Authentication) enables wallet applications to create authenticated sessions with Stellar anchors by proving control over a Stellar account. Once authenticated, wallets receive a JSON Web Token (JWT) that they use in subsequent requests to the anchor's standardized services. For the complete specification, see [SEP-10: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md). The Anchor Platform implements SEP-10 with support for: - **Challenge/Response Flow**: GET `/auth` to request a challenge, POST `/auth` to validate and receive a JWT - **Client Attribution**: Optional verification of client application identity for noncustodial wallets - **Custodial Wallet Support**: Support for custodial wallets that manage user accounts - **Multiple Home Domains**: Support for multiple domains and wildcard patterns ## Typical Authentication Flow 1. The **Client** requests a unique challenge from the **Server** 2. The **Client** verifies and signs the challenge 3. The **Client** submits the signed challenge to the **Server** 4. The **Server** verifies the challenge and responds with a JWT session token ## Enable SEP-10 To enable SEP-10, set the following environment variables in your `dev.env` file. ```bash # dev.env SEP10_ENABLED=true SEP10_HOME_DOMAINS=localhost:8080 SECRET_SEP10_SIGNING_SEED="a Stellar private key" SECRET_SEP10_JWT_SECRET="a secret encryption key" ``` ### Required Configuration (If Enabled) | Variable | Default | Description | | --- | --- | --- | | `SEP10_ENABLED` | `false` | Set to `true` to enable SEP-10 authentication | | `SEP10_HOME_DOMAINS` | `localhost:8080` | List of home domains (comma-separated). Supports wildcard patterns like `*.stellar.org`. The `home_domain` must match the host where your `stellar.toml` file is served. | | `SECRET_SEP10_SIGNING_SEED` | _Required_ | The private key corresponding to the `SIGNING_KEY` in your `stellar.toml` file. Used to sign authentication challenges. Wallets verify this signature before signing and returning the challenge. | | `SECRET_SEP10_JWT_SECRET` | _Required_ | The encryption key used to sign and verify JWT tokens issued to authenticated wallets. | :::important The `SIGNING_KEY` in your `stellar.toml` file must be the public key derived from `SECRET_SEP10_SIGNING_SEED`. Wallets will verify that challenge transactions are signed by this key. ::: ### Optional Configuration ```bash # dev.env # Optional: Specify web_auth_domain (default: first home_domain if only one is specified) SEP10_WEB_AUTH_DOMAIN=localhost:8080 # Optional: Challenge transaction timeout in seconds (default: 900) SEP10_AUTH_TIMEOUT=900 # Optional: JWT token timeout in seconds (default: 86400 = 24 hours) SEP10_JWT_TIMEOUT=86400 # Optional: Require Authorization header in GET /auth requests (default: false) SEP10_REQUIRE_AUTH_HEADER=false # Optional: Client attribution requirement (default: false) SEP10_CLIENT_ATTRIBUTION_REQUIRED=false # Optional: Client allow list (default: empty, all configured clients allowed) # Comma-separated list of client names that are allowed to authenticate SEP10_CLIENT_ALLOW_LIST=client1,client2 ``` | Variable | Default | Description | | --- | --- | --- | | `SEP10_WEB_AUTH_DOMAIN` | First `home_domain` if only one is specified, otherwise empty | The `web_auth_domain` property used in SEP-10 responses. Required if you have multiple `home_domains` or use wildcard patterns. Must match the host of the SEP server. | | `SEP10_AUTH_TIMEOUT` | `900` | Time in seconds that a challenge transaction remains valid. Clients must sign and submit the challenge within this window. | | `SEP10_JWT_TIMEOUT` | `86400` | Time in seconds that an issued JWT token remains valid. After expiration, clients must re-authenticate. | | `SEP10_REQUIRE_AUTH_HEADER` | `false` | If `true`, requires a valid Authorization header (Bearer JWT) in GET `/auth` challenge requests. This prevents unauthorized access to the endpoint and is useful for re-authentication flows where clients need to refresh their JWT tokens. | | `SEP10_CLIENT_ATTRIBUTION_REQUIRED` | `false` | If `true`, noncustodial wallets must provide a `client_domain` in challenge requests. Requires client configuration (see below). | | `SEP10_CLIENT_ALLOW_LIST` | Empty (all configured clients allowed) | Comma-separated list of client names that are allowed to authenticate. Only relevant when `SEP10_CLIENT_ATTRIBUTION_REQUIRED=true`. If empty, all configured clients are allowed. | :::tip **Multiple Home Domains**: If you specify multiple `home_domains` (e.g., `ap.stellar.org,*.sdp.stellar.org`), you must also set `SEP10_WEB_AUTH_DOMAIN` to specify which domain hosts the authentication endpoint. ::: ## Client Attribution Client attribution allows you to restrict authentication to specific wallet applications and verify their identity. This is an optional feature that should only be enabled if it's a business requirement. :::info By default, the Anchor Platform allows anyone with a Stellar account to authenticate. Client attribution is only needed if you want to: - Restrict authentication to specific wallet applications - Verify the identity of noncustodial wallet applications - Track which wallet applications your users are using ::: ### Enabling Client Attribution ```bash # dev.env SEP10_CLIENT_ATTRIBUTION_REQUIRED=true ``` When `SEP10_CLIENT_ATTRIBUTION_REQUIRED=true`, noncustodial wallets must: 1. Provide a `client_domain` parameter in the challenge request 2. Sign the challenge transaction with the `SIGNING_KEY` from that domain's `stellar.toml` file 3. Have their domain listed in your client configuration ### Client Configuration Configure allowed clients in your YAML configuration file: ```yaml clients: # Each item in the list may contain the following fields: # - name: (required) the name of the client # - type: (required) `custodial` or `noncustodial` # # If the type is `custodial`, # - signing_keys: (required) the custodial SEP-10 signing key(s) of the client. # - callback_urls.sep6: (optional) the URL of the client's SEP-6 callback API endpoint. # - callback_urls.sep24: (optional) the URL of the client's SEP-24 callback API endpoint. # - callback_urls.sep31: (optional) the URL of the client's SEP-31 callback API endpoint. # - callback_urls.sep12: (optional) the URL of the client's SEP-12 callback API endpoint. # - allow_any_destination: (optional) default to false. If set to true, allows any destination for deposits. # - destination_accounts: (optional) list of accounts allowed to be used for the deposit. # If allow_any_destination is set to true, this configuration option is ignored. # # If the type is `noncustodial`, # - domains: (required) the domains of the client. # - callback_urls.sep6: (optional) the URL of the client's SEP-6 callback API endpoint. # - callback_urls.sep24: (optional) the URL of the client's SEP-24 callback API endpoint. # - callback_urls.sep31: (optional) the URL of the client's SEP-31 callback API endpoint. # - callback_urls.sep12: (optional) the URL of the client's SEP-12 callback API endpoint. # custodial client - name: bluecorp type: custodial signing_keys: "the signing key 1 of bluecorp","the signing key 2 of bluecorp" callback_urls: sep6: https://callback.bluecorp.com/api/v1/anchor/callback/sep6 sep12: https://callback.bluecorp.com/api/v1/anchor/callback/sep12 allow_any_destination: false destination_accounts: GA... # noncustodial client - name: pinkcorp type: noncustodial domains: pinkcorp.com callback_urls: sep6: https://callback.pinkcorp.com/api/v2/anchor/callback/sep6 sep12: https://callback.pinkcorp.com/api/v2/anchor/callback/sep12 - name: redcorp type: custodial signing_keys: "the signing key of redcorp", ``` Or configure via environment variables: ```bash # dev.env # custodial client CLIENTS[0]_NAME=bluecorp CLIENTS[0]_TYPE=custodial CLIENTS[0]_SIGNING_KEYS="the signing key 1 of bluecorp","the signing key 2 of bluecorp" CLIENTS[0]_ALLOW_ANY_DESTINATION=false CLIENTS[0]_DESTINATION_ACCOUNTS=GA... # noncustodial client CLIENTS[1]_NAME=pinkcorp CLIENTS[1]_TYPE=noncustodial CLIENTS[1]_DOMAINS=pinkcorp.com # custodial client CLIENTS[2]_NAME=redcorp CLIENTS[2]_TYPE=custodial CLIENTS[2]_SIGNING_KEYS="the signing key of redcorp" ``` ## Configure stellar.toml Update your `stellar.toml` file to advertise SEP-10 support. Wallets discover your authentication endpoint through this file. ```toml # dev.stellar.toml SIGNING_KEY = "add your signing key here (public key from SECRET_SEP10_SIGNING_SEED)" WEB_AUTH_ENDPOINT = "http://localhost:8080/auth" ``` These fields should match the configuration options set in the [Enable SEP-10](#enable-sep-10) section above. `WEB_AUTH_ENDPOINT` - The URL where the authentication service is running. This is the URL that clients will use to authenticate with the anchor. The endpoint must support: - `GET ` - Request a challenge - `POST ` - Exchange signed challenge for session JWT `SIGNING_KEY` - The public key corresponding to the private key specified in `SECRET_SEP10_SIGNING_SEED`. This key is used to sign authentication challenges presented to wallet applications. :::important - **`SIGNING_KEY`**: Must be the public key derived from `SECRET_SEP10_SIGNING_SEED` - **`WEB_AUTH_ENDPOINT`**: Use `https://` in production. The path `/auth` is the standard SEP-10 endpoint. - **Host Matching**: The host in `WEB_AUTH_ENDPOINT` should match one of your `SEP10_HOME_DOMAINS` (or the `SEP10_WEB_AUTH_DOMAIN` if specified). ::: ## How to Test the Authentication Flow The SEP-10 authentication flow consists of two steps: 1. **GET `/auth`** - Request a challenge transaction - Parameters: `account` (required), `memo` (optional), `home_domain` (optional), `client_domain` (optional) - Returns: A challenge transaction in XDR format 2. **POST `/auth`** - Validate the signed challenge and receive a JWT - Body: `{ "transaction": "" }` - Returns: `{ "token": "" }` The JWT token should be included in subsequent API requests as a Bearer token in the `Authorization` header. :::tip **Testing Your Configuration**: You can test SEP-10 authentication using curl and Stellar CLI. For more information about Stellar CLI, see the [Stellar CLI documentation](../../../../tools/cli/README.mdx). ```bash # Verify if `stellar` command line is installed stellar --version # List your Stellar keys/identities stellar keys ls # Get your account ID (public key) and secret seed # Replace 'alice' with your identity name from `stellar keys ls` IDENTITY_NAME="alice" ACCOUNT_ID=$(stellar keys public-key "$IDENTITY_NAME") SECRET_SEED=$(stellar keys secret "$IDENTITY_NAME") # Step 1: Request challenge and save the transaction XDR CHALLENGE_RESPONSE=$(curl -s "http://localhost:8080/auth?account=$ACCOUNT_ID") CHALLENGE_XDR=$(echo "$CHALLENGE_RESPONSE" | jq -r '.transaction') # Step 2: Sign the challenge transaction using Stellar CLI # Note: The output includes info messages; the signed XDR is on the last line SIGNED_CHALLENGE_XDR=$(echo "$CHALLENGE_XDR" | stellar tx sign --sign-with-key "$SECRET_SEED" 2>&1 | tail -1) # Step 3: Submit the signed challenge and receive JWT token curl -X POST "http://localhost:8080/auth" \ -H "Content-Type: application/json" \ -d "{\"transaction\": \"$SIGNED_CHALLENGE_XDR\"}" ``` ::: [sep1-ap]: ../sep1/README.mdx [sep-10]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md --- ## Hosted Deposits and Withdrawals (SEP-24) SEP-24 allows for a means by which wallets and/or exchanges allow the user to directly interact with an on & off-ramp. --- ## Configuration ## Modify a Stellar Info File Next, let's modify `stellar.toml` file created [earlier][sep1-ap]. Wallets need to know that SEP-24 functionality is supported by your business, and they also need to know all currencies you support. ```toml # dev.stellar.toml ACCOUNTS = ["add your public keys for your distribution accounts here"] SIGNING_KEY = "add your signing key here" NETWORK_PASSPHRASE = "Test SDF Network ; September 2015" TRANSFER_SERVER_SEP0024 = "http://localhost:8080/sep24" WEB_AUTH_ENDPOINT = "http://localhost:8080/auth" WEB_AUTH_FOR_CONTRACTS_ENDPOINT = "http://localhost:8080/sep45/auth" WEB_AUTH_CONTRACT_ID = "Your web auth contract id" # Add support for USDC [[CURRENCIES]] code = "USDC" issuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" status = "test" is_asset_anchored = false desc = "USD Coin issued by Circle" # Optionally, add support for XLM [[CURRENCIES]] code = "native" status = "test" is_asset_anchored = false anchor_asset_type = "crypto" desc = "XLM, the native token of the Stellar network." [DOCUMENTATION] ORG_NAME = "Your organization" ORG_URL = "Your website" ORG_DESCRIPTION = "A description of your organization" ``` Note that you'll need to create another file for your production deployment that uses the public network's passphrase, your production service URLs, your Mainnet distribution accounts and signing key, as well as the Mainnet issuing accounts of the assets your service utilizes. ## Enable Hosted Deposits & Withdrawals Now you're ready to enable hosted deposits and withdrawals via the SEP-24 API. Specify the following in your `dev.assets.yaml` file, and change the values depending on your preferences. This example asset file will enable support for Circle's USDC and a fiat USD. ```yaml # dev.assets.yaml items: - id: stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 distribution_account: GBLSAHONJRODSFTLOV225NZR4LHICH63RIFQTQN37L5CRTR2IMQ5UEK7 significant_decimals: 2 sep24: enabled: true deposit: enabled: true min_amount: 0 max_amount: 10 methods: - SEPA - SWIFT withdraw: enabled: true min_amount: 0 max_amount: 10 methods: - bank_account - cash - id: iso4217:USD significant_decimals: 2 # Optional support for XLM - id: stellar:native distribution_account: GBLSAHONJRODSFTLOV225NZR4LHICH63RIFQTQN37L5CRTR2IMQ5UEK7 significant_decimals: 7 sep24: enabled: true deposit: enabled: true min_amount: 0 max_amount: 10 methods: - SEPA - SWIFT withdraw: enabled: true min_amount: 0 max_amount: 10 methods: - bank_account - cash ``` The information provided for the `assets` value closely maps to the information that will be exposed to the wallet application using the [`GET /info`][sep24-get-info] SEP-24 endpoint. The Anchor Platform also uses this information to validate requests made to your service. Add the following variables to your environment file. ```bash # dev.env // Required SEP24_ENABLED=true SEP24_INTERACTIVE_URL_BASE_URL=http://example.com SEP24_MORE_INFO_URL_BASE_URL=http://example.com SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET="your encryption key shared with your business server" SECRET_SEP24_MORE_INFO_URL_JWT_SECRET="your encryption key shared with your business server" // Optional SEP24_INITIAL_USER_DEADLINE_SECONDS=1209600 ``` `SEP24_INTERACTIVE_URL_BASE_URL` is the URL that the Anchor Platform will provide to wallet applications when they initiate transactions. Wallet applications will open this URL in a web view inside their app, handing over control of the user experience from the wallet to your business. This URL points to the web widget your business implements. It contains all business-defined logic. We'll dive further into this experience in subsequent sections. `SEP24_MORE_INFO_URL_BASE_URL` is the URL that the Anchor Platform will provide to wallet applications when they want to show information about a transaction initiated previously. This URL is most often used by wallets in their transaction history views, and your business can define what information to display about the transaction. `SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET` and `SECRET_SEP24_MORE_INFO_URL_JWT_SECRET` are encryption keys that the Anchor Platform will use to generate short-lived tokens it will add to the URLs provided to the wallet. Your business server must also have these keys in its environment so it can verify the token's signature. `SEP24_INITIAL_USER_DEADLINE_SECONDS` is an optional param that defines the time in seconds a user has to act before the transaction moves to the next status. It determines the `user_action_required_by` field, which indicates the deadline. Check [JSON-RPC Methods][json-rpc-methods] for usage examples. ## Test With the Demo Wallet Wallets should now be able to discover, authenticate, and initiate transactions with your service! Your project and source files should now look something like this. ``` ├── dev.env ├── docker-compose.yaml ├── config │ ├── dev.assets.yaml │ ├── dev.stellar.toml ``` Your environment should now look like the following. ```bash # dev.env ASSETS_TYPE=file ASSETS_VALUE=/home/dev.assets.yaml SEP1_ENABLED=true SEP1_TOML_TYPE=file SEP1_TOML_VALUE=/home/dev.stellar.toml SEP10_ENABLED=true SEP10_HOME_DOMAIN=localhost:8080 SECRET_SEP10_SIGNING_SEED="a Stellar private key" SECRET_SEP10_JWT_SECRET="a secret encryption key" SEP24_ENABLED=true SEP24_INTERACTIVE_URL_BASE_URL=http://localhost:8081 SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET="your encryption key shared with your business server" SECRET_SEP24_MORE_INFO_URL_JWT_SECRET="your encryption key shared with your business server" ``` To test this out, go to the [Stellar Demo Wallet][stellar-demo-wallet]. [![demo wallet connected to the anchor platform](/assets/ap/anchor-platform-sep24-demo-wallet.png)](/assets/ap/anchor-platform-sep24-demo-wallet.png) Initiate a transaction by doing the following: - Create a new keypair - Click the "Add Asset" button and enter - the code of the Stellar asset on your `stellar.toml` file - your home domain, `localhost:8080` - Select the dropdown and click "SEP-24 Deposit", then click "Start" The demo wallet should be able to find your `stellar.toml` file, authenticate using the Stellar keypair you just created, and initiate a transaction. However, when the demo wallet attempts to open the URL provided by the Anchor Platform, you'll get a not found page. [![demo wallet after initiating a transaction](/assets/ap/anchor-platform-sep24-demo-wallet-widget.png)](/assets/ap/anchor-platform-sep24-demo-wallet-widget.png) [sep-1]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md [sep-9]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md [sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md [sep-38]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md [sep24-get-info]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#info [anchor-platform-image]: https://hub.docker.com/r/stellar/anchor-platform [docker-compose]: https://docs.docker.com/compose/ [minikube]: https://minikube.sigs.k8s.io/docs/ [kubernetes]: https://kubernetes.io/ [nginx]: https://www.nginx.com/ [ap-default-values]: https://github.com/stellar/java-stellar-anchor-sdk/blob/develop/platform/src/main/resources/config/anchor-config-default-values.yaml [stellar-demo-wallet]: https://demo-wallet.stellar.org [stellar-lab]: https://lab.stellar.org/ [postgresql]: https://www.postgresql.org/ [aurora-postgresql]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.AuroraPostgreSQL.html [h2]: https://www.h2database.com/html/main.html [sqlite]: https://www.sqlite.org/index.html [flyway]: https://documentation.red-gate.com/fd/redgate-flyway-documentation-138346877.html [sep-24-ref-ui]: https://github.com/stellar/sep24-reference-ui [sep-24-ref]: https://github.com/stellar/java-stellar-anchor-sdk/tree/develop/kotlin-reference-server [sep1-ap]: ../sep1/README.mdx [json-rpc-methods]: ../../api-reference/platform/rpc/methods/README.mdx --- ## Example Integrating with the Anchor Platform involves three key areas: - Building a web-based user experience that can be opened in a mobile web view - Providing transaction status updates to the Anchor Platform - Fetching transaction status updates from the Anchor Platform ## Building a Web-Based User Experience The Anchor Platform does not offer a white-label UI that your business can utilize, and instead expects the business to build their own UI and backend system. We won't build an entire on & off-ramp user experience in this guide, but will cover the ways in which your existing product should be updated to be compatible with the Anchor Platform. ### Authentication If your business has an existing on & off-ramp product, you likely have an existing system for user authentication. However, because the Anchor Platform authenticates the user prior to providing the business's URL, requiring the user to go through another form of authentication is actually unnecessary. In this way, the Anchor Platform can be thought of as providing an alternative form of authentication. The business is free to continue requiring users to authenticate using their existing system, but the ideal user experience would skip this step and create an authenticated session for the user if they have already authenticated using their Stellar account. The Anchor Platform adds a JWT `token` query parameter to the business's URL given to the wallet application. This token is signed by the previously-configured `SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET` value, and includes the information you need to identify the user. The process should look something like this: 1. Pass the `token` added to the URL of your backend system 2. Verify the signature on the `token` and check its expiration 3. Create an authenticated session for the user identified by `token.sub` The decoded contents of the `token` will look something like this: ```json { "jti": "e26cf292-814f-4918-9b40-b4f76a300f98", "sub": "GB244654NC6YPEFU3AY7L25COGES445P3Q63W6Q76JHR3UBJMLT2XBOB:1234567", "exp": 1516239022, "data": { "first_name": "John", "last_name": "Doe", "email": "johndoe@example.com" } } ``` Note that the `sub` value identifies the user using a Stellar account and integer. This is what the value will be when custodial applications that use an omnibus account authenticate with your service. When non-custodial wallets authenticate, the token may look slightly different. ```json { "jti": "e26cf292-814f-4918-9b40-b4f76a300f98", "sub": "GB244654NC6YPEFU3AY7L25COGES445P3Q63W6Q76JHR3UBJMLT2XBOB", "exp": 1516239022, "data": { "client_domain": "api.vibrantapp.com", "first_name": "John", "last_name": "Doe", "email": "johndoe@example.com" } } ``` The `sub` value here only contains a public key to identify the user, and the `data.client_domain` field identifies the wallet application used to authenticate. In both cases, all information in the `data` object is optional, and will only be present if the wallet provides that information. Let's add a backend server to our compose file that will be used to verify the token and create authenticated web sessions for users initiating transactions. ```yaml # docker-compose.yaml --- business-server: build: . ports: - "8081:8081" env_file: - ./dev.env depends_on: - platform-server ``` Let's create a simple Docker container for our application. ```docker FROM node:19 WORKDIR /home COPY . . RUN npm install CMD ["node", "server.js"] ``` Now let's create a minimal NodeJS application. ```bash yarn init -y yarn add express jsonwebtoken touch server.js ``` Below is an example of a backend server authenticating a user using NodeJS. ```js # server.js const express = require("express"); const jwt = require("jsonwebtoken"); const app = express(); const port = process.env.BUSINESS_SERVER_PORT; app.use(express.json()); /* * We'll store user session data in memory, but production systems * should store this data somewhere more persistent. */ const sessions = {}; /* * Create an authenticated session for the user. * * Return a session token to be used in future requests as well as the * user data. Note that you may not have a user for the Stellar account * provided, in which case the user should go through your onboarding * process. */ app.post("/session", async (req, res) => { let decodedPlatformToken; try { decodedPlatformToken = validatePlatformToken(req.body.platformToken); } catch (err) { res.status = 400; res.send({ "error": err }); return; } let user = getUser(decodedPlatformToken.sub); let sessionToken = jwt.sign( { "jti": decodedPlatformToken.jti }, process.env.SESSION_JWT_SECRET ); sessions[sessionToken] = user; res.send({ "token": sessionToken, "user": user }); }); /* * Validate the signature and contents of the platform's token */ function validatePlatformToken(token) { if (!token) { throw "missing 'platformToken'"; } let decodedToken; try { decodedToken = jwt.verify(token, process.env.SECRET_SEP10_JWT_SECRET); } catch { throw "invalid 'platformToken'"; } if (!decodedToken.jti) { throw "invalid 'platformToken': missing 'jti'"; } return decodedToken; } /* * Query your own database for the user based on account:memo string parameter */ function getUser(sub) { return null; } app.listen(port, () => { console.log(`business server listening on port ${port}`); }); ``` Run this with the platform server and database and initiate a new transaction with the [demo wallet][stellar-demo-wallet]. Then, we'll send the token to our server. ```bash curl \ -X POST \ -H 'Content-Type: application/json' \ -d '{"platformToken": ""}' \ http://localhost:8081/session | jq ``` ## Providing Updates to the Platform Let's create an endpoint for our business server that accepts the information collected in our UI. ```js # server.js // Production systems should either let the Anchor Platform generate its own memos // or have your custodial service generate a memo for each transaction. const transactionMemos = {}; app.post("/transaction", async (req, res) => { let sessionToken; try { sessionToken = validateSessionToken(req.headers.get("authorization")); } catch (err) { res.status = 400; res.send({ "error": err }) return; } // assuming this is a withdrawal transaction, we'll provide a memo, which is // required by our third-party custodian to credit us the payment. When the // payment is made with this memo, we can match the on-chain payment with the // transaction in the Anchor Platform's database. transactionMemos[req.body.transaction.id] = parseInt(Math.random() * 100000); let rpcRequestBody = [ { "id": 1, "jsonrpc": "2.0", "method": "request_onchain_funds", "params": { "transaction_id": req.body.transaction.id,, "message": "waiting for the user to provide off-chain funds.", "amount_in": { "amount": req.body.amount_in.amount, "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, "amount_out": { "amount": req.body.amount_out.amount, "asset": "iso4217:USD" }, "fee_details": { "total": req.body.fee_details.total, "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, "destination_account": "GD...G", "memo": transactionMemos[req.body.transaction.id], "memo_type": "id" } } ]; let platformResponse; try { platformResponse = await updatePlatformTransaction(rpcRequestBody); } catch (err) { res.status = 500; res.send({ "error": err }) return; } res.send({ "transaction": platformResponse.records[0] }); }); function validateSessionToken(authorizationHeader) { let parts = authorizationHeader.split(" "); if (parts.length != 2 || parts[0] != "Bearer") { throw "invalid authorization header format"; } let sessionToken = parts[1]; try { jwt.verify(sessionToken, process.env.SESSION_JWT_SECRET); } catch { throw "invalid session token"; } if (!sessions[sessionToken]) { throw "expired session"; } return sessionToken; } async function updatePlatformTransaction(requestBody) { let response = await fetch( `${process.env.PLATFORM_SERVER}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody) } ); if (response.status != 200) { throw `unexpected status code: ${response.status}`; } return await response.json(); } ``` This will update the Anchor Platform's database with the information provided and enable wallet applications to fetch this updated information so it can relay it back to the user. You should have already informed the user of the transaction's amounts and that your business's is waiting for the on-chain payment to arrive, but providing these updates allows users to view their transactions' statuses through their mobile application without opening the business' UI again. :::note At this time, the Anchor Platform does not send notifications to the wallet application when transaction statuses change, however, it is on our roadmap to add these notifications or "callback requests" so that wallet applications do not have to poll the Anchor Platform for updates. ::: ## Fetching Updates from the Platform If you only use the Anchor Platform to expose the SEP APIs to wallet applications, then you won't have a strong reason for fetching transaction status updates from the Anchor Platform, mostly because it won't update the transaction status until you make `JSON-RPC API` requests. However, if you use the Anchor Platform to monitor the Stellar network for incoming payments (associated with withdrawal transactions), the Anchor Platform will update transaction statuses when payments are received. There are two ways to fetch updates from the Anchor Platform, - Polling the Platform API's `GET /transactions/:id` endpoint for the transactions you're expecting a payment for - Streaming transaction status change events from a Kafka cluster While streaming transaction status changes from a Kafka cluster may be a more robust and scalable approach, we're going to use the polling method in this guide. Setting up and using a Kafka cluster will be the subject of a different section of the docs. First, let's configure the Anchor Platform to observe the Stellar network for incoming payments. ```yaml # docker-compose.yml --- stellar-observer: image: stellar/anchor-platform:latest command: --stellar-observer env_file: - ./dev.env volumes: - ./config:/home depends_on: - db ``` The `--stellar-observer` command starts a process that monitors the distribution accounts configured in your `config.yaml` file for withdrawal payments. If a payment is sent to one of these accounts and the memo attached to the transaction matches a `memo` value provided or generated by the Anchor Platform, the Anchor Platform will consider the transaction that memo is associated with as received and update the transaction's status to `pending_anchor`. It does this by making a `JSON-RPC API` request, so we need to configure the URL it should use. ```bash # dev.env PLATFORM_API_BASE_URL=http://platform-server:8085 ``` Let's make some additions to the `server.js` file so we can poll the Anchor Platform for our expected payments. ```js // server.js ... /* * Fetch the transaction data from the Platform API * * Production systems should have proper retry mechanisms. */ async function getPlatformTransaction(transactionId) { let response = await fetch(`${process.env.PLATFORM_SERVER}/transactions/${transactionId}`) if (response.status != 200) { throw `unexpected status code: ${response.status}`; } return await response.json(); } (async () => { while (true) { await new Promise(r => setTimeout(r, 2000)); let requestPromises; for (const transactionId in transactionMemos) { requestPromises.push(getPlatformTransaction(transactionId)) } let transactions = await new Promise.all(requestPromises); for (const transaction in transactions) { // assuming all requests were successful if (transaction.status == "pending_anchor") { // initiate off-chain delivery of funds console.log(`received payment for transaction ${transaction.id}`); } } } })() ``` ## Full Example Implementation Stellar provides an example business server implementation for SEP-24. It's split into two parts: 1) a web UI, accessible for the end user; and 2) a back-end implementation, used to get and push updates from/to the Anchor Platform. The code for web UI can be found [here][sep-24-ref-ui] The code for the backend is a part of the Anchor Platform, and is available as a [submodule][sep-24-ref]. [sep-1]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md [sep-9]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md [sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md [sep-38]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md [sep24-get-info]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#info [anchor-platform-image]: https://hub.docker.com/r/stellar/anchor-platform [docker-compose]: https://docs.docker.com/compose/ [minikube]: https://minikube.sigs.k8s.io/docs/ [kubernetes]: https://kubernetes.io/ [nginx]: https://www.nginx.com/ [ap-default-values]: https://github.com/stellar/java-stellar-anchor-sdk/blob/develop/platform/src/main/resources/config/anchor-config-default-values.yaml [stellar-demo-wallet]: https://demo-wallet.stellar.org [stellar-lab]: https://lab.stellar.org/ [postgresql]: https://www.postgresql.org/ [aurora-postgresql]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.AuroraPostgreSQL.html [h2]: https://www.h2database.com/html/main.html [sqlite]: https://www.sqlite.org/index.html [flyway]: https://documentation.red-gate.com/fd/redgate-flyway-documentation-138346877.html [sep-24-ref-ui]: https://github.com/stellar/sep24-reference-ui [sep-24-ref]: https://github.com/stellar/java-stellar-anchor-sdk/tree/develop/kotlin-reference-server --- ## FAQ ### How To Use JWTs? As part of the flow, once a user makes a request, i.e. an interactive withdrawal/deposit request, it will be processed by the Anchor Platform and forwarded to your service. The Anchor Platform will make a `GET` call to `?token=`. This JWT token will contain: 1. `exp` is the expiration time of the token. You should check that the provided token has not expired. 2. `sub` is the account associated with this transaction. It can be used to identify the user account. Note that this value may be different from the account that will be used to receive/send funds. 3. `jti` is the hash of the transaction. 4. `data` is the extra payload that has been set by the user. It will always contain the Stellar `asset` wants to deposit or withdraw. If provided by the client, it will also contain the `amount` the user wants to transact, the `client_domain` of the wallet verified during SEP-10 authentication, and `client_name` (defined as 'name' in [clients] configuration if provided), and the `lang` (language) preference of the user. ### How To Provide Fees? Currently, it's recommended to provide fees/exchange rates in the iFrame/web view of your application. [SEP-24] standard provides a `/fee` endpoint to allow businesses to set static fees for their transactions. However, it's not currently supported by the Anchor Platform. :::note /fee endpoint will be deprecated in the future. ::: ### How to identify the user account? You should use the `sub` field of the JWT token. For custodial wallets, this value will be in the format `account:memo`. Use the memo to identity the user. For noncustodial wallets, simply use the `sub` value itself, which will be equal to the user account. ### How to identify the wallet? Utilize the `data.client_domain` attributes within the JWT token. In the presence of [clients] configuration, the JWT token will additionally incorporate the `data.client_name` field, enabling wallet identification. [sep-9]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md [sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md [clients]: ../sep10/README.mdx --- ## Getting Started(Sep24) This guide will walk you through configuring and integrating with the Anchor Platform for the purpose of building an on & off-ramp service compatible with [SEP-24][sep-24], the ecosystem's standardized protocol for hosted deposits and withdrawals. By leveraging the Anchor Platform's support for SEP-24, businesses make their on & off-ramp service available as an in-app experience through Stellar-based applications such as wallets and exchanges, extending their reach and connecting with users through the applications they already use. Before continuing with this section, make sure that you have already [installed][installation-ap] the Anchor Platform, and configured necessary features, required by SEP-24: [SEP-1 (Stellar Info File)][sep1-ap], [SEP-10 (Stellar Authentication)][sep10-ap] and [SEP-45 (Stellar Web Authentication for contract account)][sep45-ap]. ## The Basic User Experience The complete customer experience a deposit and withdrawal goes something like this: 1. The customer opens the SEP-24 wallet application of their choice 2. The customer selects an asset to deposit and the wallet finds an anchor (clients could also chose the specific anchor) 3. Once the wallet authenticates with the anchor, the customer begins entering their KYC and transaction information requested by the anchor 4. The wallet provides instructions, and the customer deposits real fiat currency with the anchor (e.g. makes a bank transfer) 5. Once the wallet receives the deposit, the customer receives the tokenized asset on the Stellar network from the anchor's distribution account The customer can then use the digital asset on the Stellar network for remittance, payments, trading, store of value, or another use case not listed here. At some later date, the customer could decide to withdraw their assets from the Stellar network, which would look something like this: 1. The customer opens their wallet application 2. The customer selects the asset for withdrawal and wallet finds the anchor 3. After authenticating with the anchor, the wallet opens the given interactive URL and allows the customer to enter their transaction information (KYC has already been collected) 4. After asking for customer approval, the wallet sends the specified amount of the customer's asset balance to the anchor's distribution account on Stellar 5. Once the anchor receives the payment, the customer receives the withdrawn funds via bank transfer. [sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md [installation-ap]: ../../admin-guide/getting-started.mdx [sep1-ap]: ../sep1/README.mdx [sep10-ap]: ../sep10/README.mdx [sep45-ap]: ../sep45/README.mdx --- ## Integration(Sep24) One of the main points of interaction with the Anchor Platform is notifying the Anchor Platform about events related to the transaction. In general, you'll want to provide updates for the following events: - Your business is processing the KYC information provided by the user - Your business is ready to receive funds from the user - Your business has received funds from the user - Your business has sent funds to the user - Your business has processed a refund for the user's transaction - Your business experienced an unexpected error This is done by making JSON-RPC requests to the Platform API's endpoint. JSON-RPC requests allow you to update the status of the transaction. To move the transaction to a specific status, it's necessary to make a corresponding JSON-RPC request and pass data that is required by this RPC method. The Anchor Platform JSON-RPC API is designed to notify the platform about changes in the status of the transaction. Given that, the API will be called every time a user or the anchor takes any action that progresses the transaction status in the flow. Communication from the Anchor Platform about transaction updates, customer updates, and quote creation is handled through the event service. This is an optional feature that needs to be configured separately from the SEP-6 integration. For more information, see [Event Handling][event-handling]. You can find out more about transaction flow and statuses in the [SEP-24 protocol document][sep-24] ## Callbacks The Anchor Platform relies on the business server to provide and store information about quotes. ### Quotes and Fees To support the exchange of non-equivalent assets, the Anchor Platform exposes a SEP-38 compliant API to provide quotes for the exchange. The quote API is used to provide the user with the expected amount of the asset they will receive in exchange for the asset they are sending. The quote API is also used to provide the user with the expected fees for the transaction. Therefore, your business server must implement the [rate API][rate-callback] to provide quotes to the Anchor Platform. ## Securing Platform API ### Using API Key To enable API key authentication, modify your `dev.env` file: ```bash # dev.env PLATFORM_API_AUTH_TYPE=api_key # Will be used as API key SECRET_PLATFORM_API_AUTH_SECRET="your API key that business server will use" ``` Once enabled, all requests must include a valid `X-Api-Key` header, set to the configured API key. ### Using JWT ## Making JSON-RPC Requests ### JSON-RPC Request ### JSON-RPC Response ### Error Codes ## Updating Deposit Transaction Via JSON-RPC SEP-24 deposit flow diagram defines sequence/rules of the transaction's status transition and a set of JSON-RPC methods that should be called to change that status. You can't define the status you want to set for a specific transaction in your requests. Each JSON-RPC method defines data structures that it expects in request. If request doesn't contain a required attributes, the Anchor Platform will return and error and won't change status of the transaction. [![sep24 deposit flow](/assets/ap/sep24-deposit-flow-diagram.png)](/assets/ap/sep24-deposit-flow-diagram.png) :::tip Statuses in green are mandatory and define the shortest way. Statuses in yellow are optional and can be skipped. Statuses in red mean the transaction is in an error status or it has expired. ::: ### Ready to Receive Funds The first step of the deposit flow after starting the deposit itself is collecting KYC. It's usually done in the web-app, but can also be optionally provided by the wallet application, using [SEP-9]. Once the necessary KYC is collected, a `request_offchain_funds` JSON-RPC request should be made. ```json // request-offchain-funds.json [ { "id": 1, "jsonrpc": "2.0", "method": "request_offchain_funds", "params": { "transaction_id": "", "message": "Request offchain funds", "amount_in": { "amount": 10, "asset": "iso4217:USD" }, "amount_out": { "amount": 9, "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, "fee_details": { "total": 1, "asset": "iso4217:USD" }, "amount_expected": { "amount": 10 } } } ] ``` - `amount_in` is the amount the user has to sent to the business. - `amount_out` is the amount the user will receive. - `fee_details` is the total amount of fees collected by the business. - `asset` is part of the `amount_x` field and is in a SEP-38 format. In this example, it's set to USD, assuming the user made a bank transfer to the system using USD. Information abouts amounts (in/out/fee) is required if you want to move the transaction from the `incomplete` to the `pending_user_transfer_start` status. If transaction status is changed from `pending_anchor` to `pending_user_transfer_start`, you can skip defining the amounts. To execute this, you need to run: ```bash ./call-json-rpc.sh request-offchain-funds.json ``` :::tip When the KYC process is long (for example, ID verification), it's advised to first set the transaction status to `pending_anchor` by using `notify_interactive_flow_completed` JSON-RPC request. This will indicate to the user that KYC is being processed. ::: ### Processing KYC Information :::tip This step is optional. Most businesses don't use it. You can skip it and go to the [next step](#funds-received). Using this status is recommended when KYC verification may need to be performed asynchronously. ::: You **must** specify the `amount_x` fields. ```json // kyc-in-process.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_interactive_flow_completed", "params": { "transaction_id": "", "message": "Interactive flow completed.", "amount_in": { "amount": 10, "asset": "iso4217:USD" }, "amount_out": { "amount": 9, "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, "fee_details": { "total": 1, "asset": "iso4217:USD" }, "amount_expected": { "amount": 10 } } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh kyc-in-process.json ``` ### Funds Received If offchain funds were received, you'll want to provide an updated transaction information. ```json // offchain-funds-received.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_received", "params": { "transaction_id": "", "message": "Offchain funds received", "funds_received_at": "2023-07-04T12:34:56Z", "external_transaction_id": "7...9", "amount_in": { "amount": 10 }, "amount_out": { "amount": 9 }, "fee_details": { "total": 1 }, "amount_expected": { "amount": 10 } } } ] ``` - `funds_received_at` is the date and time of receiving funds - `external_transaction_id` is the ID of transaction on external network The amount fields are optional. If skipped, the values from previous JSON-RPC requests will be taken. To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-received.json ``` ### Waiting For User Funds In a real world, the transfer confirmation process may take time. In such cases, transactions should be set to a new status indicating that the confirmation of the transfer has been received but the funds themselves have not been received yet. ```json // offchain-funds-sent.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_sent", "params": { "transaction_id": "", "message": "Offchain funds sent", "funds_received_at": "2023-07-04T12:34:56Z", "external_transaction_id": "7...9" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-sent.json ``` ### Sending Onchain Funds Next, send a transaction on the Stellar network to fulfill a user request. After the transaction completion, it's necessary to send the `notify_onchain_funds_sent` JSON-RPC request to notify a user that the funds were successfully sent. ```json // onchain-funds-sent.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_onchain_funds_sent", "params": { "transaction_id": "", "message": "Onchain funds sent", "stellar_transaction_id": "7...9" } } ] ``` - `stellar_transaction_id` is the transaction id on Stellar network of the transfer To execute this, you need to run: ```bash ./call-json-rpc.sh onchain-funds-sent.json ``` After this JSON-RPC request, the transaction will be transferred to the `completed` status. ### Pending Trust This status has to be set if a payment requires an asset trustline that wasn't configured by the user. There are two ways of how the transaction may be moved to the `pending_trust` status. The first one is when the business server detects that the trustline isn't configured. The second one is when the business itself detects that the trustline is missing and wants to notify the user that it has to be configured. To move the transaction to the `pending_trust` status, it's necessary to make the following JSON-RPC request: ```json // request-trust.json [ { "id": 1, "jsonrpc": "2.0", "method": "request_trust", "params": { "transaction_id": "", "message": "Asset trustine not configured" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh request-trust.json ``` :::info The business server periodically checks if the trustline was configured. If it was, it may send a payment and change the status of the transaction to `pending_stellar`. ::: ### Trust Set This status has to be set if the business has detected that the trustline was or wasn't configured by user. ```json // trust-set.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_trust_set", "params": { "transaction_id": "", "message": "Asset trustine set", "success": "true" } } ] ``` - `success` flag which defines if trustline was or wasn't configured by user To execute this, you need to run: ```bash ./call-json-rpc.sh trust-set.json ``` :::info Depending on the `success` flag, the status of the transaction will be changed to `pending_stellar` if the trustline was set, or to `pending_anchor` if it wasn't. ::: ### Sending Refund There is a possibility to send funds back to the user (refund). You can refund the whole sum(full refund) or do a set of partial refunds. Also, if user sent more money than expected, you can refund a part of the sum back to the user and send the rest as onchain funds. ```json // refund-sent.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_refund_sent", "params": { "transaction_id": "", "message": "Refund sent", "refund": { "id": "1c186184-09ee-486c-82a6-aa7a0ab1119c", "amount": { "amount": 10, "asset": "iso4217:USD" }, "amount_fee": { "amount": 1, "asset": "iso4217:USD" } } } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh refund-sent.json ``` :::info If a sum of refunds is less than `amount_in`, the status of the transaction will be set to `pending_anchor`. Only if the sum of refunds is equal to `amount_in`, the status of the transaction will be set to `refunded`. ::: ### Refund Pending It's similar to [Refund sent](#refund-sent), but it handles a case when a refund has been submitted to external network but is not yet confirmed. The status of the transaction is set to `pending_external`. This is the status that will be set when waiting for Bitcoin or other external crypto network to complete a transaction, or when waiting for a bank transfer. ### Transaction Error If you encounter an unrecoverable error when processing the transaction, it's required to set the transaction status to `error`. You can use the message field to describe the error details. ```json // transaction-error.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_transaction_error", "params": { "transaction_id": "", "message": "Error occurred" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh transaction-error.json ``` :::tip If a user has made a transfer, you should do a transaction recovery, and then you can retry processing the transaction or initiate a refund. ::: ### Expired Transaction Your business may want to handle abandoned transactions by expiring those have remained inactive for a certain period. To achieve this, check the transaction status using the `GET /transactions` endpoint and sort the results by the `user_action_required_by` timestamp. If the timestamp has passed, manually execute the appropriate logic, such as expiring the transaction or initiating an auto-refund, based on the transaction's current status. For example, to expire transaction business should change the transaction status to `expired`: ```json // transaction-expired.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_transaction_expired", "params": { "transaction_id": "", "message": "Transaction expired" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh transaction-expired.json ``` :::tip This JSON-RPC method can't be used after the user has made a transfer. ::: ### On-Hold Transaction In rare cases, you may want to pause current transaction and request more information from the user (after the transfer has been received). This could be used for compliance use cases. ```json // transaction-hold.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_transaction_on_hold", "params": { "transaction_id": "", "message": "Transaction is on hold. Please contact customer support to resolve the hold." } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh transaction-hold.json ``` ### Transaction Recovery Transaction status can be changed from `error/expired` to `pending_anchor`. After recovery, you can refund the received assets or proceed with processing of the transaction. To recover a transaction, it's necessary to make the following JSON-RPC request: ```json // transaction-recovery.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_transaction_recovery", "params": { "transaction_id": "", "message": "Transaction recovered" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh transaction-recovery.json ``` ## Updating Withdrawal Transaction Via JSON-RPC This diagram defines a sequence/rules of transaction's status transition for SEP-24 withdrawal flow. [![sep24 withdrawal flow](/assets/ap/sep24-withdrawal-flow-diagram.png)](/assets/ap/sep24-withdrawal-flow-diagram.png) :::tip Statuses in green are mandatory and define the shortest way. Statuses in yellow are optional and can be skipped. Statuses in red mean the transaction is in an error status or it has expired. ::: Once the deposit flow is finished, implementing the withdrawal is straightforward. Some parts of the flow are similar and can be reused. The starting point both for withdrawal and for deposit is the same. ### Ready to Receive Funds Similar to deposit, the next step is to notify the user that the anchor is ready to receive funds. However, as your service will be receiving transactions over the Stellar network, the update will look differently. ```json // request-onchain-funds.json [ { "id": 1, "jsonrpc": "2.0", "method": "request_onchain_funds", "params": { "transaction_id": "", "message": "Request onchain funds", "amount_in": { "amount": 10, "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, "amount_out": { "amount": 9, "asset": "iso4217:USD" }, "fee_details": { "total": 1, "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, "amount_expected": { "amount": 10 }, "destination_account": "GD...G", "memo": "12345", "memo_type": "id" } } ] ``` - `memo` Value of memo to attach to the transaction - `memo_type` Type of memo that the anchor should attach to the transaction - `destination_account` Destination account To execute this, you need to run: ```bash ./call-json-rpc.sh request-onchain-funds.json ``` :::tip Setting `memo`, `memo_type`, and `destination_account` is optional. If integration with a third-party custodian is enabled, the Anchor Platform can generate `memo`, `memo_type`, and `destination_address` if a corresponding `deposit_info_generator_type` is chosen. Also, you can provide `memo` and `memo_type` to the request as shown above. Note that the memo must be unique, this is what helps to associate Stellar transactions with SEP transactions. If your business manages the assets, the Anchor Platform can generate memos for you. When the status is changed to `pending_user_transfer_start`, the Anchor Platform sets the `memo` and `memo_type` automatically (only if it's not included in the request). ::: :::note The Stellar account that will be used to receive funds should be configured. ::: ### Processing KYC Information This step is optional, and it's similar to [Processing KYC Information](#processing-kyc-information) of the deposit flow. ### Funds Received If onchain funds were received, you need to provide amounts and change the status of the transaction to `pending_anchor`. ```json // onchain-funds-received.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_onchain_funds_received", "params": { "transaction_id": "", "message": "Onchain funds received", "stellar_transaction_id": "7...9", "amount_in": { "amount": 10 }, "amount_out": { "amount": 9 }, "fee_details": { "total": 1 } } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh onchain-funds-received.json ``` :::tip This method will be called automatically by the Stellar payment observer when it detects that onchain funds have been received. ::: ### Amount Updated If onchain funds were received, but for some reason the `amount_in` differs from specified in the interactive flow (`amount_expected`), you can update `amount_out` and `fee_details` to make them correspond to the actual `amount_in`. The status of the transaction in this case won't be changed and will be equal to `pending_anchor`. ```json // amounts-updated.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_amounts_updated", "params": { "transaction_id": "", "message": "Amounts updated", "amount_out": { "amount": 9 }, "fee_details": { "total": 1 } } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh amounts-updated.json ``` :::note Only `amount_out` and `fee_details` can be updated using this JSON-RPC request, and you don't need to specify the assets of the amounts. ::: ### Offchain Funds Sent To complete the transaction and change its status to `completed`, you need to make the `notify_offchain_funds_sent` JSON-RPC request. ```json // offchain-funds-sent.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_sent", "params": { "transaction_id": "", "message": "Offchain funds sent", "funds_sent_at": "2023-07-04T12:34:56Z", "external_transaction_id": "a...c" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-sent.json ``` ### Offchain Funds Available You can move transaction status to `pending_user_transfer_complete` if offchain funds were sent, and if it's ready for the user / recipient to pick it up. ```json // offchain-funds-available.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_available", "params": { "transaction_id": "", "message": "Offchain funds available", "external_transaction_id": "a...c" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-available.json ``` ### Offchain Funds Pending Another option is to move the transaction's status to `pending_external`. This status means that the payment has been submitted to an external network, but is not yet confirmed. ```json // offchain-funds-pending.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_pending", "params": { "transaction_id": "", "message": "Offchain funds pending", "external_transaction_id": "a...c" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-pending.json ``` ### Refund Sent The refund logic works in the same way as for the deposit flow. For more details, see [Refund Sent](#refund-sent) of the deposit flow. ### Transaction Error Works in the same manner as for the deposit flow. For more details, see [Transaction Error](#transaction-error) of the deposit flow. ### Expired Transaction Works in the same manner as for the deposit flow. For more details, see [Expired Transaction](#expired-transaction) of the deposit flow. ### On-Hold Transaction Works in the same manner as for the deposit flow. For more details, see [On-Hold Transaction](#on-hold-transaction) of the deposit flow. ### Transaction Recovery Works in the same manner as for the deposit flow. For more details, see [Transaction Recovery](#transaction-recovery) of the deposit flow. ## Tracking Stellar Transactions [sep-9]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md [sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md [event-handling]: ../../admin-guide/events/README.mdx [rate-callback]: ../../api-reference/callbacks/README.mdx [json-rpc-methods]: ../../api-reference/platform/rpc/methods/README.mdx --- ## Set Up a Production Server Once the test server is live and you have tested both deposit and withdraw flows, it's time to get started with the real deploy connected to real KYC and real banking rails providers. Before using any banking APIs, it's critical that you perform a full security audit on the system to make sure that there aren't any vulnerabilities. ## Deploying a Secure Environment Make sure to keep the test server up, and deploy the production (mainnet) system in a separate environment. Having two deploys allows you to validate new features on the testnet before moving them to the final production deploy. You can also have a third staging environment if there's a big team working on this codebase and/or there will be many pushes to be tested internally before sharing with other institutions. To switch to Stellar's public (mainnet) network, all you have to do is change the network [passphrase](../../../../networks/README.mdx#network-passphrases) (for authenticating requests) and [Horizon URL](https://horizon.stellar.org). You can copy your existing development configs to create a production configuration. First, you need to change your info file (`stellar.toml`): ```toml # stellar.toml NETWORK_PASSPHRASE = "Public Global Stellar Network ; September 2015" ``` Next, change your Anchor Platform configuration in `production.env` file: ### For Horizon server connection: ```bash # production.env STELLAR_NETWORK_NETWORK="Public" STELLAR_NETWORK_HORIZON_URL="https://horizon.stellar.org" ``` ### For Stellar RPC server connection: ```bash # production.env STELLAR_NETWORK_NETWORK="Public" STELLAR_NETWORK_RPC_URL="https://mainnet.sorobanrpc.com" ``` ## Connecting to Real KYC Most anchors need to collect [Know Your Customer](https://en.wikipedia.org/wiki/Know_your_customer) information to comply with local regulations before honoring deposits and withdrawals. The KYC flow usually consists of a simple form that gathers relevant information about the user such as name, email, address, age, and government-issued ID number. How you handle KYC is up to you: there are many services that provide KYC solutions through APIs and iFrames, and validate the input data and sync with governmental databases to verify requirements. Each jurisdiction has specific KYC requirements, and they differ from jurisdiction to jurisdiction, so it's best to find a country-specific KYC provider that meets your needs. Some countries require different KYC fields depending on the amount to be deposited or withdrawn. If that's the case in your jurisdiction and you need to adapt your KYC forms based on the deposit or withdrawal amount, simply add an amount field before the KYC form, and make sure that the KYC fields are updated based on that value. KYC information should be linked to the session created through [Stellar Web Authentication](../sep10/README.mdx) and, consequently, to the user, so you only need to ask the user for it once. After the first KYC flow is complete, a user shouldn't have to input the information again. Make sure the errors and validation messages are clear and include instructions for what to do next to ensure a good user experience and increase the KYC conversion rate. You should also localize messages based on the user's language and location. ## Pre-Filling the KYC Form Pre-filling the KYC form is a great way to reduce the friction of getting started using an anchor, and wallets usually provide a set of fields that are commonly used throughout the ecosystem. In summary, the anchor can render the KYC form with the user's values that were previously sent by the wallet in the `/transactions/deposit/interactive` and `/transactions/withdraw/interactive` endpoints. All fields from [SEP-9](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md) can be sent by wallets in the previously mentioned endpoints, but the most common are: email, first name, last name and phone number. Also, you should still enable the pre-filled fields to be editable, since the user might have inputted a different name in the Wallet's sign-up process, and could want to edit it before finalizing the Anchor's KYC process. All SEP-9 data that was sent from the wallet is a part of the [Interactive JWT](./faq.mdx#how-to-use-jwts), send by the Anchor Platform ## Connecting to Real Banking Rails Fiat-backed token issuers are expected to manage a full reserve. That means there's a 1:1 relationship between Stellar-network tokens and money in the bank. Since each fiat token on Stellar is backed by, and can be redeemed for, an underlying, real-world asset, issuers of fiat-backed tokens need to connect to real banking rails to validate user deposits (through bank transfers, credit card payments, etc.) and to complete user withdrawals (generally through bank transfers). If you're an anchor honoring deposits and withdrawals of a token another organization issues, you'll follow a similar process. In order to fetch (and identify) a user transfer, issuers usually take one of two approaches: - API Polling: this option consists of fetching the bank's API, through a cron job, to check for the updated status of the list of transfers received by (and sent from) the issuer's bank account. Once the system confirms a new transaction and identifies that it relates to a specific deposit, it can send the digital funds to that user's account - Webhook: not all banking rails support this option, but it's the leanest in terms of back-end logic. In this approach, the bank proactively hits an issuer's endpoint once it receives a new transfer, updating that information on the issuer's database. The issuer can then can match that transaction to an existing in-process deposit, and validate that the user can receive their digital funds There are many ways to identify that a specific bank transfer relates to a specific deposit (and, consequently, to a user). Some banks (and countries) have transfer infrastructure that allows the creation of a single bank account per transfer; others require users to add an identification parameter to their transfers. Some banks provide the user ID number in the transaction information so issuers can match that with the information provided in the KYC form. Make sure to do a full security audit on your systems when banking rails connections are in place. Some banks provide a testing API that can be used for development and deployment to testnet or staging environments, which means you can test and audit the codebase before moving to a final production-ready bank integration. For better security, some anchors also prefer to add a manual final step before approving withdrawal transfers. In terms of UX, this manual approval is acceptable as long as the wait times align with user expectations, which usually means they aren't longer than a couple of hours. ## Testing Edge Cases Once your application is fully functional, it's a good idea to test different scenarios and edge cases to make sure the system is behaving as expected. Here's a list of testing suggestions that should cover a large amount of the application's edge cases: ### General Tests - Test the interactive flow usability - Test the interface using different locale information, and check for translated content including error messages, responses, date formatting, and number formatting ### KYC Tests - Check that KYC appears with a new wallet SK - Check that KYC doesn't accept incorrectly formatted inputs, and that the error messages are comprehensible - Check that you can use the same KYC information (email, phone number, username, etc) multiple times - Check that you can go through KYC multiple times with the same Stellar SK. ### Interactive Test - Check that the deposit flow goes through, and that the banking rails are working - Check that you cannot make a withdrawal with a value higher than the current balance - Check that the withdrawal flow goes through, and that the banking rails are working ### Security Tests - Make sure platform endpoints are secured ## Polishing and Internationalization Supporting two languages (English and the fiat currency country language) allows users to have a seamless experience while navigating through screens, and supports international institutions (like wallets) that need to test the product before starting new integrations. You can support multiple languages in your webapp by using the `Accept-Language` parameter from the http request headers to localize the content and allowing users to change that in a simple way (e.g. a flag icon on the top bar). If a specific wallet doesn't send the header parameter, we recommend showing the user a language selection screen in the beginning of the deposit and withdraw processes. Once a user chooses a language, you can store their selection so you only need to ask them once. In addition to localizing text, make sure to check number formatting, dates, etc. Having a group of beta testers is a great way to check if there are any edge cases that need polishing, and to confirm that the system is working well with a variety of user inputs. You can beta test using a soft launch stage before you start putting effort into marketing and distribution. Documenting the testing process with screenshots and videos is very helpful for future security audits, and gives new partners and potential users clarity and confidence in the product. ## Connecting to Wallets All Anchor user interactions are done through a Wallet, so it's vital for Anchors to be connected to Wallets that have a good market penetration in the region where the business is most focused. Connecting to Wallets is a simple process, since both ends of that integration are already compliant with SEPs. Stellar.org maintains a [list of wallets](https://stellar.org/ecosystem/projects), many of which currently support SEP-24 Sending them a message with more information on an asset and an issuer account is a great way to start getting some real users to the Anchor. --- ## Cross-Border Payments (SEP-31) SEP-31 allows for a means by which wallets and/or exchanges interact with Stellar's existing set of send-side services. --- ## Configuration(Sep31) ## Modify a Stellar Info File Let's start by modifying our `stellar.toml` file created [earlier][sep1-ap]. Wallets need to know that SEP-31 functionality is supported by your business, and they also need to know all currencies you support. ```toml # dev.stellar.toml ACCOUNTS = ["add your public keys for your distribution accounts here"] SIGNING_KEY = "add your signing key here" NETWORK_PASSPHRASE = "Test SDF Network ; September 2015" DIRECT_PAYMENT_SERVER = "http://localhost:8080/sep31" WEB_AUTH_ENDPOINT = "http://localhost:8080/auth" [[CURRENCIES]] code = "USDC" issuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" status = "test" is_asset_anchored = false desc = "USD Coin issued by Circle" [DOCUMENTATION] ORG_NAME = "Your organization" ORG_URL = "Your website" ORG_DESCRIPTION = "A description of your organization" ``` Note that you'll need to create another file for your production deployment that uses the public network's passphrase, your production service URLs, your mainnet distribution accounts and signing key, as well as the mainnet issuing accounts of the assets your service utilizes. ## Enable Cross Border Payments Now you're ready to enable cross-border payments the SEP-31 API. Specify the following in your `dev.assets.yaml` file. ```yaml # dev.assets.yaml items: - id: stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 distribution_account: GBLSAHONJRODSFTLOV225NZR4LHICH63RIFQTQN37L5CRTR2IMQ5UEK7 significant_decimals: 2 sep31: enabled: true quotes_supported: true quotes_required: true receive: min_amount: 0 max_amount: 10000 methods: - ACH ``` The information provided in the `sep31` and `send` objects closely map to the information that will be exposed to the wallet application using the [`GET /info`][sep31-get-info] SEP-31 endpoint. The Anchor Platform also uses this information to validate requests made to your service. `sep31.fields.transaction` should be left empty and will be removed in a future release, but you can adjust the `send.min_amount` and `send.max_amount` values according to your service's limits. The `sep31.quotes_supported` and `sep31.quotes_required` determine whether or not sending organizations can and are required to request an FX rate using the [SEP-38 `POST /quote`][sep38-post-quote] endpoint. Almost all senders prefer this approach so that they can communicate the rate to their customers prior to proceeding. Add the following variable to your environment file. ```bash # dev.env SEP31_ENABLED=true ``` Senders should now be able to discover, authenticate, and initiate transactions with your service! Run the following command to start the Anchor Platform. ```bash docker compose up ``` Check that your API is live. ```bash curl http://localhost:8080/sep31/info | jq ``` You should get the following. ```json { "receive": { "USDC": { "enabled": true, "quotes_supported": true, "quotes_required": true, "min_amount": 0, "max_amount": 10000, "fields": { "transaction": {} } } } } ``` ## Enable the Customer KYC API Businesses need to collect and validate KYC information on the customers they're facilitating transactions for. Clients determine what KYC information needs to be collected and send that information via a SEP-12 KYC API hosted by the Anchor Platform, but the Anchor Platform never stores personally-identifiable information (PII). Instead, it forwards requests from clients to the business server, and returns the business' responses back to the client, acting as a proxy server. See the [Anchor Platform KYC API specification][platform-api-kyc] for details on the endpoints that must be implemented on your business' server. To make this API available to clients, lets add the service URL to our Stellar Info File. ```toml # dev.stellar.toml KYC_SERVER = "http://localhost:8080/sep12" ``` Lets enable it in our environment too. ```bash # dev.env SEP12_ENABLED=true ``` Finally, we have to define your business' customer types. Each type of customer requires different a set of KYC information. For example, you can offer your cross-border payments service in two distinct regulatory jurisdictions, so customers in different jurisdictions have different KYC requirements and would be represented using different types. :::info Currently, customer types must be mutually exclusive, meaning a customer cannot be more than one type. This limitation is in place because the Anchor Platform cannot validate whether a customer is approved for a specific type of transaction, such as one sending a large amount. It can only validate that a customer is approved for one of the customer types defined. This limitation will be removed in a future release. ::: In this guide, we'll only have two types, a sending customer type and a receiving customer type. Currently, our customer types are defined in our assets configuration, but this will change in a future release. ```yaml # dev.assets.yaml sep31: sep12: sender: types: sep31-sender: description: customers sending to recipients receiver: types: sep31-receiver: description: customers receiving from senders ``` Let's ping the info endpoint again to verify. After `docker compose up`, run the following command: ```bash curl http://localhost:8080/sep31/info | jq ``` You should get the following: ```json { "receive": { "USDC": { "enabled": true, "quotes_supported": true, "quotes_required": true, "min_amount": 0, "max_amount": 10000, "fields": { "transaction": {} } } } } ``` ## Enable the RFQ API Businesses need to provide their send-side counterparts with a [Rate][get-rates-api] API to check the exchange rates they're offering between the on-chain asset being used for settlement and the fiat asset being used to pay the recipient. If the rate is competitive, senders also need to be able to request a commitment to the rate currently being offered from business for a short period of time. The Anchor Platform provides the [SEP-38 RFQ API][sep38] to senders for this purpose. To make this API available to clients, lets add the service URL to our Stellar Info File. ```toml # dev.stellar.toml DIRECT_PAYMENT_SERVER = "http://localhost:8080/sep31" WEB_AUTH_ENDPOINT = "http://localhost:8080/auth" KYC_SERVER = "http://localhost:8080/sep12" QUOTE_SERVER = "http://localhost:8080/sep38" ``` Lets enable it in our environment too. ```bash # dev.env SEP38_ENABLED=true ``` We also need to enable USDC to be used in this API, as well as add an off-chain asset it can be exchanged with. ```yaml # dev.assets.yaml items: - id: stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 distribution_account: GBLSAHONJRODSFTLOV225NZR4LHICH63RIFQTQN37L5CRTR2IMQ5UEK7 significant_decimals: 2 sep31: enabled: true quotes_supported: true quotes_required: true receive: min_amount: 0 max_amount: 10000 methods: - ACH sep38: enabled: true exchangeable_assets: - iso4217:BRL country_codes: - BR - id: iso421:BRL sep38: enabled: true exchangeable_assets: - stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 country_codes: - BR significant_decimals: 2 buy_delivery_methods: - name: PIX description: Have BRL sent directly to your bank account. ``` Let's test that your RFQ API is live! Following `docker compose up`: ```bash curl http://localhost:8080/sep38/info | jq ``` You should get the following: ```json { "assets": [ { "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, { "asset": "iso4217:BRL", "country_codes": ["BR"], "buy_delivery_methods": [ { "name": "PIX", "description": "Have BRL sent directly to your bank account." } ] } ] } ``` ## Configure Callback API Authentication Just as your business will need to make requests to the Anchor Platform, the Anchor Platform will need to make requests to your business. Let's add authentication to these requests as well. ```bash # dev.env CALLBACK_API_BASE_URL=http://server:8081 CALLBACK_API_AUTH_TYPE=jwt CALLBACK_API_AUTH_JWT_EXPIRATION_MILLISECONDS=30000 SECRET_CALLBACK_API_AUTH_SECRET= ``` `CALLBACK_API_BASE_URL` uses `server` instead of `localhost` as the host because the Anchor Platform will be making requests to your business server from within the local network created by docker compose. When configuring your service in a staging or production environment, make sure to update your service urls. We'll define the server that implements the endpoints defined in the Callback API in the following section. [sep31-get-info]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md#get-info [sep1-ap]: ../sep1/README.mdx [get-rates-api]: ../../api-reference/callbacks/get-rates.api.mdx [sep38]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md [sep38-post-quote]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md#post-quote [platform-api-kyc]: ../../api-reference/callbacks/get-customer.api.mdx --- ## Getting Started(Sep31) This guide will walk you through configuring and integrating with the Anchor Platform for the purpose of building a cross-border payments recieve-side service compatible with [SEP-31][sep-31], the ecosystem's standardized protocol for cross-border payments. By leveraging the Anchor Platform's support for SEP-31, businesses make their service compatible with Stellar's existing set of send-side services. :::info As we improve the documentation, parts of this guide that are relevant to other use cases may be moved into their own sections. ::: Before continuing with this section, make sure that you have already [installed][installation-ap] the Anchor Platform and configured the necessary features required by SEP-31: [SEP-1 (Stellar Info File)][sep1-ap], [SEP-10 (Stellar Authentication)][sep10-ap] and [SEP-45 (Stellar Web Authentication for contract account)][sep45-ap]. [sep-31]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md [installation-ap]: ../../admin-guide/getting-started.mdx [sep1-ap]: ../sep1/README.mdx [sep10-ap]: ../sep10/README.mdx [sep45-ap]: ../sep45/README.mdx --- ## Integration(Sep31) Integrating with the Anchor Platform for facilitating cross-border payments involves implementing the following, at a minimum: - [`GET /customer`][get-customer] & [`PUT /customer`][put-customer] KYC API endpoints to request & collect customers' KYC data - [`GET /rate`][get-rate] RFQ API endpoint to provide FX rates between the on & off-chain assets supported - `GET /transactions` requests to fetch updates on the Anchor Platform's transactions' statuses (documentation coming soon) - [`JSON-RPC`][json-rpc-methods] requests to update the Anchor Platform's transactions' statuses The following may also be required depending on your use case: - [`DELETE /customer`][delete-customer] if your business wants or is required to allow senders to request deletion of customer data ## Create a Business Server First, lets create a business server and add it to our docker compose file. ```yaml version: "3.8" services: sep-server: image: stellar/anchor-platform:latest command: --sep-server env_file: - ./dev.env volumes: - ./config:/home ports: - "8080:8080" depends_on: - db platform-server: image: stellar/anchor-platform:latest command: --platform-server env_file: - ./dev.env volumes: - ./config:/home ports: - "8085:8085" depends_on: - db server: build: . ports: - "8081:8081" env_file: - ./dev.env db: image: postgres:14 ports: - "5432:5432" env_file: - ./dev.env ``` Next, create a simple web server using your preferred programming language and a `Dockerfile` that starts the server. `docker compose up` should successfully start all three services. This guide does not provide an example implementation of the endpoints, but you can find more information about the request and response schemas in the [Anchor Platform API Reference][ap-api], and the sections below will expand on concepts important to understand when implementing the endpoints. ## Customer Callback Endpoints The Anchor Platform never stores your customers' PII, and instead acts as a proxy server between client applications and your business, forwarding requests and responses to the other party. Currently, requests and responses are almost identical to those defined in the [SEP-12 KYC API specification][sep12]. ### Identifying Customers Customers can be identified using two approaches. The first approach uses a Stellar account and memo. When using the Anchor Platform for facilitating cross-border payments, the sending organization uses their own Stellar account, the one used to authenticate via [SEP-10 Stellar Authentication][ap-sep10], when registering customers with your business. Memos are used to distinguish unique customers originating from the same sending organization. The second approach uses customer IDs generated by your service. For example, if a sending organization is registering a customer, your business will receive a `PUT /customer` request like the following: ```json { "account": "GDJUOFZGW5WYBK4GIETCSSM6MTTIJ4SUMCQITPTLUWMQ6B4UIX2IEX47", "memo": "780284017", "type": "sep31-sender", "first_name": "John", "last_name": "Doe", "email": "johndoe@example.com" } ``` In this example, the `GDJ...X47` public key identifies the sending organization, and the `780284017` memo identifies the customer. Memos are usually 64-bit integers, but they can also be other data types, so they should be saved as strings. In response, your business should return a customer ID. ```json { "id": "fb5ddc93-1d5d-490d-ba5f-2c361cea41f7" } ``` Your business server can use any identifier for customers as long as it is a string. Following the registration of a customer, the sending organization can use either approach when checking the customer's status. For example, you may get a `GET /customer` request like the following: ``` /customer?account=GDJUOFZGW5WYBK4GIETCSSM6MTTIJ4SUMCQITPTLUWMQ6B4UIX2IEX47&memo=780284017&type=sep31-sender ``` Or, the sending organization could use the identifier you returned when they originally registered the customer. ``` /customer?id=fb5ddc93-1d5d-490d-ba5f-2c361cea41f7&type=sep31-sender ``` Your business will need to maintain a mapping between the account & memo used to originally register the customer and the ID you return in the response, as well as the KYC data provided. In future iterations of the Anchor Platform, we may maintain this mapping for your business so you only have to work with the IDs you generate. ### Customer Types Your business likely requires different sets of KYC information depending on the type of customer. You can define the labels for each of these customer types in your `dev.assets.yaml` file, and your sending organizations will need to understand which label to use when registering or querying the status of customers. In `PUT /customer` requests, you should use the type passed to evaluate whether the sender has provided all of the required fields. In `GET /customer` requests, you should use the type to determine the customer's status. ### Test with the Demo Wallet You can test your implementation with the [Stellar Demo Wallet][demo-wallet] following the steps below. 1. Select "Generate keypair for new account" 2. Select "Create account" 3. Select "Add Asset" and enter the asset code and the Anchor Platform's home domain, `localhost:8080` 4. Select "Add trustline" 5. Fund your account with a balance of the asset 6. Select "SEP-31 Send" in the dropdown menu You should see the demo wallet find your service URLs, authenticate, and check which KYC fields it needs to collect. It should then present a form for you to enter the KYC details for the sender and receiver. [![demo wallet after initiating a transaction](/assets/ap/anchor-platform-sep31-demo-wallet-widget.png)](/assets/ap/anchor-platform-sep31-demo-wallet-widget.png) Once you've entered in the information requested, it will send that information to the Anchor Platform, which will send it to your business server. Once the demo wallet has the customers' IDs you generated, it will initiate a transaction which should fail. ## Rate Callback Endpoint Once the sending organization has registered the customers involved in the transaction, it will need to request a quote, or FX rate, from your business. The Anchor Platform requests this information from your business server using the [`GET /rate` endpoint][get-rate]. ### Firm vs. Indicative Quotes Requests for quotes will have a `type` parameter that is either [`indicative`][indicative] or [`firm`][firm]. If `type=firm`, your response must include the `id` & `expires_at` date-time field and reserve the liquidity needed to fulfil this quote until the quote expires. If `type=indicative`, do not return `id` or `expires_at` fields because the rate provided will not be used in a transaction. Note that the client may request that the quote expires after a specific date-time using the `expires_after` parameter. Your business must honor this request by returning an `expires_at` value that is at or after the requested date-time or reject the request with a 400 Bad Request response, which will be forwarded to the client. ### Using the Client ID Requests may include a `client_id` parameter that identifies the sending organization requesting the rate. You can use this parameter to adhere to the commercial terms agreed upon with that sending organization, such as offering discounted rates. `client_id` may not be present for indicative requests, in which case your market price should be returned. Currently `client_id` will always be the Stellar public key the sending organization used to authenticate with the Anchor Platform. ### Delivery Methods It is common for businesses' rates and fees to differ depending on the payment rails used to send funds to the recipient. If your delivery methods are configured in your `asset.yaml` file, clients will always provide the payment rail they want your business to use for firm quote requests. Because this endpoint is currently only used paying out remittances in off-chain assets, the `buy_delivery_method` will be used. If this endpoint is ever used in other transaction flows such as SEP-24 deposits, then `sell_delivery_method` may also be passed for business that support these types of transactions. ## Fetching Transaction Status Updates To facilitate cross-border payments, you'll need to be able to detect when a sending organization has sent your business an on-chain payment and determine which transaction that payment was meant to fulfil. The easiest way to do that is to run the Stellar Observer, which will detect these payments and update the corresponding transaction record with information about the payment. Your business can then detect these updates by polling the `GET /transactions` Platform API endpoint. ### Running the Stellar Observer The Stellar Observer monitors the Stellar ledger for payments made to your account(s) and updates the corresponding transaction records with on-chain payment information. To run the observer, add the following to your docker compose file. ```yaml services: ... observer: image: stellar/anchor-platform:latest command: --stellar-observer env_file: - ./dev.env volumes: - ./config:/home ``` ### Polling for Received Payments The Stellar Observer makes JSON-RPC requests to the Platform API whenever it detects payments received for transactions initiated by sending organizations, thus updating the transaction's `transfer_received_at` date-time. Your business should periodically poll the `GET /transactions` Platform API endpoint to detect these updates. You can refer to the following example: ```bash curl http://localhost:8080/transactions?sep=31&order_by=transfer_received_at&order=desc ``` The response will include a list of cross-border payment transactions initiated by sending organizations. This list will be ordered according to the time a payment was received for that transaction. For each transaction returned, your business should check whether or not it has already detected the payment for that transaction. If it has, you have detected all payments made to your account(s). ## Updating Transaction Via JSON-RPC SEP-31 flow diagram defines sequence/rules of the transaction's status transition and a set of JSON-RPC methods that should be called to change that status. You can't define the status you want to set for a specific transaction in your requests. Each JSON-RPC method defines data structures that it expects in the request. If the request doesn't contain required attributes, the Anchor Platform will return an error and won't change the status of the transaction. [![sep31 flow](/assets/ap/sep31-transition-diagram.png)](/assets/ap/sep31-transition-diagram.png) :::tip Statuses in green are mandatory and define the shortest flow. Statuses in yellow are optional and can be skipped. Statuses in red mean the transaction is in an error status or it has expired. ::: You can create a [template][sep24-integration-make-json-rpc-request] for making a JSON-RPC requests to the Anchor Platform. This chapter also contains information about the format of [request][sep24-integration-rpc-request]/[response][sep24-integration-rpc-response] and [error codes][sep24-integration-error-codes] that might be returned by the Anchor Platform. ### Ready to Receive Funds SEP-31 Transactions should initially be in the `pending_receiver` status. To request funds from the Sending Anchor, the Receiving Anchor should change the transaction status to `pending_sender` by making the following RPC request: ```json // request-onchain-funds.json [ { "id": 1, "jsonrpc": "2.0", "method": "request_onchain_funds", "params": { "transaction_id": "", "message": "Request onchain funds", "destination_account": "GD...G", "memo": "12345", "memo_type": "id" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh request-onchain-funds.json ``` The transaction status will be changed to `pending_sender`. ### Funds Received If the Sending Anchor has sent the funds, the Receiving Anchor should change the transaction status to `pending_receiver` by making the following JSON-RPC request: ```json // onchain-funds-received.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_onchain_funds_received", "params": { "transaction_id": "", "message": "Onchain funds received", "stellar_transaction_id": "7...9", "amount_in": { "amount": 10 }, "amount_out": { "amount": 9 }, "fee_details": { "total": 1 } } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh onchain-funds-received.json ``` The transaction status will be changed to `pending_receiver`. ### Offchain Funds Sent To complete the transaction and change its status to `completed`, you need to make a `notify_offchain_funds_sent` JSON-RPC request. ```json // offchain-funds-sent.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_sent", "params": { "transaction_id": "", "message": "Offchain funds sent", "funds_sent_at": "2023-07-04T12:34:56Z", "external_transaction_id": "a...c" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-sent.json ``` ### Offchain Funds Pending Another option is to move the transaction's status to `pending_external`. This status means that payment has been submitted to external network, but it is not yet confirmed. ```json // offchain-funds-pending.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_pending", "params": { "transaction_id": "", "message": "Offchain funds pending", "external_transaction_id": "a...c" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-pending.json ``` ### Verifying Customer Information In some cases, the Receiving Anchor might need to request an updated information from the Sending Anchor. For example, the bank tells the Receiving Anchor that the provided Receiving Client's name is incorrect or missing a middle initial. Since this information was sent via SEP-12, the transaction should go into the `pending_customer_info_update` status until the Sending Anchor makes another SEP-12 `PUT /customer` request to update. The Sending Anchor can check which fields need to be updated by making a SEP-12 `GET /customer` request including the id or account & memo parameters. The Receiving Anchor should respond with a `NEEDS_INFO` status and `last_name` included in the fields described. After the Sending Anchor makes a SEP-12 `PUT /customer` request, call the `notify_customer_info_updated` JSON-RPC method againto update the transaction status. Additionally, call this method whenever the SEP-12 status for a customer changes, such as when the customer's information is being validated and the status changes from `NEEDS_INFO` to `PROCESSING`. This ensures that any clients configured with a callback URL are notified of the latest customer status, allowing the client to prompt the user to update their information. ```json // notify-customer-info-updated.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_customer_info_updated", "params": { "transaction_id": "", "message": "Customer info updated", "customer_id": "45f8884d-d6e1-477f-a680-503179263359", "customer_type": "sep31-receiver" // or sep31-sender } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh notify-customer-info-updated.json ``` ### Refund Sent There is a possibility to send all funds back to the `Sending Anchor` (refund). You need to refund the whole sum(full refund). ```json // refund-sent.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_refund_sent", "params": { "transaction_id": "", "message": "Refund sent", "refund": { "id": "1c186184-09ee-486c-82a6-aa7a0ab1119c", "amount": { "amount": 10, "asset": "iso4217:USD" }, "amount_fee": { "amount": 1, "asset": "iso4217:USD" } } } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh refund-sent.json ``` :::note You can't do multiple refunds in SEP-31 flow. For this reason, the amount to refund plus the amount fee should equal `amount_in`. Otherwise, you will get an error. ::: ### Transaction Error If you encounter an unrecoverable error when processing the transaction, it's required to set the transaction status to `error`. You can use the message field to describe the details of the error. ```json // transaction-error.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_transaction_error", "params": { "transaction_id": "", "message": "Error occurred" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh transaction-error.json ``` :::tip If a user has made a transfer, you should do a transaction recovery, and then you can retry processing the transaction or initiate a refund. ::: ### Expired Transaction Your business may want to expire those transactions that have been abandoned by the user after some time. It's a good practice to clean up inactive transactions in the `incomplete` status. To do so, simply change the transaction's status to `expired`. ```json // transaction-expired.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_transaction_expired", "params": { "transaction_id": "", "message": "Transaction expired" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh transaction-expired.json ``` :::tip This JSON-RPC method can't be used after the user has made a transfer. ::: ### Transaction Recovery The transaction status can be changed from `error/expired` to `pending-anchor`. After recovery, you can refund the received assets or proceed with the processing of the transaction. To recover the transaction, it's necessary to make the following JSON-RPC request: ```json // transaction-recovery.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_transaction_recovery", "params": { "transaction_id": "", "message": "Transaction recovered" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh transaction-recovery.json ``` ### Configuration You can enable these types of transactions by updating your `assets.yaml` file configuration: ```yaml items: - ... sep31: quotes_required: false ``` [ap-api]: ../../README.mdx [ap-sep10]: ../sep10/README.mdx [sep12]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md [demo-wallet]: https://demo-wallet.stellar.org [indicative]: https://www.investopedia.com/terms/i/indicativequote.asp [firm]: https://www.investopedia.com/terms/f/firmquote.asp [get-customer]: ../../api-reference/callbacks/get-customer.api.mdx [put-customer]: ../../api-reference/callbacks/put-customer.api.mdx [get-rate]: ../../api-reference/callbacks/get-rates.api.mdx [put-customer-callback]: ../../api-reference/callbacks/put-customer.api.mdx [delete-customer]: ../../api-reference/callbacks/del-customer.api.mdx [json-rpc-methods]: ../../api-reference/platform/rpc/methods/README.mdx [sep24-integration-make-json-rpc-request]: ../sep24/integration.mdx#making-json-rpc-requests [sep24-integration-rpc-request]: ../sep24/integration.mdx#json-rpc-request [sep24-integration-rpc-response]: ../sep24/integration.mdx#json-rpc-response [sep24-integration-error-codes]: ../sep24/integration.mdx#error-codes --- ## Stellar Authentication for Contract Accounts (SEP-45) ## Overview SEP-45 (Stellar Web Authentication for Contract Accounts) enables smart wallet applications to create authenticated sessions with Stellar anchors by proving control over a contract account (`C...`). Once authenticated, wallets receive a JSON Web Token (JWT) that they use in subsequent requests to the anchor's standardized services. For the complete specification, see [SEP-45: Stellar Web Authentication for Contract Accounts][sep-45]. The Anchor Platform implements SEP-45 with support for: - **Challenge/Response Flow**: GET `/sep45/auth` to request authorization entries, POST `/sep45/auth` to validate and receive a JWT - **Contract Account Authentication**: Support for contract accounts (`C...`) using Soroban authorization entries - **Transaction Simulation**: Automatic simulation of transactions to verify authorization entries - **Multiple Home Domains**: Support for multiple domains and wildcard patterns :::info **SEP-45 vs SEP-10**: SEP-45 does not replace SEP-10. SEP-45 supports contract accounts (`C...`), while SEP-10 supports classic accounts (`G...`) and muxed accounts (`M...`). Services wishing to support all account types should implement both SEPs. ::: ## Typical Authentication Flow 1. The **Client** requests a unique challenge from the **Server** 2. The **Client** verifies and signs the challenge 3. The **Client** submits the signed challenge to the **Server** 4. The **Server** verifies the challenge and responds with a JWT session token An implementation of the SEP-45 contract is provided in the [Anchor Platform repository][sep-45-contract]. An instance of this contract is deployed at `CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX` on testnet. ## Enable SEP-45 SEP-45 requires integration with Stellar RPC to simulate transactions. The Anchor Platform can connect to the Stellar RPC server of your choice. You can use a public Stellar RPC provider, or you can run your own. A list of public providers can be found [here][rpc-providers]. To enable SEP-45, set the following environment variables in your `dev.env` file. ```bash # dev.env STELLAR_NETWORK_TYPE=rpc STELLAR_NETWORK_RPC_URL=https://soroban-testnet.stellar.org SEP45_ENABLED=true SEP45_HOME_DOMAINS=localhost:8080 SEP45_WEB_AUTH_CONTRACT_ID="CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX" SECRET_SEP10_SIGNING_SEED="a Stellar private key" SECRET_SEP45_JWT_SECRET="a secret encryption key" ``` ### Required Configuration (If Enabled) | Variable | Default | Description | | --- | --- | --- | | `STELLAR_NETWORK_TYPE` | _Required_ | Must be set to `rpc` for SEP-45. This enables RPC mode for the Anchor Platform. | | `STELLAR_NETWORK_RPC_URL` | _Required_ | The URL of the Stellar RPC server used to simulate transactions. You can use a public provider or run your own. A list of public providers can be found [here][rpc-providers]. | | `SEP45_ENABLED` | `false` | Set to `true` to enable SEP-45 authentication | | `SEP45_HOME_DOMAINS` | _Required_ | List of home domains (comma-separated). Supports wildcard patterns like `*.stellar.org`. The `home_domain` must match the host where your `stellar.toml` file is served. | | `SEP45_WEB_AUTH_CONTRACT_ID` | _Required_ | The contract ID of the SEP-45 contract. This contract must implement the `web_auth_verify` function as described in the [SEP-45 specification][sep-45]. | | `SECRET_SEP10_SIGNING_SEED` | _Required_ | The private key corresponding to the `SIGNING_KEY` in your `stellar.toml` file. SEP-45 uses the same signing key as SEP-10. Used to sign authentication challenges. | | `SECRET_SEP45_JWT_SECRET` | _Required_ | The encryption key used to sign and verify JWT tokens issued to authenticated wallets. | :::important - **RPC Requirement**: SEP-45 requires Stellar RPC to simulate transactions. You must set `STELLAR_NETWORK_TYPE=rpc` and provide a valid `STELLAR_NETWORK_RPC_URL`. - **Contract ID**: The `SEP45_WEB_AUTH_CONTRACT_ID` must match the contract deployed at the address specified in your `stellar.toml` file's `WEB_AUTH_CONTRACT_ID` field. - **Signing Key**: The `SIGNING_KEY` in your `stellar.toml` file must be the public key derived from `SECRET_SEP10_SIGNING_SEED`. ::: ### Optional Configuration ```bash # dev.env # Optional: Specify web_auth_domain (default: first home_domain if only one is specified) SEP45_WEB_AUTH_DOMAIN=localhost:8080 # Optional: Challenge timeout in seconds (default: 900) SEP45_AUTH_TIMEOUT=900 # Optional: JWT token timeout in seconds (default: 86400 = 24 hours) SEP45_JWT_TIMEOUT=86400 ``` | Variable | Default | Description | | --- | --- | --- | | `SEP45_WEB_AUTH_DOMAIN` | First `home_domain` if only one is specified, otherwise empty | The `web_auth_domain` property used in SEP-45 responses. Required if you have multiple `home_domains` or use wildcard patterns. Must match the host of the SEP server. | | `SEP45_AUTH_TIMEOUT` | `900` | Time in seconds that a challenge remains valid. Clients must sign and submit the authorization entries within this window. | | `SEP45_JWT_TIMEOUT` | `86400` | Time in seconds that an issued JWT token remains valid. After expiration, clients must re-authenticate. | :::tip **Multiple Home Domains**: If you specify multiple `home_domains` (e.g., `ap.stellar.org,*.sdp.stellar.org`), you must also set `SEP45_WEB_AUTH_DOMAIN` to specify which domain hosts the authentication endpoint. ::: ## Configure stellar.toml Update your `stellar.toml` file to advertise SEP-45 support. Wallets discover your authentication endpoint through this file. ```toml # dev.stellar.toml ACCOUNTS = ["add your public keys for your distribution accounts here"] SIGNING_KEY = "add your signing key here (public key from SECRET_SEP10_SIGNING_SEED)" WEB_AUTH_FOR_CONTRACTS_ENDPOINT = "http://localhost:8080/sep45/auth" WEB_AUTH_CONTRACT_ID = "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX" ``` These fields should match the configuration options set in the [Enable SEP-45](#enable-sep-45) section above. `WEB_AUTH_FOR_CONTRACTS_ENDPOINT` - The URL where the authentication service is running. This is the URL that clients will use to authenticate with the anchor. The endpoint must support: - `GET ` - Request authorization entries - `POST ` - Exchange signed authorization entries for session JWT `WEB_AUTH_CONTRACT_ID` - The contract ID of the SEP-45 contract. This is the contract that will be used to construct the challenge. The contract must implement the `web_auth_verify` function as described in the [SEP-45 specification][sep-45]. This should match `SEP45_WEB_AUTH_CONTRACT_ID`. :::important - **`SIGNING_KEY`**: Must be the public key derived from `SECRET_SEP10_SIGNING_SEED` - **`WEB_AUTH_FOR_CONTRACTS_ENDPOINT`**: Use `https://` in production. The path `/sep45/auth` is the standard SEP-45 endpoint. - **Host Matching**: The host in `WEB_AUTH_FOR_CONTRACTS_ENDPOINT` should match one of your `SEP45_HOME_DOMAINS` (or the `SEP45_WEB_AUTH_DOMAIN` if specified). ::: [sep1-ap]: ../sep1/README.mdx [sep-45]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0045.md [sep-45-contract]: https://github.com/stellar/anchor-platform/tree/main/soroban/contracts/web-auth [rpc-providers]: /docs/data/apis/rpc/providers --- ## Programmatic Deposits and Withdrawals (SEP-6) SEP-6 allows for a means by which wallets and/or exchanges interact with an anchor on behalf of users, never requiring the user to directly interact with the on & off-ramp. --- ## Configuration(Sep6) To enable SEP-6 deposits and withdraws, the Anchor Platform must be configured to do the following: - Provide the necessary service URLs for SEP-6, 12, & 38 endpoints in the `stellar.toml` file - Provide information about the on & off-chain assets, as well as the payment rails, supported by your business via SEP-6 and SEP-38 `/info` endpoints - Support the endpoints and callbacks required to request KYC information and provide exchange rates ## Enable Programmatic Deposits & Withdrawals Add the following variables to your environment file. ```bash # dev.env SEP6_ENABLED=true SEP12_ENABLED=true SEP38_ENABLED=true ``` ### Modify a Stellar Info File Let's modify the `stellar.toml` file created [earlier][sep1-ap]. Wallets need to know that SEP-6 functionality is supported by your business, and they also need to know all the Stellar assets you support. ```toml # dev.stellar.toml ACCOUNTS = ["add your public keys for your distribution accounts here"] SIGNING_KEY = "add your signing key here" NETWORK_PASSPHRASE = "Test SDF Network ; September 2015" TRANSFER_SERVER = "http://localhost:8080/sep6" WEB_AUTH_ENDPOINT = "http://localhost:8080/auth" KYC_SERVER = "http://localhost:8080/sep12" ANCHOR_QUOTE_SERVER = "http://localhost:8080/sep38" # Add support for USDC [[CURRENCIES]] code = "USDC" issuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" status = "test" is_asset_anchored = false desc = "USD Coin issued by Circle" [DOCUMENTATION] ORG_NAME = "Your organization" ORG_URL = "Your website" ORG_DESCRIPTION = "A description of your organization" ``` Note that you will need to create another file for your production deployment that uses the public network's passphrase, your production service URLs, your Mainnet distribution accounts and signing key, as well as the Mainnet issuing accounts of the assets your service utilizes. ### Modify the Assets Configuration File Now you're ready to specify the following in your `dev.assets.yaml` file, and change the values depending on your use case. This example asset file enables support for Circle's USDC and a fiat USD to deposit from and withdraw to. The methods specified in the `sep38` sections are methods that will be exposed by the SEP-38 [`GET /info`][sep38] endpoint. The methods specified in the `deposit` and `withdraw` sections are the methods that will be exposed by the SEP-6 [`GET /info`][sep-6] endpoint. The methods listed should match the methods defined in the SEP-38 section of the file. Also note that fiat assets, those with the `schema: iso4217`, do not need the `sep6_enabled`, `deposit`, or `withdraw` configuration objects specified. In the same way, Stellar assets, those with `schema: stellar`, do not need the `sep38.sell_delivery_methods` or `sep38.buy_delivery_methods` configuration objects specified. ```yaml items: - id: stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 distribution_account: GBLSAHONJRODSFTLOV225NZR4LHICH63RIFQTQN37L5CRTR2IMQ5UEK7 significant_decimals: 2 sep6: enabled: true deposit: enabled: true min_amount: 0 max_amount: 10 methods: - ACH withdraw: enabled: true min_amount: 0 max_amount: 10 methods: - ACH sep38: enabled: true exchangeable_assets: - iso4217:USD - id: iso4217:USD significant_decimals: 2 sep38: enabled: true exchangeable_assets: - stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 buy_delivery_methods: - name: ACH description: ACH debits for US bank accounts sell_delivery_methods: - name: ACH description: ACH credit for US bank accounts ``` ### Managing Distribution Accounts Note that the example above lists a `distribution_account` attribute for the USDC entry. If specified, this account will be provided along with a randomly generated and unique-per-transaction memo to clients as the address to send funds to for withdrawal transactions. The transaction's memo is how you or the Anchor Platform will match the funds received with a transaction record in the Anchor Platform's database. If you do not have your own Stellar account and instead use a third party that provides you a Stellar account and memo so they can receive funds on your behalf, such as an exchange or custodian, you should omit the `distribution_account` field from your assets config file. Instead, you'll need to provide the Stellar account and memo you'd like to use to receive funds through a request to the Anchor Platform's [`request_onchain_funds`][request-onchain-funds] on a per-transaction basis. To configure the Anchor Platform to expect the Stellar account and memo to be provided via API instead of configured via the assets file, specify the following environment variable. ```bash # dev.env SEP6_DEPOSIT_INFO_GENERATOR_TYPE=none ``` ### Enable Callbacks to the Business Server Businesses need to collect and validate KYC information on the customers they're facilitating transactions for. Clients ask your business what KYC information needs to be collected and sends that information via the SEP-12 KYC API hosted by the Anchor Platform, but the Anchor Platform never stores personally-identifiable information (PII). Instead, it forwards requests from clients to the business server, and returns the business' responses back to the client, acting as a proxy server. Additionally, businesses need to provide clients with a [Rates][get-rates-api] API to check the exchange rates they're offering between the onchain and offchain assets supported by the business. If the rate is competitive, clients also need to be able to request a commitment to the rate currently being offered from business for a short period of time. Similarly to the KYC API, the Anchor Platform makes requests to your business server to fetch exchange rates and quotes and returns them to clients. To enable these requests to your business server, first you'll need to add your business server to the docker compose file. Then, to support requests to your business server from the Anchor Platform, you need to enable callbacks. ```bash # dev.env CALLBACK_API_BASE_URL=http://business-server:3000/callbacks CALLBACK_API_AUTH_TYPE=jwt CALLBACK_API_AUTH_JWT_EXPIRATION_MILLISECONDS=30000 CALLBACK_API_AUTH_JWT_HTTP_HEADER=Authorization SECRET_CALLBACK_API_AUTH_SECRET="a secret used to sign JWTs" ``` The above tells the Anchor Platform to include a [JWT][how-to-use-jwt], signed with the configured secret, in the `Authorization` header of requests made to `/callbacks/` so your server can authenticate the Anchor Platform before processing requests. See the [KYC API][platform-api-kyc] and [Rates API][get-rates-api] for details on the endpoints that must be implemented on your business server. ### Additional Optional Configuration `more_info_url` is an optional URL provided by your business server for wallet applications to display information about previously initiated transactions. This URL is typically used by wallets in their transaction history views, and your business can specify the information to be displayed about the transaction. ```bash # dev.env SEP6_MORE_INFO_URL_BASE_URL=http://example.com SECRET_SEP6_MORE_INFO_URL_JWT_SECRET="your encryption key shared with your business server" ``` Businesses can set a deadline for user actions on transactions using the `user_action_required_by` field. For examples, see [JSON-RPC Methods][json-rpc-methods]. In addition, the `initial_user_deadline_seconds` parameter sets a default time (in seconds) a user has to act before the transaction moves into the `EXPIRED` status. ```bash # dev.env SEP6_INITIAL_USER_DEADLINE_SECONDS=1209600 ``` ## Test With the Demo Wallet Wallets should now be able to discover, authenticate, and initiate transactions with your service! Your project and source files should now look something like this. ``` ├── dev.env ├── docker-compose.yaml ├── config │ ├── dev.assets.yaml │ ├── dev.stellar.toml ``` Your environment should now look something like the following. ```bash # dev.env ASSETS_TYPE=file ASSETS_VALUE=/home/dev.assets.yaml SEP1_ENABLED=true SEP1_TOML_TYPE=file SEP1_TOML_VALUE=/home/dev.stellar.toml SEP6_ENABLED=true SEP6_DEPOSIT_INFO_GENERATOR_TYPE=none SEP6_MORE_INFO_URL_BASE_URL=http://example.com SECRET_SEP6_MORE_INFO_URL_JWT_SECRET="your encryption key shared with your business server" SEP10_ENABLED=true SEP10_HOME_DOMAIN=localhost:8080 SECRET_SEP10_SIGNING_SEED="a Stellar private key" SECRET_SEP10_JWT_SECRET="a secret used to sign JWTs" SEP12_ENABLED=true SEP38_ENABLED=true CALLBACK_API_BASE_URL=http://business-server:3000/callbacks CALLBACK_API_AUTH_TYPE=jwt CALLBACK_API_AUTH_JWT_EXPIRATION_MILLISECONDS=30000 CALLBACK_API_AUTH_JWT_HTTP_HEADER=Authorization SECRET_CALLBACK_API_AUTH_SECRET="a secret used to sign JWTs" ``` To test this out, go to the [Stellar Demo Wallet][stellar-demo-wallet]. Initiate a deposit transaction by doing the following: - Create a new keypair - Click the "Add Asset" button and enter - the code of the Stellar asset on your `stellar.toml` file - your home domain, `localhost:8080` - Select the dropdown and click "SEP-6 Deposit", then click "Start" The demo wallet should be able to find your `stellar.toml` file, authenticate using the Stellar keypair you just created, and initiate a transaction. [sep-6]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md [sep1-ap]: ../sep1/README.mdx [stellar-demo-wallet]: https://demo-wallet.stellar.org/ [get-rates-api]: ../../api-reference/callbacks/get-rates.api.mdx [sep38]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md [platform-api-kyc]: ../../api-reference/callbacks/get-customer.api.mdx [request-onchain-funds]: ../../api-reference/platform/rpc/methods/request_onchain_funds.mdx [how-to-use-jwt]: ../sep24/faq.mdx [json-rpc-methods]: ../../api-reference/platform/rpc/methods/README.mdx --- ## Getting Started(Sep6) This guide will walk you through configuring and integration with the Anchor Platform for the purpose of build an on & off-ramp service compatible with [SEP-6][sep-6], the ecosystem's standardized protocol for programmatic deposit and withdrawals. By leveraging the Anchor Platform's support for SEP-6, businesses make their own on & off-ramp service available as an in-app experience through Stellar-based applications such as wallets and exchanges, extending their reach and connecting with users through the applications they already use. Before continuing with this section, make sure that you have already [installed][installation-ap] the Anchor Platform, and configured necessary features, required by SEP-6: [SEP-1 (Stellar Info File)][sep1-ap], [SEP-10 (Stellar Authentication)][sep10-ap] and [SEP-45 (Stellar Web Authentication for contract account)][sep45-ap]. ## The Basic User Experience The complete customer experience for a deposit or withdrawal using SEP-6 is as follows: 1. The customer opens the SEP-6 wallet application of their choice 2. The customer selects an asset to deposit and the wallet finds an anchor (clients could also choose the specific anchor) 3. Once the wallet authenticates with the anchor, the customer begins entering their KYC and transaction information requested by the anchor 4. The wallet provides instructions, and the customer deposits real fiat currency with the anchor (such as bank transfer) 5. Once the wallet receives the deposit, the customer receives the tokenized asset on the Stellar network from the anchor's distribution account The customer can then use the digital asset on the Stellar network for remittance, payments, trading, store of value, or another use case not listed here. At some later date, the customer could decide to withdraw their assets from the Stellar network, which would look something like this: 1. The customer opens their wallet application 2. The customer selects the asset for withdrawal and wallet finds the anchor 3. After authenticating with the anchor, the customer can enter their transaction information and any additional KYC information that wasn't already collected 4. After asking for customer approval, the wallet sends the specified amount of the customer's asset balance to the anchor's distribution account on Stellar 5. Once the anchor receives the payment, the customer receives the withdrawn funds via any method supported by the anchor (such as bank transfer) [sep-6]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md [installation-ap]: ../../admin-guide/getting-started.mdx [sep1-ap]: ../sep1/README.mdx [sep10-ap]: ../sep10/README.mdx [sep45-ap]: ../sep45/README.mdx --- ## Integration(Sep6) One of the main points of interaction with the Anchor Platform is notifying the Platform about events related to transactions. In general, you will want to provide updates for the following events. - Your business requires the user to submit KYC information to process a transaction - Your business updated the in/out/fee amounts for a transaction - Your business is ready to receive funds from the user - Your business has received funds from the user - Your business has sent funds to the user - Your business has a processed a refund for the user's transaction - Your business experienced an unexpected error This is done by making JSON-RPC requests to the Platform API's endpoint. JSON-RPC requests allow you to update the status of the transaction. To move the transaction to a specific status, it's necessary to make a corresponding JSON-RPC request and pass data that is required by the RPC method. The Anchor Platform JSON-RPC API is designed to notify the platform about changes in the status of the transaction. Given that, the API will be called every time a user or the anchor takes any action that progresses the transaction status in the flow. Communication from the Anchor Platform about transaction updates, customer updates, and quote creation is handled through the event service. This is an optional feature that needs to be configured separately from the SEP-6 integration. For more information, see [Event Handling][event-handling]. You can find out more about transaction flow and statuses in the [SEP-6 protocol document][sep-6]. ## Callbacks The Anchor Platform relies on the business server to provide and store information about customers and quotes. ### Customer Information The Anchor Platform does not store customer information. Instead, it forwards all SEP-12 customer requests to the business server. The business server is responsible for storing and managing this information. Therefore, your business server must implement the [customer APIs][customer-callback] to handle KYC updates. ### Quotes and Fees To support the exchange of non-equivalent assets, the Anchor Platform exposes a SEP-38 compliant API to provide quotes for the exchange. The quote API is used to provide the user with the expected amount of the asset they will receive in exchange for the asset they are sending. The quote API is also used to provide the user with the expected fees for the transaction. Therefore, your business server must implement the [rate API][rate-callback] to provide quotes to the Anchor Platform. ## Securing Platform API ### Using API Key To enable API key authentication, modify your `dev.env` file: ```bash # dev.env PLATFORM_API_AUTH_TYPE=api_key # Will be used as API key SECRET_PLATFORM_API_AUTH_SECRET="your API key that business server will use" ``` Once enabled, all requests must include a valid `X-Api-Key` header, set to the configured API key. ### Using JWT ## Making JSON-RPC Requests ### JSON-RPC Request ### JSON-RPC Response ### Error Codes ## Updating Deposit (Exchange) Transaction Via JSON-RPC SEP-6 deposit flow diagram defines sequences/rules of the transaction's status transition and a set of JSON-RPC method that should be called to change that status. You can't define the status you want to set for a specific transaction in your requests. Each JSON-RPC method defines data structures that it expects in request. If request doesn't contain a required attributes, the Anchor Platform will return and error and won't change status of the transaction. The deposit exchange flow is the same as the deposit flow, except the amounts will not need to be recalculated when requesting offchain funds, if the user has provided a firm quote from the anchor. [![sep6 deposit flow](/assets/ap/sep6-deposit-flow-diagram.png)](/assets/ap/sep6-deposit-flow-diagram.png) :::tip Statuses in green are mandatory and define the shortest way. Statuses in yellow are optional and can be skipped. Statuses in red mean the transaction is in an error status or it has expired. ::: ### Verifying KYC Information Although Anchor Platform does not require a customer to have their KYC information collected before initiating a deposit, your business may want to collect this information before the customer makes a transfer. By listening to transaction created events, or by polling the [`GET /transactions`][get-transactions] endpoint, you can require determine if a transaction requires the customer to update their information. The required SEP-9 fields can be communicated to the user by returning a `NEEDS_INFO` status with the required fields in the `fields` attribute. After the user has submitted their KYC information, call the `notify_customer_info_updated` JSON-RPC method againto update the transaction status. Additionally, call this method whenever the SEP-12 status for a customer changes, such as when the customer's information is being validated and the status changes from `NEEDS_INFO` to `PROCESSING`. This ensures that any clients configured with a callback URL are notified of the latest customer status, allowing the client to prompt the user to update their information. ```json // notify-customer-info-updated.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_customer_info_updated", "params": { "transaction_id": "", "message": "Customer info updated", "customer_id": "45f8884d-d6e1-477f-a680-503179263359", "customer_type": "sep6-deposit" // or sep6-withdrawal } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh notify-customer-info-updated.json ``` ### Ready to Receive Funds After the user has submitted their KYC information, the anchor can notify the Platform that they are ready to receive funds. The anchor should use the `request_offchain_funds` RPC to provide the final amounts to the user. To do so, make the following JSON-RPC request. ```json // request-offchain-funds.json [ { "id": 1, "jsonrpc": "2.0", "method": "request_offchain_funds", "params": { "transaction_id": "", "message": "Request offchain funds", "amount_in": { "amount": 10, "asset": "iso4217:USD" }, "amount_out": { "amount": 9, "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, "fee_details": { "total": 1, "asset": "iso4217:USD" }, "amount_expected": { "amount": 10 }, "instructions": { "organization.bank_number": { "value": "123456789", "description": "US Bank routing number" }, "organization.bank_account_number": { "value": "123456789", "description": "US Bank account number" } } } } ] ``` - `amount_in` is the amount the user has to send to the business. - `amount_out` is the amount the user will receive. - `fee_details` is the total amount of fees collected by the business. - `asset` is part of the `amount_x` field and is in a SEP-38 format. In this example, it's set to USD, assuming the user made a bank transfer to the system using USD. - `instructions` is the set of SEP-9 standard fields that user should use to send funds to the business. In this example, the user should send funds to the bank account with the routing number `123456789` and account number `123456789`. Information about amounts (in/out/fee) is required if you want to move the transaction to the `pending_user_transfer_start` status. To execute this, you need to run: ```bash ./call-json-rpc.sh request-offchain-funds.json ``` :::caution For exchange deposits with a firm quote (the request is associated with a `quote_id`), no amounts should not be provided. ::: ### Funds Received If offchain funds were received, you'll want to provide updated transaction information. ```json // offchain-funds-received.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_received", "params": { "transaction_id": "", "message": "Offchain funds received", "funds_received_at": "2023-07-04T12:34:56Z", "external_transaction_id": "7...9", "amount_in": { "amount": 10 }, "amount_out": { "amount": 9 }, "fee_details": { "total": 1 }, "amount_expected": { "amount": 10 } } } ] ``` - `funds_received_at` is the date and time of receiving funds. - `external_transaction_id` is the ID of transaction on external network. The amount fields are optional. If skipped, the values prior to this request will be used. To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-received.json ``` ### Waiting For User Funds In the real world, the transfer confirmation process may take time. In such cases, transactions should be set to a new status indicating that the confirmation of the transfer has been received but the funds themselves have not been received yet. ```json // offchain-funds-sent.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_sent", "params": { "transaction_id": "", "message": "Offchain funds sent", "funds_received_at": "2023-07-04T12:34:56Z", "external_transaction_id": "7...9" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-sent.json ``` ### Sending Onchain Funds Next, send a transaction on the Stellar network to fulfill the user deposit. After the Stellar transaction has been submitted, it's necessary to send the `notify_onchain_funds_sent` JSON-RPC request to notify a user that the funds were successfully sent. ```json // onchain-funds-sent.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_onchain_funds_sent", "params": { "transaction_id": "", "message": "Onchain funds sent", "stellar_transaction_id": "7...9" } } ] ``` - `stellar_transaction_id` is the transaction id on Stellar network of the transfer. To execute this, you need to run: ```bash ./call-json-rpc.sh onchain-funds-sent.json ``` After this JSON-RPC request, the transaction will be transferred to the `completed` status. ### Pending Trust This status has to be set if a payment requires an asset trustline that wasn't configured by the user. There are two ways of how the transaction may be moved to the `pending_trust` status. The first one is when the business server detects that the trustline isn't configured. The second one is when the business itself detects that the trustline is missing and wants to notify the user that it has to be configured. To move the transaction to the `pending_trust` status, make the following JSON-RPC request. ```json // request-trust.json [ { "id": 1, "jsonrpc": "2.0", "method": "request_trust", "params": { "transaction_id": "", "message": "Asset trustine not configured" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh request-trust.json ``` :::info The payment processing system periodically checks if the trustline was configured. If it was, it will automatically send a payment and change the status of the transaction to `pending_stellar`. ::: ### Trust Set This status has to be set if the business has detected that the trustline was or wasn't configured by user. ```json // trust-set.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_trust_set", "params": { "transaction_id": "", "message": "Asset trustine set", "success": "true" } } ] ``` - `success` flag which defines if trustline was or wasn't configured by user To execute this, you need to run: ```bash ./call-json-rpc.sh trust-set.json ``` :::info Depending on the `success` flag, the status of the transaction will be changed to `pending_stellar` if the trustline was set, or to `pending_anchor` if it wasn't. ::: ### Refund Sent Sometimes, funds need to be sent back to the user (refund). You can refund the whole sum (full refund) or do a set of partial refunds back to the `source_account` using the `refund_memo` and `refund_memo_type` associated with the transaction if present. Also, if user sent more money than expected, you can refund a part of the sum back to the user and send the rest as onchain funds. ```json // refund-sent.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_refund_sent", "params": { "transaction_id": "", "message": "Refund sent", "refund": { "id": "1c186184-09ee-486c-82a6-aa7a0ab1119c", "amount": { "amount": 10, "asset": "iso4217:USD" }, "fee_details": { "total": 1, "asset": "iso4217:USD" } } } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh refund-sent.json ``` :::info If a sum of refunds is less than `amount_in`, the status of the transaction will be set to `pending_anchor`. Only if the sum of refunds is equal to `amount_in`, the status of the transaction will be set to `refunded`. ::: ### Refund Pending This is similar to [Refund Sent](#refund-sent), but it handles the case when a refund has been submitted to external network but is not yet confirmed. The status of the transaction is set to `pending_external`. This is the status that will be set when waiting for Bitcoin or other external crypto network to complete a transaction, or when waiting for a bank transfer. ### Transaction Error If you encounter an unrecoverable error when processing the transaction, it's required to set the transaction status to `error`. You can use the message field to describe the error details. ```json // transaction-error.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_transaction_error", "params": { "transaction_id": "", "message": "Error occurred" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh transaction-error.json ``` :::tip If a user has made a transfer, you should do a transaction recovery, and then you can retry processing the transaction or initiate a refund. ::: ### Expired Transaction Your business may want to handle abandoned transactions by expiring those have remained inactive for a certain period. To achieve this, check the transaction status using the `GET /transactions` endpoint and sort the results by the `user_action_required_by` timestamp. If the timestamp has passed, manually execute the appropriate logic, such as expiring the transaction or initiating an auto-refund, based on the transaction's current status. ```json // transaction-expired.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_transaction_expired", "params": { "transaction_id": "", "message": "Transaction expired" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh transaction-expired.json ``` :::tip This JSON-RPC method can't be used after the user has made a transfer. ::: ### Transaction Recovery Transaction status can be changed from `error/expired` to `pending_anchor`. After recovery, you can refund the received assets or proceed with processing of the transaction. To recover a transaction, make the following JSON-RPC request. ```json // transaction-recovery.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_transaction_recovery", "params": { "transaction_id": "", "message": "Transaction recovered" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh transaction-recovery.json ``` ## Updating Withdrawal (Exchange) Transaction Via JSON-RPC The SEP-6 withdrawal flow diagram defines the sequence/rules of the transaction's status transition. You can't define the status you want to set for a specific transaction in your requests. Each JSON-RPC method defines data structures that it expects in request. If request doesn't contain a required attributes, the Anchor Platform will return and error and won't change status of the transaction. The withdrawal exchange flow is the same as the withdrawal flow, except the amounts will not need to be recalculated when requesting onchain funds, if the user has provided a firm quote from the anchor. [![sep6 withdrawal flow](/assets/ap/sep6-withdrawal-flow-diagram.png)](/assets/ap/sep6-withdrawal-flow-diagram.png) :::tip Statuses in green are mandatory and define the shortest way. Statuses in yellow are optional and can be skipped. Statuses in red mean the transaction is in an error status or it has expired. ::: Once the withdrawal flow is finished, implementing the withdrawal is straightforward. Some parts of the flow are similar and can be reused. The starting point both for withdrawal and for deposit is the same. ### Ready to Receive Funds Similarly to deposit, the step after KYC has been collected is to notify the user that the anchor is ready to receive funds. However, as your service will be receiving transactions over the Stellar network, the RPC request will be different. The anchor should use the `request_onchain_funds` RPC to provide the final amounts to the user. To do so, make the following JSON-RPC request. ```json // request-onchain-funds.json [ { "id": 1, "jsonrpc": "2.0", "method": "request_onchain_funds", "params": { "transaction_id": "", "message": "Request onchain funds", "amount_in": { "amount": 10, "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, "amount_out": { "amount": 9, "asset": "iso4217:USD" }, "fee_details": { "total": 1, "asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, "amount_expected": { "amount": 10 }, "destination_account": "GD...G", "memo": "12345", "memo_type": "id" } } ] ``` - `amount_in` is the amount the user has to send to the business. - `amount_out` is the amount the user will receive. - `fee_details` is the total amount of fees collected by the business. - `asset` is part of the `amount_x` field and is in a SEP-38 format. In this example, it's set to USD, assuming the user made a bank transfer to the system using USD. - `memo` is the memo the user should use when sending their onchain funds to the anchor. - `memo_type` is the memo type the user should use when sending their onchain funds to the anchor. - `destination_account` is the account the user should send the funds to. To execute this, you need to run: ```bash ./call-json-rpc.sh request-onchain-funds.json ``` :::caution For exchange withdrawals with a firm quote (the request is associated with a `quote_id`), no amounts should not be provided. ::: :::tip Setting `memo`, `memo_type`, and `destination_account` is optional. If integration with a third-party custodian is enabled, the Anchor Platform can generate `memo`, `memo_type`, and `destination_address` if a corresponding `deposit_info_generator_type` is chosen. Also, you can provide `memo` and `memo_type` to the request as shown above. Note that the memo must be unique, this is what helps to associate Stellar transactions with SEP transactions. If your business manages the assets, the Anchor Platform can generate memos for you. When the status is changed to `pending_user_transfer_start`, the Anchor Platform sets the `memo` and `memo_type` automatically (only if it's not included in the request). ::: :::note The Stellar account that will be used to receive funds should be configured. ::: ### Funds Received If onchain funds were received, you need to provide amounts and change the status of the transaction to `pending_anchor`. ```json // onchain-funds-received.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_onchain_funds_received", "params": { "transaction_id": "", "message": "Onchain funds received", "stellar_transaction_id": "7...9", "amount_in": { "amount": 10 }, "amount_out": { "amount": 9 }, "fee_details": { "total": 1 } } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh onchain-funds-received.json ``` :::tip This method will be called by the Stellar payment observer when it detects that onchain funds have been received. ::: ### Amount Updated If onchain funds were received, but for some reason the `amount_in` differs from specified in the interactive flow (`amount_expected`), you can update `amount_out` and `fee_details` to make them correspond to the actual `amount_in`. The status of the transaction in this case won't be changed and will be equal to `pending_anchor`. ```json // amounts-updated.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_amounts_updated", "params": { "transaction_id": "", "message": "Amounts updated", "amount_out": { "amount": 9 }, "fee_details": { "total": 1 } } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh amounts-updated.json ``` :::note Only `amount_out` and `fee_details` can be updated using this JSON-RPC request, and you don't need to specify the assets of the amounts. ::: ### Offchain Funds Available You can move transaction status to `pending_user_transfer_complete` if offchain funds were sent, and if it's ready for the user / recipient to pick it up. ```json // offchain-funds-available.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_available", "params": { "transaction_id": "", "message": "Offchain funds available", "external_transaction_id": "a...c" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-available.json ``` ### Offchain Funds Pending Another option is to move the transaction's status to `pending_external`. This status means that the payment has been submitted to an external network, but is not yet confirmed. ```json // offchain-funds-pending.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_pending", "params": { "transaction_id": "", "message": "Offchain funds pending", "external_transaction_id": "a...c" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-pending.json ``` ### Offchain Funds Sent To complete the transaction and change its status to `completed`, you need to make the `notify_offchain_funds_sent` JSON-RPC request. ```json // offchain-funds-sent.json [ { "id": 1, "jsonrpc": "2.0", "method": "notify_offchain_funds_sent", "params": { "transaction_id": "", "message": "Offchain funds sent", "funds_sent_at": "2023-07-04T12:34:56Z", "external_transaction_id": "a...c" } } ] ``` To execute this, you need to run: ```bash ./call-json-rpc.sh offchain-funds-sent.json ``` ### Refund Sent The refund logic works in the same way as for the deposit flow. For more details, see [Refund Sent](#refund-sent) of the deposit flow. ### Transaction Error Works in the same manner as for the deposit flow. For more details, see [Transaction Error](#transaction-error) of the deposit flow. ### Expired Transaction Works in the same manner as for the deposit flow. For more details, see [Expired Transaction](#expired-transaction) of the deposit flow. ### Transaction Recovery Works in the same manner as for the deposit flow. For more details, see [Transaction Recovery](#transaction-recovery) of the deposit flow. ## Tracking Stellar Transactions [sep-6]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md [event-handling]: ../../admin-guide/events/README.mdx [customer-callback]: ../../api-reference/callbacks/README.mdx [rate-callback]: ../../api-reference/callbacks/README.mdx [get-transactions]: ../../api-reference/platform/transactions/get-transactions.api.mdx --- ## Stellar Disbursement Platform Introduction # Stellar Disbursement Platform The Stellar Disbursement Platform (SDP) is a tool built for organizations to make bulk payments to a group of recipients over the Stellar network. This is an open-source project that is built on top of the Stellar network, and the code can be found on the following repositories: - [stellar/stellar-disbursement-platform-backend](https://github.com/stellar/stellar-disbursement-platform-backend): This repository contains the backend and infrastructure code for the Stellar Disbursement Platform. - [stellar/stellar-disbursement-platform-frontend](https://github.com/stellar/stellar-disbursement-platform-frontend): This repository contains the web frontend code for the Stellar Disbursement Platform. - [stellar/helm-charts](https://github.com/stellar/helm-charts/tree/main/charts/stellar-disbursement-platform): This repository contains the Helm chart for deploying the Stellar Disbursement Platform using Kubernetes. In this section, you'll find an [Admin Guide](./admin-guide/overview.mdx) that will teach you how to run the Stellar Disbursement Platform as well as an [API Reference](./api-reference/admin.tag.mdx). --- ## Advanced Configuration In this guide, you will learn about advanced configuration options for the Stellar Disbursement Platform (SDP). These configurations allow you to tailor the SDP to meet specific requirements, such as multi-tenancy, network selection, and performance tuning. ## Testnet to Mainnet Configuration Upon provisioning a new SDP instance, it is configured to operate either in Mainnet or Testnet mode based on the environment variables set during setup. Most users will start with Testnet for development and testing purposes before transitioning to Mainnet for production use. :::caution An SDP instance is designed to operate on either Testnet or Mainnet. Switching between these networks on an existing instance is not supported and may lead to unexpected behavior. If you need to change the network, it is recommended to set up a new SDP instance with the desired configuration. ::: Once you validated your setup on Testnet, you can deploy a new instance configured for Mainnet by setting the appropriate environment variables during the provisioning process. ### Environment Variables When switching from Testnet to Mainnet, you need to update the following environment variables for each service to point to the public network resources. #### SDP Core Service | Variable | Testnet Value | Mainnet Value | Description | | :-- | :-- | :-- | :-- | | `NETWORK_PASSPHRASE` | `Test SDF Network ; September 2015` | `Public Global Stellar Network ; September 2015` | The passphrase for the Stellar network. | | `HORIZON_URL` | `https://horizon-testnet.stellar.org` | `https://horizon.stellar.org` | The URL of the Horizon server. | | `DISABLE_MFA` | x | `false` | Disables Multi-Factor Authentication. **Must be `false` for Mainnet.** | #### Transaction Submission Service (TSS) | Variable | Testnet Value | Mainnet Value | Description | | :-- | :-- | :-- | :-- | | `NETWORK_PASSPHRASE` | `Test SDF Network ; September 2015` | `Public Global Stellar Network ; September 2015` | The passphrase for the Stellar network. | | `HORIZON_URL` | `https://horizon-testnet.stellar.org` | `https://horizon.stellar.org` | The URL of the Horizon server. | #### Dashboard | Variable | Testnet Value | Mainnet Value | Description | | :-- | :-- | :-- | :-- | | `HORIZON_URL` | `https://horizon-testnet.stellar.org` | `https://horizon.stellar.org` | The URL of the Horizon server used by the frontend. | | `STELLAR_EXPERT_URL` | `https://stellar.expert/explorer/testnet` | `https://stellar.expert/explorer/public` | The URL for the Stellar Expert explorer. | ### Critical Considerations Before deploying to Mainnet, you must address the following critical requirements to ensure your instance operates correctly. You **must** generate a new, secure keypair for your Mainnet Distribution Account. Do not reuse Testnet keys. - **Generate Keys**: Create a new keypair and set `DISTRIBUTION_PUBLIC_KEY` and `DISTRIBUTION_SEED`. - **Generate Encryption Passphrase**: You should generate new encryption passphrases for your tenant distribution accounts and channel accounts by setting `DISTRIBUTION_ACCOUNT_ENCRYPTION_PASSPHRASE` and `CHANNEL_ACCOUNTS_ENCRYPTION_PASSPHRASE`. - **Fund the Account**: The Distribution Account requires an initial balance of XLM to function. It is responsible for: 1. **Creating Channel Accounts**: The system will automatically create `NUM_CHANNEL_ACCOUNTS` (default: 2). 2. **Bootstrapping Tenants**: When a new tenant is provisioned, the system transfers a bootstrap amount of XLM from the Distribution Account to the Tenant's Distribution Account. This is controlled by `TENANT_XLM_BOOTSTRAP_AMOUNT` (default: 5 XLM). ### Configuration Methods You can configure the SDP for Mainnet using either Helm Charts (for Kubernetes deployments) or Docker Compose (for local or simple deployments). #### Helm Charts If you are deploying via Helm, the chart provides a global setting that automatically configures the necessary network parameters. In your `values.yaml` file, set `global.isPubnet` to `true`. This will automatically set the correct `NETWORK_PASSPHRASE`, `HORIZON_URL` and `STELLAR_EXPERT_URL` for all services. ```yaml global: # Set to true for Mainnet isPubnet: true ``` #### Docker Compose Update your `.env` file with the following values: ```bash # Network Configuration NETWORK_TYPE="pubnet" NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" HORIZON_URL="https://horizon.stellar.org" # Security DISABLE_MFA=false # Distribution Account (Mainnet Keys) DISTRIBUTION_PUBLIC_KEY="G..." DISTRIBUTION_SEED="S..." # Encryption Passphrases DISTRIBUTION_ACCOUNT_ENCRYPTION_PASSPHRASE="S..." CHANNEL_ACCOUNTS_ENCRYPTION_PASSPHRASE="S..." ``` ## Single Tenant to Multi-Tenant Configuration The Stellar Disbursement Platform (SDP) supports multi-tenancy, allowing a single instance to serve multiple organizations (tenants). Each tenant has its own isolated data, users, and distribution account (source of funds). ### Configuration To enable multi-tenancy, you must update your configuration to disable single-tenant mode and ensure the Admin API is accessible. 1. **Disable Single Tenant Mode**: Set the `SINGLE_TENANT_MODE` environment variable to `false`. 2. **Expose Admin Port**: Ensure the Admin API port (default `8003`) is exposed in the sdp backend service. ### Routing and Ingress The SDP identifies the tenant for each request using one of the following methods, in order of precedence: 1. **HTTP Header**: The `SDP-Tenant-Name` header. 2. **Subdomain**: The prefix of the hostname (e.g., `tenant1` in `tenant1.sdp.stellar.org`). #### HTTP Header You can explicitly specify the tenant by setting the `SDP-Tenant-Name` header in your HTTP requests. ```bash curl -H "SDP-Tenant-Name: tenant1" https://sdp.stellar.org/ ... ``` #### Subdomain Routing In a production environment, it is common to use subdomain routing. For example, `tenant1.sdp.stellar.org` and `tenant2.sdp.stellar.org` will both point to the same SDP instance. #### Helm Charts When deploying via Helm, you configure the wildcard domain using the `sdp.route.mtnDomain` value. This creates an Ingress rule that matches all subdomains. In your `values.yaml`: ```yaml sdp: route: # The wildcard domain for multi-tenancy mtnDomain: "*.sdp.stellar.org" ``` :::note Ensure your DNS provider has a wildcard A record (e.g., `*.sdp.stellar.org`) pointing to your Ingress Controller's Load Balancer IP. ::: #### Docker Compose (Local Development) For local development with Docker Compose you must map specific tenant subdomains to `127.0.0.1` in your machine's `/etc/hosts` file (For Windows users, the file is located at `C:\Windows\System32\drivers\etc\hosts`). **Example `/etc/hosts`:** ```text 127.0.0.1 localhost 127.0.0.1 sdp.local # Default/Admin domain 127.0.0.1 tenant1.sdp.local # First tenant 127.0.0.1 tenant2.sdp.local # Second tenant ``` ### Provisioning Tenants In multi-tenant mode, you provision new tenants using the Admin API (port `8003` by default). Each tenant will have its own isolated data. **Endpoint**: [POST /tenants](../api-reference/create-tenant.api.mdx) **Example Request:** ```bash curl --location 'http://localhost:8003/tenants' \ --header 'Content-Type: application/json' \ --header 'Authorization: Basic ' \ --data '{ "name": "tenant1", "organization_name": "Tenant One Organization", "base_url": "https://tenant1.sdp-api.stellar.org", "sdp_ui_base_url": "https://tenant1.sdp-dashboard.stellar.org", "owner_email": "owner@tenant1.com", "owner_first_name": "Jane", "owner_last_name": "Doe", "distribution_account_type": "DISTRIBUTION_ACCOUNT.STELLAR.DB_VAULT" }' ``` #### Multi-tenant Distribution Accounts This is by far the most important field, as it determines the source of funds (distribution account) for the tenant, as well as how the secret for this distribution account is stored. This is determined by the field `distribution_account_type` in the API call above. The possible values are described below: - `DISTRIBUTION_ACCOUNT.STELLAR.DB_VAULT` - **Platform**: Stellar - **Secret Storage Location**: Database, encrypted with `DISTRIBUTION_ACCOUNT_ENCRYPTION_PASSPHRASE` - **Assets Supported**: Any Stellar asset - **Key/Secret Isolation**: Segregated per tenant - **Appropriate for**: Multi-tenant and single-tenant - **How is it configured?**: The distribution account is randomly generated and funded during the provisioning process, and the secret is encrypted and safely stored in the database. The account is funded from the HOST distribution account by an amount defined in `TENANT_XLM_BOOTSTRAP_AMOUNT`. - `DISTRIBUTION_ACCOUNT.CIRCLE.DB_VAULT` - **Platform**: [Circle](https://www.circle.com) - **Secret Storage Location**: Database, encrypted with `DISTRIBUTION_ACCOUNT_ENCRYPTION_PASSPHRASE` - **Assets Supported**: [USDC](https://www.circle.com/en/usdc)/[EURC](https://www.circle.com/en/eurc) - **Key/Secret Isolation**: Segregated per tenant - **Appropriate for**: Multi-tenant and single-tenant - **How is it configured?**: The Circle API key is provided by the tenant themselves once they have access to the dashboard. The secret is encrypted and safely stored in the database. - 🔴 `DISTRIBUTION_ACCOUNT.STELLAR.ENV` - **Platform**: Stellar - **Secret Storage Location**: Environment variable `DISTRIBUTION_SEED` - **Assets Supported**: Any Stellar asset - **Key/Secret Isolation**: 🚨 Same distribution account as the HOST - **Appropriate for**: Single-tenant only - **How is it configured?**: The tenant will use the HOST account **as is**. The host is responsible for creating the account and configuring it with the `DISTRIBUTION_SEED` secret. :::warning Once a tenant is created, the `distribution_account_type` cannot be changed. If you wish to use a different distribution account type, you will need to create a new tenant. ::: ## Embedded Wallets Configuration Embedded Wallets allow receivers to receive disbursements without downloading a separate wallet application. Instead, the SDP creates lightweight, passkey-secured smart contract wallets on the Stellar network. This feature requires configuration across the SDP backend, Transaction Submission Service (TSS), and frontend dashboard. For a complete guide on using Embedded Wallets, see the [Embedded Wallets](./embedded-wallets) documentation. ### Overview To enable Embedded Wallets, you need to: 1. Make sure the SEP-10 account exists on the Stellar network by funding it 2. Configure the backend with the RPC endpoint 3. Configure TSS with the same RPC endpoint 4. Enable RPC features in the frontend dashboard :::caution[HTTPS Requirement] **The frontend dashboard MUST be served over HTTPS for Embedded Wallets to work.** Passkeys use the WebAuthn standard, which requires a secure context. ::: ### Backend (SDP Core Service) Configuration The following environment variables configure the SDP backend for Embedded Wallets: | Variable | Description | Default | Required | | :-- | :-- | :-- | :-- | | `ENABLE_EMBEDDED_WALLETS` | Enable embedded wallet features. Set to `true` to activate. | `false` | Yes | | `EMBEDDED_WALLETS_WASM_HASH` | The WASM hash of the deployed smart contract for embedded wallets. This is network-specific. | - | Yes (when enabled) | | `RPC_URL` | The URL of the Stellar RPC server. Required for interacting with smart contracts. | - | Yes (when enabled) | | `RPC_REQUEST_AUTH_HEADER_KEY` | The HTTP header name for authenticating requests to a protected RPC server (e.g., `X-API-Key`). Optional. | - | No | | `RPC_REQUEST_AUTH_HEADER_VALUE` | The value of the authentication header for protected RPC servers. Optional. | - | No | | `ENABLE_SEP45` | Enable SEP-45 web authentication for smart contracts. Recommended for Embedded Wallets. | `false` | No | | `SEP45_CONTRACT_ID` | The contract ID for SEP-45 authentication. Required when SEP-45 is enabled. | - | When SEP45 enabled | ### Transaction Submission Service (TSS) Configuration The TSS also requires RPC configuration to submit smart contract transactions: | Variable | Description | Default | Required | | :-- | :-- | :-- | :-- | | `RPC_URL` | The URL of the Stellar RPC server. Must match the backend RPC_URL. | - | Yes (when embedded wallets enabled) | | `RPC_REQUEST_AUTH_HEADER_KEY` | The HTTP header name for RPC authentication. Optional. | - | No | | `RPC_REQUEST_AUTH_HEADER_VALUE` | The authentication header value. Optional. | - | No | ### Frontend (Dashboard) Configuration The frontend dashboard requires a single environment variable to enable embedded wallet features: | Variable | Description | Default | Required | | :-- | :-- | :-- | :-- | | `RPC_ENABLED` | Enable RPC-dependent features including Embedded Wallets. Set to `true`. | `false` | Yes | ### HTTPS Requirement for Passkeys :::danger[Critical: HTTPS Required] Embedded Wallets use passkeys (WebAuthn) for authentication. **Passkeys only work in secure contexts**. If your frontend is not served over HTTPS, receivers will not be able to create passkeys or authenticate with their embedded wallets. ::: --- ## CLI Manual ## Root Command The `stellar-disbursement-platform` is the main entry point for the application. It provides various subcommands to manage the service, database, and other utilities. ### Usage ```bash stellar-disbursement-platform [command] [flags] ``` :::tip For all the following commands, you can use the `--help` flag to get more information about the command and its options. For example: ```bash stellar-disbursement-platform serve --help ``` ::: ## Global Flags The following flags are available for all commands: | Flag | Description | | :-- | :-- | | `--base-url` | The SDP backend server's base URL. Defaults to `http://localhost:8000`. | | `--database-url` | Postgres DB URL. Defaults to `postgres://localhost:5432/sdp?sslmode=disable`. | | `--environment` | The environment where the application is running. Example: `development`, `staging`, `production`. Defaults to `development`. | | `--log-level` | The log level used in this project. Options: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`, or `PANIC`. Defaults to `TRACE`. | | `--network-passphrase` | The Stellar network passphrase. Defaults to `Test SDF Network ; September 2015`. | | `--sdp-ui-base-url` | The SDP UI server's base URL. Defaults to `http://localhost:3000`. | | `--sentry-dsn` | The DSN (client key) of the Sentry project. If not provided, Sentry will not be used. | ## Serve Command The `serve` command starts the Stellar Disbursement Platform backend server. This server handles API requests, processes disbursements, and manages tenant operations. ### Usage ```bash stellar-disbursement-platform serve [flags] ``` ### Flags | Flag | Description | | :-- | :-- | | `--admin-account` | ID of the admin account. To use, add to the request header as 'Authorization', formatted as Base64-encoded 'ADMIN_ACCOUNT:ADMIN_API_KEY'. | | `--admin-api-key` | API key for the admin account. To use, add to the request header as 'Authorization', formatted as Base64-encoded 'ADMIN_ACCOUNT:ADMIN_API_KEY'. | | `--admin-port` | Port where the admin tenant server will be listening on. Defaults to `8003`. | | `--aws-access-key-id` | The AWS access key ID. | | `--aws-region` | The AWS region. | | `--aws-secret-access-key` | The AWS secret access key. | | `--aws-ses-sender-id` | The email address that AWS will use to send emails. Uses AWS SES. | | `--aws-sns-sender-id` | The sender ID of the AWS account sending the SMS message. Uses AWS SNS. | | `--bridge-api-key` | Bridge API key. This needs to be configured only if the Bridge integration is enabled. | | `--bridge-base-url` | Bridge Base URL. This needs to be configured only if the Bridge integration is enabled. Defaults to `https://api.bridge.xyz`. | | `--captcha-type` | The type of CAPTCHA to use. Options: `GOOGLE_RECAPTCHA_V2`, `GOOGLE_RECAPTCHA_V3`. Defaults to `GOOGLE_RECAPTCHA_V2`. | | `--channel-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt/decrypt the channel accounts' private keys. When not set, it will default to the value of the `distribution-seed` option. | | `--circle-api-type` | The Circle API type. Options: `TRANSFERS`, `PAYOUTS`. Defaults to `TRANSFERS`. | | `--cors-allowed-origins` | Cors URLs that are allowed to access the endpoints, separated by ",". | | `--crash-tracker-type` | Crash tracker type. Options: `SENTRY`, `DRY_RUN`. Defaults to `DRY_RUN`. | | `--db-conn-max-idle-time-seconds` | Maximum idle time in seconds before a connection is closed. Defaults to `10`. | | `--db-conn-max-lifetime-seconds` | Maximum lifetime in seconds for a single connection. Defaults to `300`. | | `--db-max-idle-conns` | Maximum number of idle DB connections retained per pool. Defaults to `2`. | | `--db-max-open-conns` | Maximum number of open DB connections per pool. Defaults to `20`. | | `--disable-mfa` | Disables the email Multi-Factor Authentication (MFA). | | `--disable-recaptcha` | Disables ReCAPTCHA for login and forgot password. | | `--distribution-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt and decrypt the private keys of tenants' distribution accounts. | | `--distribution-public-key` | The public key of the HOST's Stellar distribution account, used to create channel accounts. | | `--distribution-seed` | The private key of the HOST's Stellar distribution account, used to create channel accounts. | | `--ec256-private-key` | The EC256 Private Key used to sign the authentication token. This EC key needs to be at least as strong as prime256v1 (P-256). | | `--email-sender-type` | Email Sender Type. Options: `DRY_RUN`, `TWILIO_EMAIL`, `AWS_EMAIL`. Defaults to `DRY_RUN`. | | `--enable-bridge-integration` | Enable Bridge integration for Liquidity Sourcing. | | `--horizon-url` | The URL of the Stellar Horizon server where this application will communicate with. Defaults to `https://horizon-testnet.stellar.org/`. | | `--instance-name` | Name of the SDP instance. Example: `SDP Testnet`. | | `--max-base-fee` | The max base fee for submitting a Stellar transaction. Defaults to `10000`. | | `--max-invitation-resend-attempts` | The maximum number of attempts to resend the invitation to the Receiver Wallets. Defaults to `3`. | | `--metrics-port` | Port where the metrics server will be listening on. Defaults to `8002`. | | `--metrics-type` | Metric monitor type. Options: `PROMETHEUS`. Defaults to `PROMETHEUS`. | | `--port` | Port where the server will be listening on. Defaults to `8000`. | | `--recaptcha-site-key` | The Google 'reCAPTCHA v2 - I'm not a robot' site key. | | `--recaptcha-site-secret-key` | The Google 'reCAPTCHA v2 - I'm not a robot' site SECRET key. | | `--recaptcha-v3-min-score` | The minimum score threshold for reCAPTCHA v3 (0.0 to 1.0, where 1.0 is very likely a good interaction). Only used when captcha-type is GOOGLE_RECAPTCHA_V3. Defaults to `0.5`. | | `--reset-token-expiration-hours` | The expiration time in hours of the Reset Token. Defaults to `24`. | | `--scheduler-payment-job-seconds` | The interval in seconds for the payment jobs that synchronize transactions between SDP and TSS. Must be greater than 5 seconds. Defaults to `30`. | | `--scheduler-receiver-invitation-job-seconds` | The interval in seconds for the receiver invitation job that sends invitations to new receivers. Must be greater than 5 seconds. Defaults to `30`. | | `--sep10-client-attribution-required` | If true, SEP-10 authentication requires client_domain to be provided and validated. If false, client_domain is optional. Defaults to `true`. | | `--sep10-signing-private-key` | The private key of the Stellar account that signs the SEP-10 transactions. It's also used to sign URLs. | | `--sep10-signing-public-key` | The public key of the Stellar account that signs the SEP-10 transactions. It's also used to sign URLs. | | `--sep24-jwt-secret` | The JWT secret that's used to sign the SEP-24 JWT token. | | `--single-tenant-mode` | This option enables the Single Tenant Mode feature. In the case where multi-tenancy is not required, this options bypasses the tenant resolution by always resolving to the default tenant configured in the database. | | `--sms-sender-type` | SMS Sender Type. Options: `DRY_RUN`, `TWILIO_SMS`, `TWILIO_WHATSAPP`, `AWS_SMS`. Defaults to `DRY_RUN`. | | `--tenant-xlm-bootstrap-amount` | The amount of the native asset that will be sent to the tenant distribution account from the host distribution account when it's created if applicable. Defaults to `5`. | | `--twilio-account-sid` | The SID of the Twilio account. | | `--twilio-auth-token` | The Auth Token of the Twilio account. | | `--twilio-sendgrid-api-key` | The API key of the Twilio SendGrid account. | | `--twilio-sendgrid-sender-address` | The email address that Twilio SendGrid will use to send emails. | | `--twilio-service-sid` | The service ID used within Twilio to send messages. | | `--twilio-whatsapp-from-number` | The WhatsApp Business number used to send messages (with whatsapp: prefix). | | `--twilio-whatsapp-receiver-invitation-template-sid` | The Twilio Content SID for WhatsApp receiver invitation template (starts with HX). | | `--twilio-whatsapp-receiver-otp-template-sid` | The Twilio Content SID for WhatsApp receiver OTP template (starts with HX). | ## TSS Command The `tss` command runs the Transaction Submission Service, which is responsible for submitting transactions to the Stellar network. ### Usage ```bash stellar-disbursement-platform tss [flags] ``` ### Flags | Flag | Description | | :-- | :-- | | `--channel-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt/decrypt the channel accounts' private keys. When not set, it will default to the value of the `distribution-seed` option. | | `--crash-tracker-type` | Crash tracker type. Options: `SENTRY`, `DRY_RUN`. Defaults to `DRY_RUN`. | | `--db-conn-max-idle-time-seconds` | Maximum idle time in seconds before a connection is closed. Defaults to `10`. | | `--db-conn-max-lifetime-seconds` | Maximum lifetime in seconds for a single connection. Defaults to `300`. | | `--db-max-idle-conns` | Maximum number of idle DB connections retained per pool. Defaults to `2`. | | `--db-max-open-conns` | Maximum number of open DB connections per pool. Defaults to `20`. | | `--distribution-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt and decrypt the private keys of tenants' distribution accounts. | | `--distribution-public-key` | The public key of the HOST's Stellar distribution account, used to create channel accounts. | | `--distribution-seed` | The private key of the HOST's Stellar distribution account, used to create channel accounts. | | `--horizon-url` | The URL of the Stellar Horizon server where this application will communicate with. Defaults to `https://horizon-testnet.stellar.org/`. | | `--max-base-fee` | The max base fee for submitting a Stellar transaction. Defaults to `10000`. | | `--num-channel-accounts` | Number of channel accounts to utilize for transaction submission. Defaults to `2`. | | `--queue-polling-interval` | Polling interval (seconds) to query the database for pending transactions to process. Defaults to `6`. | | `--tss-metrics-port` | Port where the metrics server will be listening on. Defaults to `9002`. | | `--tss-metrics-type` | Metric monitor type. Options: `TSS_PROMETHEUS`. Defaults to `TSS_PROMETHEUS`. | ## DB Command The `db` command provides utilities for database management and migrations. It performs two main functions: 1. Running database migrations for various schemas (admin, auth, sdp, tss). 2. Setting up assets and wallets based on the network passphrase. ### Usage ```bash stellar-disbursement-platform db [command] [flags] ``` ### Subcommands | Command | Description | | :------------------ | :----------------------------------------------------- | | `admin` | Admin migrations for multi-tenant module. | | `auth` | Authentication schema migrations. | | `sdp` | SDP schema migrations. | | `setup-for-network` | Set up assets and wallets based on network passphrase. | | `tss` | TSS schema migrations. | --- ### DB Admin The `db admin` command manages the migrations for the admin schema, which handles multi-tenancy configuration. #### Usage ```bash stellar-disbursement-platform db admin [command] [flags] ``` #### Subcommands | Command | Description | | :-------- | :------------------------ | | `migrate` | Schema migration helpers. | #### DB Admin Migrate The `migrate` command allows you to run migrations up or down. **Usage** ```bash stellar-disbursement-platform db admin migrate [command] [flags] ``` **Subcommands** | Command | Description | | :------ | :---------------------------------------- | | `up` | Migrates database up [count] migrations | | `down` | Migrates database down [count] migrations | **Examples** ```bash # Apply all pending migrations stellar-disbursement-platform db admin migrate up # Apply the next 2 migrations stellar-disbursement-platform db admin migrate up 2 # Revert the last migration stellar-disbursement-platform db admin migrate down 1 ``` --- ### DB Auth The `db auth` command manages the migrations for the authentication schema. #### Usage ```bash stellar-disbursement-platform db auth [command] [flags] ``` #### Flags | Flag | Description | | :-- | :-- | | `--all` | Apply the command to all tenants. Either `--tenant-id` or `--all` must be set, but the `--all` option will be ignored if `--tenant-id` is set. | | `--tenant-id` | The tenant ID where the command will be applied. | #### Subcommands | Command | Description | | :-------- | :------------------------ | | `migrate` | Schema migration helpers. | #### DB Auth Migrate Similar to `admin migrate`, this command accepts `up` and `down` subcommands. **Examples** ```bash # Apply migrations for a specific tenant stellar-disbursement-platform db auth migrate up --tenant-id # Apply migrations for all tenants stellar-disbursement-platform db auth migrate up --all ``` --- ### DB SDP The `db sdp` command manages the migrations for the SDP (Stellar Disbursement Platform) schema, which contains the core business logic tables. #### Usage ```bash stellar-disbursement-platform db sdp [command] [flags] ``` #### Flags | Flag | Description | | :-- | :-- | | `--all` | Apply the command to all tenants. Either `--tenant-id` or `--all` must be set, but the `--all` option will be ignored if `--tenant-id` is set. | | `--tenant-id` | The tenant ID where the command will be applied. | #### Subcommands | Command | Description | | :-------- | :------------------------ | | `migrate` | Schema migration helpers. | #### DB SDP Migrate Similar to `admin migrate`, this command accepts `up` and `down` subcommands. **Examples** ```bash # Apply migrations for a specific tenant stellar-disbursement-platform db sdp migrate up --tenant-id # Apply migrations for all tenants stellar-disbursement-platform db sdp migrate up --all ``` --- ### DB TSS The `db tss` command manages the migrations for the TSS (Transaction Submission Service) schema. #### Usage ```bash stellar-disbursement-platform db tss [command] [flags] ``` #### Subcommands | Command | Description | | :-------- | :------------------------ | | `migrate` | Schema migration helpers. | #### DB TSS Migrate Similar to `admin migrate`, this command accepts `up` and `down` subcommands. **Examples** ```bash # Apply all pending migrations stellar-disbursement-platform db tss migrate up ``` --- ### DB Setup For Network The `db setup-for-network` command sets up the assets and wallets registered in the database based on the network passphrase. It inserts or updates the entries of these tables according to the configured Network Passphrase. #### Usage ```bash stellar-disbursement-platform db setup-for-network [flags] ``` #### Flags | Flag | Description | | :-- | :-- | | `--all` | Apply the command to all tenants. Either `--tenant-id` or `--all` must be set, but the `--all` option will be ignored if `--tenant-id` is set. | | `--tenant-id` | The tenant ID where the command will be applied. | #### Example ```bash # Setup for a specific tenant stellar-disbursement-platform db setup-for-network --tenant-id # Setup for all tenants stellar-disbursement-platform db setup-for-network --all ``` ## Auth Command The `auth` command provides helpers for authentication management, specifically for adding users to the system. ### Usage ```bash stellar-disbursement-platform auth [command] [flags] ``` ### Subcommands | Command | Description | | :--------- | :---------------------- | | `add-user` | Add user to the system. | --- ### Auth Add User The `auth add-user` command adds a new user to the system. The email must be unique, and the password must be at least 12 characters long. #### Usage ```bash stellar-disbursement-platform auth add-user [flags] ``` #### Flags | Flag | Description | | :-- | :-- | | `--owner` | Set the user as Owner (superuser). Defaults to "false". | | `--password` | Sets the user password. It should be at least 12 characters long. If omitted, the command will generate a random one. | | `--roles` | Set the user roles. It should be comma-separated. Example: `role1, role2`. Available roles: `owner`, `financial_controller`, `developer`, `business`, `initiator`, `approver`. | | `--tenant-id` | The tenant ID to which the user will be added. | #### Example To add a new user with specific roles and a password: ```bash stellar-disbursement-platform auth add-user mary.jane@stellar.org Mary Jane \ --roles approver,initiator --password \ --tenant-id 'f347e6b0-249c-4960-b0d2-aebcf4c6a60d' ``` ## Channel Accounts Command The `channel-accounts` command manages channel accounts used for transaction submission. ### Usage ```bash stellar-disbursement-platform channel-accounts [command] [flags] ``` ### Flags | Flag | Description | | :-- | :-- | | `--crash-tracker-type` | Crash tracker type. Options: `SENTRY`, `DRY_RUN`. Defaults to `DRY_RUN`. | | `--distribution-public-key` | The public key of the HOST's Stellar distribution account, used to create channel accounts. | | `--tss-metrics-port` | Port where the metrics server will be listening on. Defaults to `9002`. | | `--tss-metrics-type` | Metric monitor type. Options: `TSS_PROMETHEUS`. Defaults to `TSS_PROMETHEUS`. | ### Subcommands | Command | Description | | :------- | :-------------------------------------------------- | | `create` | Create channel accounts. | | `delete` | Delete a specified channel account. | | `ensure` | Ensure a specific number of channel accounts exist. | | `verify` | Verify channel accounts exist on the network. | | `view` | List public keys of all channel accounts. | --- ### Channel Accounts Create The `create` command creates channel accounts. #### Usage ```bash stellar-disbursement-platform channel-accounts create [count] [flags] ``` #### Flags | Flag | Description | | :-- | :-- | | `--channel-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt/decrypt the channel accounts' private keys. When not set, it will default to the value of the `distribution-seed` option. | | `--distribution-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt and decrypt the private keys of tenants' distribution accounts. | | `--distribution-seed` | The private key of the HOST's Stellar distribution account, used to create channel accounts. | | `--horizon-url` | The URL of the Stellar Horizon server where this application will communicate with. Defaults to `https://horizon-testnet.stellar.org/`. | | `--max-base-fee` | The max base fee for submitting a Stellar transaction. Defaults to `10000`. | --- ### Channel Accounts Delete The `delete` command deletes a specified channel account from storage and on the network. #### Usage ```bash stellar-disbursement-platform channel-accounts delete [flags] ``` #### Flags | Flag | Description | | :-- | :-- | | `--channel-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt/decrypt the channel accounts' private keys. When not set, it will default to the value of the `distribution-seed` option. | | `--channel-account-id` | The ID of the channel account to delete. | | `--delete-all-accounts` | Delete all managed channel accounts in the database and on the network. | | `--distribution-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt and decrypt the private keys of tenants' distribution accounts. | | `--distribution-seed` | The private key of the HOST's Stellar distribution account, used to create channel accounts. | | `--horizon-url` | The URL of the Stellar Horizon server where this application will communicate with. Defaults to `https://horizon-testnet.stellar.org/`. | | `--max-base-fee` | The max base fee for submitting a Stellar transaction. Defaults to `10000`. | --- ### Channel Accounts Ensure The `ensure` command ensures that the specified number of channel accounts exist. If they do not exist, it will create them. If more channel accounts exist than specified, it will delete the excess accounts. #### Usage ```bash stellar-disbursement-platform channel-accounts ensure [flags] ``` #### Flags | Flag | Description | | :-- | :-- | | `--channel-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt/decrypt the channel accounts' private keys. When not set, it will default to the value of the `distribution-seed` option. | | `--distribution-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt and decrypt the private keys of tenants' distribution accounts. | | `--distribution-seed` | The private key of the HOST's Stellar distribution account, used to create channel accounts. | | `--horizon-url` | The URL of the Stellar Horizon server where this application will communicate with. Defaults to `https://horizon-testnet.stellar.org/`. | | `--max-base-fee` | The max base fee for submitting a Stellar transaction. Defaults to `10000`. | #### Example ```bash stellar-disbursement-platform channel-accounts ensure 5 ``` --- ### Channel Accounts Verify The `verify` command verifies that all the channel accounts in the database exist on the Stellar network. #### Usage ```bash stellar-disbursement-platform channel-accounts verify [flags] ``` #### Flags | Flag | Description | | :-- | :-- | | `--channel-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt/decrypt the channel accounts' private keys. When not set, it will default to the value of the `distribution-seed` option. | | `--delete-invalid-accounts` | Delete channel accounts from storage that are verified to be invalid on the network. | | `--distribution-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt and decrypt the private keys of tenants' distribution accounts. | | `--distribution-seed` | The private key of the HOST's Stellar distribution account, used to create channel accounts. | | `--horizon-url` | The URL of the Stellar Horizon server where this application will communicate with. Defaults to `https://horizon-testnet.stellar.org/`. | | `--max-base-fee` | The max base fee for submitting a Stellar transaction. Defaults to `10000`. | --- ### Channel Accounts View The `view` command lists public keys of all channel accounts currently stored in the database. #### Usage ```bash stellar-disbursement-platform channel-accounts view [flags] ``` ## Distribution Account Command The `distribution-account` command manages the distribution account. ### Usage ```bash stellar-disbursement-platform distribution-account [command] [flags] ``` ### Flags | Flag | Description | | :-- | :-- | | `--crash-tracker-type` | Crash tracker type. Options: `SENTRY`, `DRY_RUN`. Defaults to `DRY_RUN`. | | `--distribution-public-key` | The public key of the HOST's Stellar distribution account, used to create channel accounts. | ### Subcommands | Command | Description | | :------- | :-------------------------------------------- | | `rotate` | Rotate the distribution account for a tenant. | --- ### Distribution Account Rotate The `rotate` command rotates the distribution account for a tenant. #### Usage ```bash stellar-disbursement-platform distribution-account rotate [flags] ``` #### Flags | Flag | Description | | :-- | :-- | | `--channel-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt/decrypt the channel accounts' private keys. When not set, it will default to the value of the `distribution-seed` option. | | `--distribution-account-encryption-passphrase` | A Stellar-compliant ed25519 private key used to encrypt and decrypt the private keys of tenants' distribution accounts. | | `--distribution-seed` | The private key of the HOST's Stellar distribution account, used to create channel accounts. | | `--horizon-url` | The URL of the Stellar Horizon server where this application will communicate with. Defaults to `https://horizon-testnet.stellar.org/`. | | `--max-base-fee` | The max base fee for submitting a Stellar transaction. Defaults to `10000`. | | `--tenant-id` | The tenant ID where the command will be applied. | | `--tenant-xlm-bootstrap-amount` | The amount of the native asset that will be sent to the tenant distribution account from the host distribution account when it's created if applicable. Defaults to `5`. | #### Example To rotate the distribution account for a specific tenant: ```bash stellar-disbursement-platform distribution-account rotate --tenant-id 'f347e6b0-249c-4960-b0d2-aebcf4c6a60d' ``` ## Message Command The `message` command provides messenger related commands. ### Usage ```bash stellar-disbursement-platform message [command] [flags] ``` ### Flags | Flag | Description | | :-- | :-- | | `--aws-access-key-id` | The AWS access key ID. | | `--aws-region` | The AWS region. | | `--aws-secret-access-key` | The AWS secret access key. | | `--aws-ses-sender-id` | The email address that AWS will use to send emails. Uses AWS SES. | | `--aws-sns-sender-id` | The sender ID of the aws account sending the SMS message. Uses AWS SNS. | | `--message-sender-type` | Message Sender Type. Options: `TWILIO_SMS`, `TWILIO_WHATSAPP`, `TWILIO_EMAIL`, `AWS_SMS`, `AWS_EMAIL`, `DRY_RUN`. | | `--twilio-account-sid` | The SID of the Twilio account. | | `--twilio-auth-token` | The Auth Token of the Twilio account. | | `--twilio-sendgrid-api-key` | The API key of the Twilio SendGrid account. | | `--twilio-sendgrid-sender-address` | The email address that Twilio SendGrid will use to send emails. | | `--twilio-service-sid` | The service ID used within Twilio to send messages. | | `--twilio-whatsapp-from-number` | The WhatsApp Business number used to send messages (with `whatsapp:` prefix). | | `--twilio-whatsapp-receiver-invitation-template-sid` | The Twilio Content SID for WhatsApp receiver invitation template (starts with HX). | | `--twilio-whatsapp-receiver-otp-template-sid` | The Twilio Content SID for WhatsApp receiver OTP template (starts with HX). | ### Subcommands | Command | Description | | :------ | :-------------- | | `send` | Send a message. | --- ### Message Send The `send` command sends a message to a recipient. #### Usage ```bash stellar-disbursement-platform message send [flags] ``` #### Flags | Flag | Description | | :-- | :-- | | `--email` | The email to send the message to. Mandatory if sending an email. | | `--message` | The text of the message to be sent. | | `--phone-number` | The phone number to send the message to, in E.164. Mandatory if sending an SMS. | | `--title` | The title to be set in the email. Mandatory if sending an email. | #### Example ```bash # Send an SMS stellar-disbursement-platform message send --phone-number "+1234567890" --message "Hello World" --message-sender-type TWILIO_SMS # Send an Email stellar-disbursement-platform message send --email "user@example.com" --title "Hello" --message "Hello World" --message-sender-type AWS_EMAIL ``` --- ## Configuration(Admin-guide) Stellar Disbursement Platform services can be configured using a set of configuration options that are passed to the command line or set as environment variables. Depending on how you're using and deploying the SDP, these configurations can be set in a ConfigMap in Kubernetes, as environment variables in a Docker container, passed in as command line arguments, etc. In this section we will discuss the different configuration options available for the SDP. :::tip[Notes] - Configurations that are tagged with 🔑 are sensitive and should be stored securely. - All configurations can be passed in as either environment variables or CLI flags. For instance, the env var `BASE_URL` could be passed in through the `--base-url` flag. CLI flags take priority over env vars, even though env vars are more convenient. ::: ## SDP Core Service For the most up-to-date configuration, you can run the following command in the [stellar-disbursement-platform-backend git repository](https://github.com/stellar/stellar-disbursement-platform-backend): ```bash ./stellar-disbursement-platform serve --help ``` ### Operational Configuration Operational Configuration allows controlling metrics, logging, and other operational aspects of the SDP Core Service. - `PORT` - The port on which the SDP Core Service will listen for incoming HTTP requests. Default: 8000. - `LOG_LEVEL` - Determines the verbosity level of logs. Options: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", or "PANIC". Default: "TRACE". - `METRICS_PORT` - The port on which the SDP Core Service will expose its metrics. Default: 8002. - `METRICS_TYPE` - The type of metrics to expose. Options: "PROMETHEUS". Default: "PROMETHEUS". - `CRASH_TRACKER_TYPE` - The crash tracker type to use. Options: "SENTRY", "DRY_RUN". Default: "DRY_RUN". - `SENTRY_DSN` - 🔑 The DSN (client key) of the Sentry project. If not provided, Sentry will not be used. - `ENVIRONMENT` - The environment where the application is running. Example: "development", "staging", "production". Default: "development". - `BASE_URL` - The SDP backend server's base URL. Default: "http://localhost:8000". Tenant-specific URLs will be configured during the [tenant provisioning process](./advanced-configuration#provisioning-tenants). - `SDP_UI_BASE_URL` - The SDP UI/dashboard Base URL used to send the invitation link when a new user is created. Tenant-specific URLs will be configured during the [tenant provisioning process](./advanced-configuration#provisioning-tenants). ### Database Configuration The following configurations are related to the PostgreSQL database used by the SDP Core Service. - `DATABASE_URL` - 🔑 The connection string for the PostgreSQL database. Format is `postgres://username:password@host:port/database?sslmode=disable`. Default: "postgres://localhost:5432/sdp?sslmode=disable". - `DB_MAX_OPEN_CONNS` - Maximum open connections per pool to the database. Default: 20. - `DB_MAX_IDLE_CONNS` - Maximum idle connections retained in the pool. Default: 2. - `DB_CONN_MAX_IDLE_TIME_SECONDS` - Close idle connections after N seconds. Default: 10. - `DB_CONN_MAX_LIFETIME_SECONDS` - Recycle connections after N seconds. Default: 300. ### Messaging Configuration Messaging Configuration allows configuring the messaging service used to send messages to recipients and sdp dashboard users. The default configuration is set to "DRY_RUN" which means no messages will be sent and the messages will be logged to the console. This is recommended for testing purposes only. - `EMAIL_SENDER_TYPE`: The messenger type used to send invitations to new dashboard users. Options: "DRY_RUN", "TWILIO_EMAIL", "AWS_EMAIL". Default: "DRY_RUN". - `SMS_SENDER_TYPE`: The messenger type used to send SMS messages to recipients. Options: "DRY_RUN", "TWILIO_SMS", "TWILIO_WHATSAPP", "AWS_SMS". Default: "DRY_RUN". #### AWS Configuration The following configurations are required when using AWS SES or SNS to send emails or SMS messages. - `AWS_ACCESS_KEY_ID` - 🔑 The AWS access key ID. - `AWS_REGION` - The AWS region where the SES service is available. - `AWS_SECRET_ACCESS_KEY` - 🔑 The AWS secret access key. - `AWS_SES_SENDER_ID` - The email that AWS SES will use as the sender when sending emails. Required when `EMAIL_SENDER_TYPE` is set to "AWS_EMAIL". - `AWS_SNS_SENDER_ID` - The sender ID to use when sending SMS messages using AWS SNS. Required when `SMS_SENDER_TYPE` is set to "AWS_SMS". #### Twilio Configuration The following configurations are required when `SMS_SENDER_TYPE=TWILIO_SMS`. - `TWILIO_ACCOUNT_SID` - 🔑 The Twilio account SID. - `TWILIO_AUTH_TOKEN` - 🔑 The Twilio auth token. - `TWILIO_SERVICE_SID` - The Twilio service SID. The following configurations are required when `SMS_SENDER_TYPE=TWILIO_WHATSAPP`. - `TWILIO_ACCOUNT_SID` - 🔑 The Twilio account SID. - `TWILIO_AUTH_TOKEN` - 🔑 The Twilio auth token. - `TWILIO_SERVICE_SID` - The Twilio service SID. - `TWILIO_WHATSAPP_FROM_NUMBER` - The WhatsApp Business number used to send messages (with whatsapp: prefix). - `TWILIO_WHATSAPP_RECEIVER_INVITATION_TEMPLATE_SID` - The Twilio Content SID for WhatsApp receiver invitation template (starts with HX). - `TWILIO_WHATSAPP_RECEIVER_OTP_TEMPLATE_SID` - The Twilio Content SID for WhatsApp receiver OTP template (starts with HX). The following configurations are required when `EMAIL_SENDER_TYPE=TWILIO_EMAIL`. - `TWILIO_SENDGRID_API_KEY` - 🔑 The API key for the Twilio SendGrid (email) service. - `TWILIO_SENDGRID_SENDER_ADDRESS` - The email address used to send emails via Twilio SendGrid. #### General Messaging Configuration - `MAX_INVITATION_RESEND_ATTEMPTS` - The maximum number of attempts to (auto) resend the invitation to the Receiver Wallets. Default: 3. ### Stellar Configuration Stellar Configuration allows configuring accounts, transactions, and other Stellar-related settings. - `NETWORK_PASSPHRASE` - The Stellar network passphrase. Default "Test SDF Network ; September 2015". - `HORIZON_URL` - The URL of the Horizon server to use for submitting transactions. Default "https://horizon-testnet.stellar.org/". - `SEP10_SIGNING_PUBLIC_KEY` - The public key of the Stellar account that signs the SEP-10 transactions. It's also used to sign URLs. - `SEP10_SIGNING_PRIVATE_KEY` - 🔑 The private key of the Stellar account that signs the SEP-10 transactions. It's also used to sign URLs. - `MAX_BASE_FEE` - The max base fee for submitting a Stellar transaction. Default: 10000. - `SEP10_CLIENT_ATTRIBUTION_REQUIRED` - Determines if the SEP-10 client attribution is required. Default: true. #### Channel Accounts Configuration The following configuration is required for using the [channel-accounts CLI](./cli-manual#channel-accounts-command) to manage channel accounts. - `CHANNEL_ACCOUNT_ENCRYPTION_PASSPHRASE` - 🔑 A Stellar ed25519 secret key (starting with `S`) used to encrypt/decrypt the channel accounts' private keys. When not set, it will default to the value of `DISTRIBUTION_SEED`. #### Distribution Accounts Configuration The following configurations are related to the distribution accounts used to send funds to recipients. This configuration should match the configuration in the SDP Core Service. - `DISTRIBUTION_ACCOUNT_ENCRYPTION_PASSPHRASE` - 🔑 A Stellar ed25519 secret key (starting with `S`) used to encrypt/decrypt the in-memory distribution accounts' private keys. - `DISTRIBUTION_PUBLIC_KEY` - The public key of the HOST's Stellar distribution account. Used to disburse funds, create channel accounts and tenant distribution accounts. - `DISTRIBUTION_SEED` - 🔑 The private key of the HOST's Stellar distribution account. Used to disburse funds, create channel accounts and tenant distribution accounts. ### Security Configuration Security Configuration allows configuring the security aspects of the SDP Core Service. - `CORS_ALLOWED_ORIGINS` - Specifies the domains allowed to make cross-origin requests. "_" means all domains are allowed. Domains can contain wildcards, e.g., "https://_.example.com". - `SEP24_JWT_SECRET` - 🔑 The secret used to sign the JWT token for SEP-24 transactions. This secret is used during the receiver wallet registration flow. #### Dashboard Authentication Configuration The following configurations are related to dashboard user authentication and authorization. - `RESET_TOKEN_EXPIRATION_HOURS` - The expiration time in hours of the Reset Password Token. Default: 24 (hours). - `EC256_PUBLIC_KEY` - The EC256 Public Key used to validate the token signature. This EC key needs to be at least as strong as prime256v1 (P-256). - `EC256_PRIVATE_KEY` - 🔑 The EC256 Private Key used to sign the authentication token. This EC key needs to be at least as strong as prime256v1 (P-256). - `DISABLE_MFA` - Disables Multi-Factor Authentication (MFA) for the SDP dashboard users. - `DISABLE_RECAPTCHA` - Disables Google reCAPTCHA v2 for the SDP dashboard users. This flag doesn't affect the reCAPTCHA used during the SEP-24 flow. #### Recaptcha Configuration The following configurations are required when using Google reCAPTCHA v2 to protect the SDP Core Service from bots. ReCaptcha is used both for dashboard users and receivers of funds during the SEP-24 flow. - `RECAPTCHA_SITE_KEY` - The Google reCAPTCHA v2 - I'm not a robot site key. - `RECAPTCHA_SITE_SECRET_KEY` - 🔑 The reCAPTCHA site secret key used to validate reCAPTCHA responses. ### Background Jobs Configuration - `SCHEDULER_PAYMENT_JOB_SECONDS`: Interval in seconds for the job that synchronizes payments between SDP and TSS. Minimum is 5s. - `SCHEDULER_RECEIVER_INVITATION_JOB_SECONDS`: Interval in seconds for the job that submits receiver invitations. Minimum is 5s. ### Multi-tenancy Configuration - `ADMIN_ACCOUNT`: The username of the admin account used to authenticate HTTP requests to the Admin server. The Admin-targeted requests should add the "Authorization" header, formatted as Base64-encoded `"ADMIN_ACCOUNT:ADMIN_API_KEY"`. - `ADMIN_API_KEY`: The api key of the admin accountused to authenticate HTTP requests to the Admin server. The Admin-targeted requests should add the "Authorization" header, formatted as Base64-encoded `"ADMIN_ACCOUNT:ADMIN_API_KEY"`. - `ADMIN_PORT`: the port of the Admin server used to create and manage tenants. Default is 8003. - `INSTANCE_NAME`: the name of the SDP instance to be displayed in the `stellar.toml` file. Example: "SDP Testnet". - `SINGLE_TENANT_MODE`: When set to `"true"`, it enables the single-tenant mode, which is useful for local development or single-tenant setups. In addition to set it to true, you'll need to configure the default tenant by calling the [`POST /tenants/default-tenant`](../api-reference/default-tenant.api.mdx) request. - `TENANT_XLM_BOOTSTRAP_AMOUNT`: The amount of XLM that the HOST Stellar account will deposit deposited to the tenant distribution account for tenant bootstrap. ### Bridge Integration Configuration The following configurations are required when using the Bridge Integration. - `ENABLE_BRIDGE_INTEGRATION` - Determines if the bridge integration is enabled. - `BRIDGE_BASE_URL` - The base URL of the bridge API. Default: `"https://api.bridge.xyz"`. - `BRIDGE_API_KEY` - 🔑 The API key for the bridge integration. Required if `ENABLE_BRIDGE_INTEGRATION` is set to true. ## Transaction Submission Service (TSS) For the most up-to-date configuration, you can run the following command in the [stellar-disbursement-platform-backend git repository](https://github.com/stellar/stellar-disbursement-platform-backend): ```bash ./stellar-disbursement-platform tss --help ``` ### General Configuration - `QUEUE_POLLING_INTERVAL` - Polling interval (seconds) to query the database for pending transactions to process. Default: 6. ### Operational Configuration Operational Configuration allows controlling metrics, logging, and other operational aspects of the Transaction Submission Servic (TSS) - `LOG_LEVEL` - Determines the verbosity level of logs. Options: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", or "PANIC". Default: "TRACE". - `TSS_METRICS_PORT` - The port on which the TSS will expose its metrics. Default: 9002. - `TSS_METRICS_TYPE` - The type of metrics to expose. Options: "PROMETHEUS". Default: "PROMETHEUS". - `CRASH_TRACKER_TYPE` - The crash tracker type to use. Options: "SENTRY", "DRY_RUN". Default: "DRY_RUN". - `SENTRY_DSN` - 🔑 The DSN (client key) of the Sentry project. If not provided, Sentry will not be used. - `ENVIRONMENT` - The environment where the application is running. Example: "development", "staging", "production". Default: "development". ### Database Configuration The following configurations are related to the PostgreSQL database used by the SDP Core Service. - `DATABASE_URL` - 🔑 The connection string for the PostgreSQL database. Format is `postgres://username:password@host:port/database?sslmode=disable`. Default: "postgres://localhost:5432/sdp?sslmode=disable". - `DB_MAX_OPEN_CONNS` - Maximum open connections per pool to the database. Default: 20. - `DB_MAX_IDLE_CONNS` - Maximum idle connections retained in the pool. Default: 2. - `DB_CONN_MAX_IDLE_TIME_SECONDS` - Close idle connections after N seconds. Default: 10. - `DB_CONN_MAX_LIFETIME_SECONDS` - Recycle connections after N seconds. Default: 300. ### Stellar Configuration Stellar Configuration allows configuring accounts, transactions, and other Stellar-related settings. - `NETWORK_PASSPHRASE` - The Stellar network passphrase. Default "Test SDF Network ; September 2015". - `HORIZON_URL` - The URL of the Horizon server to use for submitting transactions. Default "https://horizon-testnet.stellar.org/". - `MAX_BASE_FEE` - The max base fee for submitting a Stellar transaction. Default: 10000. #### Channel Accounts Configuration The following configurations are required for using channel accounts to submit transactions to the Stellar network. - `NUM_CHANNEL_ACCOUNTS` - Number of channel accounts to utilize for transaction submission. Default: 2. - `CHANNEL_ACCOUNT_ENCRYPTION_PASSPHRASE` - 🔑 A Stellar ed25519 secret key (starting with `S`) used to encrypt/decrypt the channel accounts' private keys. When not set, it will default to the value of the 'DISTRIBUTION_SEED' option. #### Distribution Accounts Configuration The following configurations are related to the distribution accounts used to send funds to recipients. This configuration should match the configuration in the SDP Core Service. - `DISTRIBUTION_ACCOUNT_ENCRYPTION_PASSPHRASE` - 🔑 A Stellar ed25519 secret key (starting with `S`) used to encrypt/decrypt the in-memory distribution accounts' private keys. - `DISTRIBUTION_PUBLIC_KEY` - The public key of the HOST's Stellar distribution account. Used to disburse funds, create channel accounts and tenant distribution accounts. - `DISTRIBUTION_SEED` - 🔑 The private key of the HOST's Stellar distribution account. Used to disburse funds, create channel accounts and tenant distribution accounts. ## Dashboard The SDP Dashboard is a web application that allows users to manage their accounts, view transaction history, and more. Environment variables can be set either on a global `window._env_` object or as `process.env` variables. All environment variables used in this repo are in `src/constants/envVariables.ts` file, including types. The default location of the `window._env_` object is `public/settings/env-config.js`. ### General Configuration - `API_URL` - The base URL of the SDP Core Service. Default: "http://localhost:8000". - `STELLAR_EXPERT_URL` - The base URL of the Stellar Expert explorer. Default: "https://stellar.expert/explorer/testnet". - `HORIZON_URL` - The base URL of the Horizon server. Default: "https://horizon-testnet.stellar.org". - `RECAPTCHA_SITE_KEY` - The Google reCAPTCHA v2 - I'm not a robot site key. This key needs to match the key used in the SDP Core Service. - `SINGLE_TENANT_MODE` - When set to `"true"`, it enables the single-tenant mode, which is useful for local development or single-tenant setups. In addition to set it to true, you'll need to configure the default tenant by calling the [`POST /tenants/default-tenant`](../api-reference/default-tenant.api.mdx) request. Default: "false". --- ## Deployment ## Deployment via Helm Charts ### Minimum System Requirements - **Stellar Accounts**: You will need a **Distribution Account** (funded) and a **SEP-10 Signing Account**. - **Certificates**: When running the SDP in a multi-tenant configuration, you will need to acquire wildcard TLS certificates to facilitate tenant provisioning as the SDP relies on subdomains to differentiate between tenants. This will allow you to provision tenants without having to manually configure TLS certificates for each tenant. You can use a service like [Let's Encrypt](https://letsencrypt.org) or [Namecheap](https://www.namecheap.com/security/ssl-certificates) to acquire these certificates. | Component | Requirement | Notes | | :-- | :-- | :-- | | **Kubernetes** | v1.19+ | For Helm deployment | | **Helm** | v3.14.0+ | For Helm deployment | | **PostgreSQL** | v14.0+ | Required for both deployment methods | | **RAM** | 4GB+ | Minimum memory recommended for running the full stack in single tenant mode | ### Installing the Chart #### 1. Add the Stellar Helm Repository Add the official Stellar Helm chart repository to your local Helm client: ```shell helm repo add stellar https://helm.stellar.org/charts ``` #### 2. Prepare Configuration Download the minimal configuration file to serve as a baseline: ```shell curl -LJO https://raw.githubusercontent.com/stellar/stellar-disbursement-platform-backend/main/helmchart/sdp/minimal-values.yaml ``` The following parameters can be set in the `minimal-values.yaml` file or overridden directly via the CLI during installation: - `global.distributionPublicKey`: Public key of the distribution account. - `global.distributionPrivateKey`: Private key of the distribution account. - `global.sep10PublicKey`: Public key for SEP-10 authentication. - `global.sep10PrivateKey`: Private key for SEP-10 authentication. - `global.isPubnet`: Set to `true` for Mainnet. Refer to the [Helm Chart README](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/helmchart/sdp/README.md#parameters) for a complete list of parameters. :::tip There is a more detailed explanation of how to configure the SDP in the [Configuration Guide](configuring-sdp). ::: #### 3. Install the Chart Install the chart using your customized values file. You can override values directly via the CLI or modify the `minimal-values.yaml` file. ```shell helm install sdp -f minimal-values.yaml stellar/stellar-disbursement-platform \ --set "global.distributionPublicKey=" \ --set "global.distributionPrivateKey=" \ --set "global.sep10PublicKey=" \ --set "global.sep10PrivateKey=" ``` ## Deployment via Docker Compose This section outlines how to deploy the SDP using Docker Compose for a production-like environment. Unlike the development setup, this configuration uses pre-built production images and requires explicit configuration of environment variables and secrets. ### Minimum System Requirements - **Stellar Accounts**: You will need a **Distribution Account** (funded) and a **SEP-10 Signing Account**. - **Network Access**: Outbound access to the Stellar network (Horizon/Soroban) and any third-party integrations (Twilio, AWS SES, etc.). | Component | Requirement | Notes | | :-- | :-- | :-- | | **Docker** | v20.10+ | Required for container orchestration | | **RAM** | 4GB+ | Minimum memory recommended for running the full stack | ### Deployment Steps #### 1. Clone the Repository ```shell git clone https://github.com/stellar/stellar-disbursement-platform-backend.git cd stellar-disbursement-platform-backend ``` #### 2. Create Environment File Copy the example environment file. ```shell cp dev/.env.example dev/.env ``` #### 3. Configure Environment Variables Edit `dev/.env` and populate the following variables with your Stellar account keys: - `DISTRIBUTION_PUBLIC_KEY` - `DISTRIBUTION_SEED` - `SEP10_SIGNING_PUBLIC_KEY` - `SEP10_SIGNING_PRIVATE_KEY` For mainnet deployment, set: _ `NETWORK_TYPE=pubnet` _ `NETWORK_PASSPHRASE=Public Global Stellar Network ; September 2015` _ `HORIZON_URL=https://horizon.stellar.org` _ `DISABLE_MFA=false` (Enforced for security) #### 4. Start the Services ```shell docker compose -f dev/docker-compose.yml up -d ``` --- ## Architecture(3) The Stellar Disbursement Platform consists of three services deployed together: - **Dashboard**: the user interface administrators use to initiate and track the progress of disbursements - **SDP Core Service**: the core backend service that performs several functions: - **Dashboard API**: the API used by the front-end UI for all disbursement requests. The API is documented [here](../api-reference) - **Admin API**: the API used by the host organization to manage tenant provisioning and configuration. The API is documented [here](../api-reference/admin) - **Messaging Service**: a recurring process that sends text messages to users prompting them to download the wallet selected for a particular disbursement and verify their phone with an OTP - **Wallet Registration**: a web application registers a recipient by collecting and verifying their OTP code and verification information via Stellar’s [SEP-24: Hosted Deposit and Withdrawal](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md) protocol - **Transaction Submission Service**: the service that submits all payment transactions to the Stellar network. This service is designed to maximize payment throughput, handle queuing, and graceful resubmission/error handling ## Dependencies {/* #dependencies */} - **Container Orchestration**: the SDP is packaged as Docker containers and can be deployed to Kubernetes or AWS Fargate. SDF provides a Helm Chart for Kubernetes - **Postgres**: the SDP uses a Postgres database server for all of its services - **Twilio or AWS SNS and SES**: the SDP’s messaging service uses SMS/WhatsApp messages via Twilio or AWS SNS and administrative emails for organization account setup and recovery via AWS SES or Twilio SendGrid - **Stellar Accounts**: - Distribution Account: the SDP requires access to at least one funded Stellar account to make payments to the recipient - SEP-10 Auth Account: the SDP requires a Stellar account for the mutual authentication protocol [SEP-10: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) used to connect to wallet applications ## Architecture Diagram ![Architecture Diagram](/assets/SDP/SDP2-2.png) ### User Roles {/* #user-roles */} The SDP defines the following user roles: - **Host Admin**: the organization that hosts the SDP instance and manages tenant provisioning and configuration via the `Tenant Admin` API - **Dashboard User**: a user that belongs to a tenant and uses the SDP Dashboard to create and manage disbursements, recipients, and other tenant-specific data. - **API User**: a user that belongs to a tenant and uses the `Dashboard API` to create and manage disbursements, recipients, and other tenant-specific data programmatically - **Receivers**: the end users that receive the funds sent via the SDP. Receivers can either use a wallet application that supports SEP-24 for automatic registration, or they can receive funds directly to their Stellar account. ### Workflow {/* #workflow */} 1. Host admin uses the `Tenant Admin` API to provision and manage tenants. 2. `Dashboard User` and `API User` use the SDP Core Service to send disbursements and manage / invite other users. This can be done via the Dashboard UI or directly via the `Dashboard API`. 3. For payments that require SEP-24 registration, the `SDP Core Service` sends a message to notify the receivers. The message contains a deeplink that launches the target wallet, which in turn triggers the **SEP-24 deposit flow** and registers the receivers. 4. The TSS pulls payments that ready to be processed then submits them to the Stellar Network through Channel Accounts. ## Database & Schemas {/* #database */} The SDP uses a Postgres database for all of its services. The database schema is managed by the SDP Core Service and is versioned in the codebase. The database schema is designed to be tenant-aware, meaning that each tenant has its own set of tables and data. This allows the SDP to be multi-tenant and support multiple organizations using the same instance. There are 3 types of schemas in the database: - **Admin Schema**: contains tables for managing tenants. This schema is used by the Admin API to manage tenant configuration and provisioning. - **TSS Schema**: contains tables for managing transactions. This schema is used by the Transaction Submission Service to manage the state of payment transactions. - **Tenant Schemas**: each tenant has its own schema that contains tables for managing disbursements, recipients, and other tenant-specific data. These schemas are prefixed with `sdp_`. ## Multi-tenancy {/* #multi-tenancy */} The SDP can be deployed in a multi-tenant configuration, where multiple organizations share the same instance of the SDP. Each organization is referred to as a tenant and has its own set of data and configuration. A host organization can manage multiple tenants and manage their configuration through the Admin API. ### Tenant Resolution {/* #tenant-resolution */} The SDP uses a tenant resolution strategy to determine which tenant a request belongs to. Tenant resolution is only required for unauthenticated requests, as authenticated requests include the tenant information already in the JWT token. - **Header**: the `SDP-Tenant-Name` header is used to specify the tenant name in the request. When present, this header is used to attempt resolving the tenant. - **Subdomain**: the SDP can use the subdomain of the request URL to resolve the tenant. For example, `tenant1.sdp.backend.test` would resolve to the tenant `tenant1`. Resolution priority goes as follows: JWT token (authenticated requests) > Header > Subdomain. #### Single Tenant Mode {/* #single-tenant-mode */} When single tenant mode is enabled using the `SINGLE_TENANT_MODE` environment variable, all tenants will automatically resolve to the default tenant. A default tenant is set by calling the API [`POST /tenants/default-tenant`](../api-reference/default-tenant.api.mdx). Default tenant is useful for development purposes or when the SDP is used by a single organization. This allows the organization to skip specifying the tenant in every request and simplifies the SDP setup operationally by removing the need of providing wildcard TLS certificates for multi-tenant configurations. #### Subdomain Resolution {/* #subdomain-resolution */} When running the SDP in multi-tenant mode, the SDP uses the subdomain of the request URL to resolve the tenant. For example, `tenant1.sdp.backend.test` would resolve to the tenant `tenant1`. This allows the SDP to differentiate between tenants without requiring the tenant name to be specified in the request. The subdomain resolution is particularly important for the Wallet Registration process, as the SDP relies on subdomains to differentiate between tenants during the SEP-24 deposit flow. Home domains, which contain the tenant name as a subdomain, are used during the registration process by the SDP to identify the tenant and route the wallet-registration request to the correct tenant context. --- ## Embedded Wallets ## Introduction Embedded Wallets allow receivers to receive disbursements without needing to download or manage a separate wallet application. When you create a disbursement with Embedded Wallets as the target wallet provider, the SDP automatically creates a lightweight, passkey-secured smart contract wallet for each receiver. ### Why Use Embedded Wallets? Embedded Wallets significantly reduce friction for receivers who don't have an existing Stellar wallet: - **No App Download Required**: Receivers don't need to download a separate wallet application - **Passwordless Authentication**: Uses passkeys (biometric or device-based authentication) instead of passwords - **Phishing-Resistant**: Built on WebAuthn standards that prevent credential theft - **Simple User Experience**: Receivers can claim their funds with just a few clicks ### Limitations Before enabling Embedded Wallets, note the current limitations: - **No exchange transfers**: Embedded wallets cannot send directly to exchanges. - **No built-in offramp**: Offramping must be handled out-of-band (for example, a direct transfer to a user-provided address). - **Single wallet per receiver**: Each receiver can create only one embedded wallet ### Key Concepts **Smart Contract Wallets**: Embedded Wallets are Stellar smart contract accounts that are deployed on-chain when a receiver creates their passkey. These contracts are controlled by the receiver's passkey credential. **Passkeys**: A modern authentication method that replaces passwords with cryptographic keys stored securely on the user's device, unlocked with biometrics (fingerprint, Face ID, etc.) or device PIN. **SEP-45**: A Stellar web authentication protocol for contract accounts (`C...`) that lets wallets prove control of a smart contract wallet and obtain a JWT session token from a service. The SDP uses SEP-45 during SEP-24 flows to verify that receivers control their embedded wallets. --- ## What are Passkeys? Passkeys are a replacement for passwords that provide stronger security and a better user experience. Instead of remembering and typing a password, users authenticate with biometrics (like fingerprint or facial recognition) or their device's PIN/pattern. ### How Passkeys Work When a receiver creates an embedded wallet: 1. The browser or device generates a **cryptographic key pair** (public key and private key) 2. The **private key** is stored securely on the receiver's device and never leaves it 3. The **public key** is sent to the SDP and used to create the smart contract wallet 4. When signing in later, the receiver uses biometrics to unlock their private key ### Why Passkeys are More Secure **Phishing-Resistant**: Unlike passwords, passkeys are cryptographically bound to your domain. Even if a receiver visits a fake website, their passkey won't work there. **Automatically Unique**: Each passkey is unique per service. There's no risk of password reuse across sites. **Breach-Resistant**: The SDP only stores public keys. Even if the database is compromised, attackers cannot use public keys to authenticate. **No Weak Passwords**: Users can't create weak or easily-guessed credentials. All passkeys use strong cryptography. :::info[Learn More About Passkeys] For more details about passkey technology, visit [passkeys.dev](https://passkeys.dev/docs/intro/what-are-passkeys) or read about the [WebAuthn specification](https://www.w3.org/TR/webauthn). ::: --- ## How It Works The Embedded Wallet flow includes the following steps: 1. **Disbursement Creation**: An administrator creates a disbursement and selects "Embedded Wallet" as the wallet provider 2. **Invitation Sent**: The SDP sends an invitation link to each receiver via SMS or email. The link is unique per receiver. 3. **Passkey Creation**: The receiver clicks the link and creates a passkey using their device's biometric authentication 4. **Wallet Deployment**: The SDP deploys a smart contract wallet on the Stellar network, controlled by the receiver's passkey 5. **Verification**: The receiver completes identity verification (e.g., entering an OTP or date of birth) 6. **Payment Transfer**: Once verified, the SDP automatically transfers the disbursement funds to the receiver's contract wallet :::danger Each link is unique per receiver. Treat it as sensitive: if a link is leaked and you have **no verification** enabled, an attacker could create the receiver’s wallet and claim the funds. Skipping verification should only be done in low-risk scenarios and with small disbursement amounts. ::: ### Behind the Scenes When a receiver logs in with their passkey, several things happen: - The frontend uses **WebAuthn** to authenticate the receiver with their biometric or device PIN - The backend verifies the authentication using the stored public key - A **wallet-auth JWT** is generated for SDP APIs (e.g., profile and RPC access) - The SDP **sponsors transactions** on behalf of the receiver, covering all network fees - Payments are made using **Stellar Asset Contract (SAC)** transfers to the smart contract address --- ## Prerequisites Before using Embedded Wallets, ensure your SDP instance is properly configured: 1. **Backend Configuration**: Embedded Wallets require specific environment variables to be set. See the [Embedded Wallets Configuration](./advanced-configuration#embedded-wallets-configuration) section in the Advanced Configuration guide. 2. **Frontend HTTPS Requirement**: The frontend dashboard **must** be served over HTTPS for passkeys to work. WebAuthn requires a secure context and will not function over plain HTTP. 3. **Network Selection**: Deploy the SEP-45 contract and the embedded wallet Wasm. For detailed configuration instructions, see the [Embedded Wallets Configuration](./advanced-configuration#embedded-wallets-configuration) section. --- ## Using Embedded Wallets ### Step 1: Create a Disbursement with Embedded Wallet When creating a new disbursement, select **"Embedded Wallet"** as the wallet provider in the disbursement details form. ![Creating a disbursement with Embedded Wallet selected](/assets/SDP/SDP46.png) **What to configure:** - **Registration Contact Type**: Choose how receivers will be contacted (Email or SMS) - **Wallet Provider**: Select "Embedded Wallet" from the dropdown - **Asset**: Choose the asset to disburse (e.g., USDC, XLM) - **Verification Type**: Select what information receivers must verify (e.g., PIN, date of birth) :::note There is also a "None" option, but using it is only recommended for low-risk scenarios due to security concerns (e.g. disbursing small amounts). ::: Once you've uploaded your disbursement CSV file and submitted the disbursement, the SDP will begin sending invitations to all receivers in the list. --- ### Step 2: Receiver Creates Passkey Receivers will receive an invitation message (via SMS or email) with a secure link to create their embedded wallet. The link format looks like this: ```j https://your-tenant.sdp.stellar.org/wallet?asset=native&token=c09bd254-b77a-4685-bc18-377231484267&signature=26b48d7ce... ``` :::danger[Reminder] Each link is unique per receiver. Treat it as sensitive: if a link is leaked and you have **no verification** enabled, an attacker could create the receiver’s wallet and claim the funds. Skipping verification should only be done in low-risk scenarios and with small disbursement amounts. We also recommend enabling verification because it requires users to authenticate via SEP-45 before receiving funds. This confirms their passkey works correctly. This helps avoid cases where accounts are created and funds disbursed, but users are prevented from accessing the wallet due to passkey issues that would have been caught during verification. ::: When the receiver clicks the link, they'll see a page inviting them to create their wallet account: ![Invitation to create an embedded wallet with passkey](/assets/SDP/SDP47.png) **The passkey creation process:** 1. The receiver clicks **"Log in with passkey"** 2. Their browser or device prompts them to create a passkey (first-time users) or authenticate with an existing passkey (returning users) 3. For new users, they'll scan their fingerprint, face, or enter their device PIN 4. The browser generates a cryptographic key pair securely on the device **What happens behind the scenes:** - The frontend calls `POST /embedded-wallets/passkey/registration/start` with the invitation token - The backend validates the token and initiates a WebAuthn registration ceremony - After biometric authentication, the frontend calls `POST /embedded-wallets/passkey/registration/finish` with the credential - The backend queues a smart contract wallet deployment transaction with the public key - The Transaction Submission Service (TSS) deploys the contract to the Stellar network - Once deployed, the receiver's wallet is ready to receive funds :::tip[Returning Users] If a receiver already has a passkey for a different disbursement, they can simply authenticate with their existing passkey instead of creating a new one. The same passkey can be used across multiple disbursements. ::: --- ### Step 3: Complete Verification After creating their passkey, receivers are prompted to complete verification before they can receive funds: ![Verification prompt after wallet creation](/assets/SDP/SDP48.png) The verification step ensures that the receiver is who they claim to be. Depending on how you configured the disbursement, receivers may need to: - Enter an **OTP (One-Time Password)** sent to their email or phone - Provide their **date of birth** - Enter a **PIN** that was shared with them - Provide **national ID** information **What happens after verification:** 1. The receiver submits their verification information 2. The SDP validates the information against the receiver record 3. If verification succeeds, the receiver wallet status changes from `READY` to `REGISTERED` 4. The SDP automatically initiates the payment to the receiver's contract wallet address :::info[Verification Flow] The verification process uses the SDP's native SEP-24 implementation. The receiver authenticates with a SEP-24 JWT token that's generated during the passkey authentication flow. For background on SEP-45 (contract-account web authentication), see [SEP-45](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0045.md). ::: --- ### Step 4: Receiving Funds Once verification is complete, the SDP automatically transfers the disbursement amount to the receiver's smart contract wallet address. **Payment Process:** 1. The SDP queues a payment transaction to the receiver's contract address 2. The payment uses a **Stellar Asset Contract (SAC) transfer** to move funds from the distribution account to the contract wallet 3. The transaction is **fee-sponsored** by the distribution account, so the receiver pays nothing 4. Once confirmed on the network, the funds are available in the receiver's embedded wallet **What receivers see:** - The embedded wallet interface displays their asset balance - They can view their wallet address (a Stellar C-address starting with "C") - They can initiate transactions to send funds or withdraw to fiat ## External Resources To learn more about the technologies behind Embedded Wallets, check out these resources: ### Passkeys & WebAuthn - [passkeys.dev](https://passkeys.dev) - Comprehensive guide to passkeys - [WebAuthn Specification](https://www.w3.org/TR/webauthn) - W3C standard for web authentication - [FIDO Alliance](https://fidoalliance.org) - The organization behind passkey standards ### Stellar Protocols - [SEP-24: Hosted Deposit and Withdrawal](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md) - Interactive deposit/withdrawal flow - [SEP-45: Smart Contract Domain Verification](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0045.md) - Web authentication for smart contracts - [Stellar Asset Contract (SAC)](https://developers.stellar.org/docs/learn/smart-contract-internals/stellar-asset-contract) - Token standard for Stellar smart contracts ### Smart Contracts - [Soroban Documentation](https://soroban.stellar.org) - Stellar's smart contract platform - [Contract Address Format](https://developers.stellar.org/docs/learn/encyclopedia/contract-development/types/custom-types#addresses) - Understanding C-addresses vs G-addresses --- ## Getting Started(3) This guide covers running the Stellar Disbursement Platform locally, sending a sample disbursement, and claiming it through the demo wallet on Testnet. Treat this walkthrough as a learning environment rather than the path for production deployments. ### Prerequisites - **Docker:** Make sure you have Docker installed on your system. If not, you can download it from [here](https://www.docker.com/products/docker-desktop) and start it once installed. - **Git:** You will need Git to clone the repository. You can download it from [here](https://git-scm.com/downloads). - **Go:** Required to generate your environment file. Install from [here](https://golang.org/dl). - **jq:** Useful for optional scripts and diagnostics. You can download it from [here](https://jqlang.org/download) For macOS, Linux, you can install with Homebrew: ```bash brew install --cask docker brew install git go jq ``` ### Clone the repository ```bash git clone https://github.com/stellar/stellar-disbursement-platform-backend.git cd stellar-disbursement-platform-backend ``` ### Run the wizard ```bash make setup ``` At the prompts, choose according to below. The wizard generates and funds the SEP-10 signer and distribution accounts (testnet), starts Docker, and prints tenant credentials. ``` ? Select an existing run configuration or create new: ▸ Create new configuration ✔ Setup name (optional): ? Select network: ▸ testnet ? Select tenant mode: ▸ single-tenant ? Account setup: ▸ Generate new accounts ? Launch local environment now (project=sdp-sdp-test, setup=)? [Y/n] Y ? Initialize tenants and users? [y/N] Y ``` After completion you should see: ``` 🎉🎉🎉🎉 SUCCESS! 🎉🎉🎉🎉 Single tenant mode - Login URL: 🔗Default tenant: http://localhost:3000 username: owner@default.local password: Password123! ``` ### Log into the SDP Open http://localhost:3000 and log in using the admin credentials generated by the setup wizard (organization: default). ![Login](/assets/SDP/SDP32.png) ### Send a test disbursement Click `New Disbursement +` on the dashboard; you’ll see a funded distribution account ready to use: - Choose phone number as the Registration Contact Type. This is the channel recipients will use to receive messages. - Select XLM as the asset to disburse. - Pick Demo Wallet as the recipient wallet. - Choose Date of Birth as the verification method; recipients will enter it to confirm their identity. - Give disbursement a name. ![Disbursement Details](/assets/SDP/SDP33.png) Create and Upload a Disbursement File: - Download the sample via `Download CSV Template`; it includes all required columns. - Update the placeholder/invalid phone numbers before using it. - The verification column holds the identity data recipients must match. ![Disbursement CSV](/assets/SDP/SDP34.png) Click the Review button. When you are ready to start the disbursement, click the "Confirm disbursement" button. In Disbursement Details you’ll see the payment in `Ready` status, meaning the receiver has yet to accept the invitation and payment. ![Disbursement Dashboard](/assets/SDP/SDP35.png) ### Receive Payment :::note This section shows the Testnet-only Demo Wallet flow so you can observe the receiver experience while running SDP locally. For production or real wallet integrations, follow the guidance in [Making Your Wallet SDP-Ready](./making-your-wallet-sdp-ready.mdx). ::: Claim the payment in the demo wallet by first creating a wallet: - Open the demo wallet: http://localhost:4000. - Click `Generate Keypair for new account` to create a keypair; save the public and secret keys if you plan to reuse the account. - Click `Create account` to create the account on the Stellar testnet (the account starts with 10,000 XLM). ![Demo Wallet Creation](/assets/SDP/SDP36.png) To receive your payment, initialize a SEP-24 deposit: - Under Asset XLM, click `Add Home Domain`, enter `localhost:8000`, and click `Override`. - In the `Select action` dropdown, choose `SEP-24 Deposit`, then click `Start`. ![SEP-24 Deposit](/assets/SDP/SDP37.png) Verify your identity: - When prompted, enter the same phone number used in the disbursement CSV. - Complete the OTP and Date of Birth verification. The OTP appears in the `sdp-api` container logs (e.g., “Here is the 6-digit verification code you requested ...”). ![OTP Code](/assets/SDP/SDP38.png) ![PII Verification](/assets/SDP/SDP39.png) ### Monitoring - In the SDP dashboard, the payment moves from `Ready` to `PENDING` during the wallet flow, then to `Success` when funds are deposited. ![SDP Dashboard](/assets/SDP/SDP40.png) - In the demo wallet, the balance updates to reflect the new amount. ![Demo Wallet Balance](/assets/SDP/SDP41.png) --- ## Making Your Wallet SDP-Ready Remember that any SDP instance will need an agreement with a wallet provider before sending disbursements into that wallet. This ensures the wallets are comfortable receiving funds from your organization and governs any commercial arrangement between the organizations. The wallet will need to allowlist the SDP domain before the SDP can send disbursements to that wallet. When the wallet domain is added to a SDP, it's effectively being allowlisted by the SDP. Both sides listing the other allows them to retrieve the stellar.toml file and check the signing key needed for the [SEP-10] handshake. In this page, we will cover the technical aspects of the SDP-Wallet integration, including how to add a Wallet in the SDP database, how to validate and support the registration links using mobile app's [deep linking], how to start the user registration flow in the wallet using [SEP-24], and a recommended approach for handling [deferred deep linking]. ## Adding a Wallet to an SDP The default list of SDP wallets depends on which network is being used (testnet or pubnet). The network is passed as an environment variable and then the list of wallets can be seeded appropriately on SDP startup through the CLI command `./stellar-disbursement-platform db setup-for-network`, according with a hardcoded list of known wallets. Alternatively, wallets can be inserted directly into the SDP database through a SQL command. Both methods require adding the wallet name, homepage, SEP-10 client domain, and deep link schema. To insert it directly into the database, update your values and run the following Postgres query. Make sure to check your database and namespace first. ```sql INSERT INTO wallets (name, homepage, deep_link_schema, sep_10_client_domain) VALUES ('Vibrant Assist', 'https://vibrantapp.com', 'https://vibrantapp.com/sdp', 'api.vibrantapp.com'); ``` To configure a wallet through the code, add it to the testnet or pubnet section of `DefaultWalletsNetworkMap`. This will be used when you execute the `./stellar-disbursement-platform db setup-for-network` CLI command, which updates the SDP database and makes the wallet available for new disbursements. Add your new wallet following the same format already present in the code. ```go var DefaultWalletsNetworkMap = WalletsNetworkMapType{ utils.PubnetNetworkType: { { Name: "Vibrant Assist", Homepage: "https://vibrantapp.com/assist", DeepLinkSchema: "https://vibrantapp.com/sdp", SEP10ClientDomain: "api.vibrantapp.com", }, }, utils.TestnetNetworkType: { { Name: "Vibrant Assist", Homepage: "https://vibrantapp.com", DeepLinkSchema: "https://vibrantapp.com/sdp-dev", SEP10ClientDomain: "api-dev.vibrantapp.com", }, { Name: "Demo Wallet", Homepage: "https://demo-wallet.stellar.org", DeepLinkSchema: "https://demo-wallet.stellar.org", SEP10ClientDomain: "demo-wallet-server.stellar.org", }, }, } ``` ## Recipient Registration Experience The recipient registration experience is paramount to make this application smooth and easy to use. this requires the wallet to support [deferred deep linking], which will be discussed in a later section. A good description of the registration experience is as follows: 1. The recipient receives an invitation message notifying them they have a payment waiting from the organization and prompts them to click a [deep link] to open or install&open a wallet application 1. When the recipient opens the wallet app, the wallet immediately onboards the recipient, creates a Stellar account and trustline for the desired asset, initiates a [SEP-24] deposit transaction with the SDP, and opens the SDP's registration webpage as an overlay screen/iframe inside the app. 1. The user confirms their phone number and date of birth directly with the SDP, without sharing any data with the wallet, and after the registration finishes, the user is sent back to the wallet application. Here are the screens demonstrating these steps: ![Registration Flow](/assets/SDP/SDP25.png) 1. The user receives the payment within seconds ## Registration Deep Link Once the user has installed the wallet application, the wallet should be able to interpret a [deep link] that follows the format registered in the SDP in order to kick off the [Wallet Registration Procedure](#wallet-registration-procedure). The deep link format supported by the SDP follows this format: ```url https://?asset=&domain=&name=&signature= ``` - `asset`: the Stellar asset. - `domain`: the domain hosting the SDP's `stellar.toml` file. The wallet will need to use it to both fetch the `stellar.toml` file, and to populate the `home_domain` field in the [SEP-10] GET challenge transaction. - `name`: the name of the organization sending payments. - `signature`: a signature from the SDP's [SEP-10] signing key. :::info Note that the deep link is specific to each SDP, payer org, and asset. It is not specific per individual receiver. There is no risk in sharing the link with receivers who are part of the same disbursement. The link will be the same for multiple receivers and they will prove their identity as part of the [SEP-24] deposit flow. ::: Below is an example of a registration link (signed) ```url https://vibrantapp.com/sdp-dev?asset=USDC-GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5&domain=ap-stellar-disbursement-platform-backend-dev.stellar.org&name=Stellar+Test&signature=fea6c5e805a29b903835bea2f6c60069113effdf1c5cb448d4948573c65557b1d667bcd176c24a94ed9d54a1829317c74f39319076511512a3e697b4b746ae0a ``` In this example, the host is `https://vibrantapp.com/sdp-dev` and the signature is the result of signing the below (unsigned) url using the [SEP-10] signing key `SBUSPEKAZKLZSWHRSJ2HWDZUK6I3IVDUWA7JJZSGBLZ2WZIUJI7FPNB5`, with the public key being `GBFDUUZ5ZYC6RAPOQLM7IYXLFHYTMCYXBGM7NIC4EE2MWOSGIYCOSN5F`: ```url https://vibrantapp.com/sdp-dev?asset=USDC-GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5&domain=ap-stellar-disbursement-platform-backend-dev.stellar.org&name=Stellar+Test ``` In this example, the signature is `fea6c5e805a29b903835bea2f6c60069113effdf1c5cb448d4948573c65557b1d667bcd176c24a94ed9d54a1829317c74f39319076511512a3e697b4b746ae0a`. Below is a JavaScript snippet demonstrating how to verify the signature: ```js #!/usr/bin/env node const { Keypair } = require("@stellar/stellar-sdk"); // The SDP's stellar.toml SIGNING_KEY // // For security, this should ideally be fetched from // https:///.well-known/stellar.toml on demand const keypair = Keypair.fromPublicKey( "GBFDUUZ5ZYC6RAPOQLM7IYXLFHYTMCYXBGM7NIC4EE2MWOSGIYCOSN5F", ); console.log("public key:", keypair.publicKey()); let url = "https://vibrantapp.com/sdp-dev?asset=USDC-GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5&domain=ap-stellar-disbursement-platform-backend-dev.stellar.org&name=Stellar%20Test"; let signature = "fea6c5e805a29b903835bea2f6c60069113effdf1c5cb448d4948573c65557b1d667bcd176c24a94ed9d54a1829317c74f39319076511512a3e697b4b746ae0a"; console.log( "verified:", keypair.verify( Buffer.from(url.toString(), "utf8"), Buffer.from(signature, "hex"), ), ); ``` ### Wallet Registration Procedure When opening registration [deep link], these are the steps the wallet should follow in order to enforce the security and privacy measures expected in this flow, and to allow the user to input their information directly with the SDP: 1. 🚨 Confirm that the `domain` of the deep link is on the wallet's allowlist. This is crucial for authenticating from a trusted wallet.🚨 1. Fetch the SDP's toml file at `{domain}/.well-known/stellar.toml` and confirm the `SIGNING_KEY` variable is populated. 1. Verify that the registration link signature was made using `SIGNING_KEY` similar to the `keypairPk.verify(...)` function in the snippet above, and that the signature is valid with the content of the link. 1. Check the `asset` from the link and confirm that the recipient user has a trustline for that asset. Create one if it doesn't exist. 1. (Optional) Use the `name` from the link to update the wallet user interface. 1. Initiate the [SEP-24] deposit flow with that asset using the `TRANSFER_SERVER_SEP0024` value from the SDP's toml file. - This includes using [SEP-10] to authenticate the user with the SDP server. Please notice that the SDP requires both the `client_domain` and `home_domain` fields to be provided in the `GET ` request, and they should be set as follows: - `client_domain`: the domain of the wallet server that exposes the wallet server's `stellar.toml` file. - `home_domain`: the domain of the SDP's server that was present in the registration link. - `account`: the Stellar account of the receiver's wallet. 1. Launch the deposit flow interactive _in-app browser_ within your mobile app, following the instructions in the [SEP-24] spec. - ATTENTION: the wallet should not, in any circumstances, scrape or attempt to scrape the content from the _in-app browser_ for the recipient's information. - NOTE: it's highly recommended to use an _in-app browser_ rather than a webview. 1. 🎉 Congratulations! The recipient user can now fill out the forms in the _in-app browser_ and register to receive their payment 🎉. Additionally, the wallet should save the link and/or link attributes and associate it with the individual receiving user for these reasons: 1. This is how the wallet will know that the user is associated with a certain org or SDP. 1. Saving the data is useful for reporting and troubleshooting, especially if the wallet needs to justify the source of funds for regulatory or tax purposes. 1. If the payer org wants to pay any cashout fees charged by the wallet or offramp, the wallet will need to know which users and transactions should be invoiced upstream. ### Deferred Deep Links Most likely, the intended recipient will not have the necessary wallet application installed on their device. For this reason, wallets should support the concept of [deferred deep linking], which enables the following flow: 1. The recipient's initial action of clicking the deep link should redirect them to the appropriate app store to download the wallet application. 1. After installing and opening the application, the recipient should be rerouted to the wallet's typical onboarding flow. 1. Once the user has successfully onboarded, the wallet should use the information included in the deep link to kick off the [Wallet Registration Procedure](#wallet-registration-procedure). Deferred deep linking is a feature commonly supported by numerous mobile deep linking solutions, there are third-party services that can be used to implement this functionality, such as Singular, Branch, AppsFlyer, Adjust, and others. [Here](https://medium.com/bumble-tech/universal-links-for-android-and-ios-1ddb1e70cab0) is a blog post with more information on how to implement [deferred deep linking]. The SDP supports a basic link format, such as `https://`. If your wallet's deep linking system needs a more complex structure, you'll have to manage this with a web application. This application should be owned by the wallet provider, and it should be able to receive the deep link, interpret it, and direct the user to the correct location. [deferred deep linking]: https://en.wikipedia.org/wiki/Mobile_deep_linking#Deferred_deep_linking [deep link]: https://en.wikipedia.org/wiki/Mobile_deep_linking [deep linking]: https://en.wikipedia.org/wiki/Mobile_deep_linking [sep-10]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md [sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md --- ## Monitoring(3) This guide explains how the Stellar Disbursement Platform (SDP) exposes runtime metrics and how to hook those metrics into the Prometheus + Grafana stack. ### Metrics Endpoints Both the Dashboard API and the Transaction Submission Service (TSS) expose Prometheus-compatible `/metrics` endpoints. The HTTP server is defined in [serve_metrics.go](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/internal/serve/serve_metrics.go) and is controlled through the following environment variables: - `METRICS_PORT` – Port used by the Dashboard API metrics server (defaults to [8002](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/dev/docker-compose-sdp.yml)). - `METRICS_TYPE` – Monitoring backend in use (currently `PROMETHEUS`). - `TSS_METRICS_PORT` – Port used by the TSS metrics server (defaults to [9002](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/dev/docker-compose-tss.yml)). - `TSS_METRICS_TYPE` – Monitoring backend for the TSS (defaults to `TSS_PROMETHEUS`). When the server starts, it mounts the `/metrics` route and surfaces request, database, and TSS-specific counters and histograms that align with the Grafana dashboards provided in [Grafana README](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/resources/grafana/README.md). ### Local Prometheus and Grafana Stack We provide a Docker Compose file that boots Prometheus and Grafana pre-wired to scrape the SDP metrics endpoints. 1. From the [repository root](https://github.com/stellar/stellar-disbursement-platform-backend), run: ```sh cd dev docker compose -p sdp-multi-tenant -f docker-compose-monitoring.yml up -d ``` This launches Prometheus on port `9090` and Grafana on port `3002` by default [monitoring config](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/dev/docker-compose-monitoring.yml). 2. The Prometheus container loads its configuration from [prometheus config](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/dev/prometheus/prometheus.yml), which targets `host.docker.internal:8002/metrics` by default. Adjust the `targets` list if you run the API on a different host or if you want to scrape the TSS metrics (`host.docker.internal:9002`). 3. Grafana uses the datasource configuration in [datasource](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/dev/grafana/datasource.yaml), which points to the Prometheus instance above. If you have an existing Prometheus deployment, update this URL accordingly. To tear down the monitoring stack, run: ```sh cd dev docker compose -p sdp-multi-tenant -f docker-compose-monitoring.yml down ``` ### Load the SDP Grafana Dashboard 1. Navigate to [http://localhost:3002](http://localhost:3002) and sign in with the default `admin` / `admin` credentials. ![Grafana Login](/assets/SDP/SDP42.png) 2. Click the `+` icon in the top navigation bar (next to the search input), choose `Import dashboard`, and paste the contents of [Grafana Dashboard Json](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/resources/grafana/dashboard.json). ![Import Dashboard](/assets/SDP/SDP43.png) 3. Select the `prometheus` datasource provided by [datasource](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/dev/grafana/datasource.yaml). ![Select Datasource](/assets/SDP/SDP44.png) This dashboard visualizes HTTP request volume/latency, database query timings, and TSS transaction statistics, details described in [Grafana README.md](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/resources/grafana/README.md). All panels can be filtered by method, route, tenant, or instance so you can distinguish traffic between multiple deployments. ### Integrating with External Prometheus Instances If you already operate a Prometheus cluster, add scrape jobs equivalent to the ones in [prometheus config](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/dev/prometheus/prometheus.yml). Each subsystem exposes metrics at `http://:/metrics` (Dashboard API) and `http://:/metrics` (TSS). [Helm](https://github.com/stellar/stellar-disbursement-platform-backend/blob/develop/helmchart/sdp/values.yaml) deployments expose the same configuration knobs through `sdp.configMap.data.METRICS_*` and `tss.configMap.data.TSS_METRICS_*`. Once the new jobs are present, you can import the same dashboard JSON into your existing Grafana deployment or adapt the PromQL queries to your preferred observability suite. --- ## Overview(3) The entire SDP step-by-step process usually looks something like the following after the SDP is deployed and organizational users have been set up: 1. The organization funds the SDP’s distribution account with a Stellar-based asset (e.g. USDC) 2. An administrator logs in to the SDP’s dashboard and uploads a CSV file containing the payment information to initiate a new disbursement 3. The SDP sends a text message to every first-time recipient in the CSV inviting them to download a Stellar-enabled wallet application 4. Meanwhile, the SDP immediately begins making payments to each recipient that already has a wallet registered to them 5. Each first-time recipient clicks a deep link to download the Stellar-enabled wallet application chosen by the organization for this disbursement, downloads the app, and goes through the wallet sign-up process 6. Once the recipient has signed up and their Stellar account has been created, the wallet immediately authenticates with the SDP using parameters from the deep link and opens the SDP registration web view for the recipient to complete verification 7. The user confirms their identity by providing an OTP code sent to their phone number and an additional piece of verification information for security purposes. The SDP supports three different types of verification information: Date of Birth, Personal PIN, and National ID. This information is input by the recipient in a web flow and passes directly to the SDP, meaning the wallet does not need to process or store this information. 8. The SDP verifies the recipient’s information. If it matches the information from the CSV, the SDP automatically makes the payment to the recipient’s Stellar account Graphic representation of flow of funds: ![Flow of Funds](/assets/SDP/SDP1.png) --- ## Security(Admin-guide) This manual outlines the security measures implemented in the Stellar Disbursement Platform (SDP) to protect the integrity of the platform and its users. By adhering to these guidelines, you can ensure that your use of the SDP is as secure as possible. Security is a critical aspect of the SDP. The measures outlined in this document are designed to mitigate risks and enhance the security of the platform. Users are strongly encouraged to follow these guidelines to protect their accounts and operations. ### Implementation of reCAPTCHA Google's reCAPTCHA has been integrated into the SDP to prevent automated attacks and ensure that interactions are performed by humans, not bots. ReCAPTCHA is enabled by default and can be disabled by setting the `DISABLE_RECAPTCHA` environment variable to `true`. Configuration is available at two levels: 1. **Environment default** – Set `DISABLE_RECAPTCHA=true` to apply the setting globally across all tenants. 2. **Tenant override** – Each organization can enable or disable reCAPTCHA via its own settings (UI or API). When present, the tenant-level choice overrides the environment default. Use the following environment variables to control how reCAPTCHA behaves: - `CAPTCHA_TYPE` – `GOOGLE_RECAPTCHA_V2` (default) or `GOOGLE_RECAPTCHA_V3`. - `RECAPTCHA_SITE_KEY` – Google site key issued for the chosen CAPTCHA type. - `RECAPTCHA_SITE_SECRET_KEY` – Google secret key paired with the site key. - `RECAPTCHA_V3_MIN_SCORE` – Minimum allowed score (0.0–1.0, default 0.5) when `CAPTCHA_TYPE=GOOGLE_RECAPTCHA_V3`. **Note:** Disabling reCAPTCHA in production (pubnet) deployments substantially reduces protection against automated abuse. This configuration should be used only when equivalent compensating controls are in place. ### Enforcement of Multi-Factor Authentication Multi-Factor Authentication (MFA) provides an additional layer of security to user accounts. It is enforced by default on the SDP and it relies on OTPs sent to the account's email. MFA is enabled by default and can be disabled in the development environment by setting the `DISABLE_MFA` environment variable to `true`. **Note:** MFA cannot be disabled in production (pubnet) environments due to security risk. ### Request Rate Limiting and Network Protections SDP enforces rate limiting at the HTTP layer to curb scripted abuse. Each unique `` pair is limited to 40 requests within 20 seconds (rolling window). Requests that exceed this threshold receive throttled responses until the window resets. ### Authentication and Authorization Models All authenticated API routes require clients to present either an SDP-issued API key or a JWT derived from the SEP10/SEP24 flows. These two mechanisms run in parallel: JWTs are for interactive users, while API keys enable programmatic integrations with their own scoping model. #### JWT Roles JWTs represent human users that sign in through the UI. After authentication, the platform authorizes them based on the roles assigned to their user account. The primary roles are: - **Owner** – Full control, including creating users, assigning roles, and editing organization configuration. Owner is the only role that can grant or revoke access for others. - **Financial Controller** – Can perform every operational task (wallets, assets, disbursements, statistics) except user management. This role is ideal for finance staff executing payouts. - **Developer** – Manages technical configuration such as wallets, assets, and API keys, and can view statistics; it cannot modify users or financial workflows. - **Business** – Read-only across business data (disbursements, recipients, statistics) but cannot access user management details. - **Initiator** – Creates and saves disbursements but cannot submit them. Mutually exclusive with the Approver role to enforce separation of duties. - **Approver** – Reviews and submits disbursements but cannot create new ones; mutually exclusive with Initiator. Each API endpoint specifies which JWT roles may access it—for example, API key management routes (`/api-keys`) require Owner or Developer, while disbursement creation requires Initiator or Financial Controller and submission requires Approver or Financial Controller. #### API Key Permissions API keys bypass JWT roles and instead embed their own permission scopes. When a request includes an API key, the middleware validates the key, confirms the caller’s IP address is allowed (if restricted), checks the expiration, and finally ensures the key contains the scopes required by the endpoint. API keys are typically used for automation and service-to-service integrations where precise read/write access is needed; creating or rotating them still requires a user with the appropriate JWT role (Owner or Developer) to hit the `/api-keys` endpoints. Available scopes map directly to the major SDP resources: - `read:all`, `write:all` - `read:disbursements`, `write:disbursements` - `read:receivers`, `write:receivers` - `read:payments`, `write:payments` - `read:organization`, `write:organization` - `read:users`, `write:users` - `read:wallets`, `write:wallets` - `read:statistics` - `read:exports` #### Recommended Configuration To enhance security, disbursement responsibilities should be distributed among multiple financial controller users. 1. **Approval Flow**: Enable the approval flow on the organization page to require two users for the disbursement process. The owner can do that at _Profile > Organization > ... > Edit details > Approval flow > Confirm_. 2. **Financial Controller Role**: Create two users with the _Financial Controller_ role on the organization page to enforce separation of duties. The owner can do that at _Settings > Team Members_. 3. **Owner Account Management**: Use the Owner account solely for user management and organization configuration. Avoid using the Owner account for financial controller tasks to minimize the exposure of that account. ### Best Practices for Wallet Management The SDP wallet should be used primarily as a hot wallet with a limited amount of funds to minimize potential losses. #### Hot and Cold Wallets - A hot wallet is connected to the internet and allows for quick transactions. - A cold wallet is offline and used for storing funds securely. - Learn more about these concepts at [Investopedia](https://www.investopedia.com/hot-wallet-vs-cold-wallet-7098461). --- ## Troubleshooting This guide helps you diagnose and resolve common issues with the Stellar Disbursement Platform (SDP). ## Quick Reference | Symptom | Likely Cause | Jump to | | --- | --- | --- | | Payment stuck in "Pending" | TSS issue, missing accounts, or insufficient funds | [Pending Payments](#payment-stuck-in-pending) | | "Resource Missing" in logs | Account doesn't exist on network | [Pending Payments](#payment-stuck-in-pending) | | Payment failed with `op_no_trust` | Receiver missing trustline for asset | [Operation Errors](#payment-failed-operation-error) | | Payment failed with `op_underfunded` | Distribution account low on funds | [Operation Errors](#payment-failed-operation-error) | | Payment failed with `op_no_destination` | Receiver account doesn't exist | [Operation Errors](#payment-failed-operation-error) | | Receiver didn't get invitation | Scheduler config or messaging provider issue | [Invitation Issues](#receiver-not-receiving-invitation) | | Receiver didn't get OTP | Mismatched contact info or provider issue | [OTP Issues](#receiver-not-receiving-otp) | | Channel account errors after testnet reset | Channel accounts were wiped | [Recreating Channel Accounts](#recreating-channel-accounts) | | Payments processing slowly | Not enough channel accounts | [Slow Payments](#slow-payments-due-to-insufficient-channel-accounts) | | "Max attempts exceeded" on verification | Receiver entered wrong verification value too many times | [Verification Locked](#receiver-verification-locked-out) | --- ## Payments ### Payment Stuck in "Pending" {/* #payment-stuck-in-pending */} Payments can get stuck in "Pending" status for several reasons. Work through these checks in order. #### 1. Check TSS Service Health The Transaction Submission Service (TSS) must be running and reachable. ```bash # Check TSS container status docker ps | grep tss # View recent TSS logs docker logs --tail 100 ``` If TSS is down or unreachable, restart it and monitor the logs for errors. #### 2. Verify Distribution Account Funds The distribution account must have sufficient XLM to cover the payment amount plus transaction fees. Use [Stellar Expert](https://stellar.expert/explorer/public) or the Stellar CLI to check the balance. #### 3. Validate Channel Accounts Channel accounts may become invalid after testnet resets. Look for errors like this in your logs:
Example error: "Resource Missing" ``` time="2025-12-19T18:43:37.017Z" level=error msg="[DRY_RUN Crash Reporter] unexpected TSS error: preparing bundle for processing: building transaction: horizon response error: getting account detail: horizon error: \"Resource Missing\" - check horizon.Error.Problem for more information" app_version=6.0.1 asset=XLM channel_account=GBKEVxxxx ... ```
**Diagnosis:** This error means either the **destination account** or the **channel account** doesn't exist on the network. Check both using [Stellar Expert](https://stellar.expert/explorer/public). **Solution:** The receiver's account hasn't been created on the Stellar network. The account must be funded with the minimum balance (currently 1 XLM on mainnet) before it can receive payments. Channel accounts may disappear after testnet resets. See [Recreating Channel Accounts](#recreating-channel-accounts) below. --- ### Payment Failed with Operation Error {/* #payment-failed-operation-error */} When a payment fails, the Status History shows a Horizon error with operation codes that explain why the transaction was rejected. ![Payment Failed Error](/assets/SDP/SDP45.png) #### Reading the Error Look for the `operation codes` at the end of the error message: ``` Extras=transaction: tx_fee_bump_inner_failed - inner transaction: tx_failed - operation codes: [ op_no_trust ] ``` The operation code (e.g., `op_no_trust`) tells you exactly what went wrong. #### Common Operation Codes | Code | Meaning | Solution | | --- | --- | --- | | `op_no_trust` | Receiver hasn't established a trustline for this asset | Receiver must add a trustline for the asset (e.g., EURC) before they can receive it | | `op_underfunded` | Source account doesn't have enough of the asset | Fund the distribution account with more of the asset | | `op_no_destination` | Destination account doesn't exist | Receiver must create and fund their Stellar account first | :::info[Trustlines explained] On Stellar, accounts must explicitly "trust" an asset before receiving it. This is a security feature—it prevents spam tokens. The receiver needs to add a trustline for the specific asset (like EURC) using their wallet or a Stellar tool. ::: #### Example: `op_no_trust`
Full error message ``` horizon response error: StatusCode=400, Type=https://stellar.org/horizon-errors/transaction_failed, Title=Transaction Failed, Detail=The transaction failed when submitted to the stellar network. The `extras.result_codes` field on this response contains further details. Descriptions of each code can be found at: https://developers.stellar.org/docs/data/apis/horizon/api-reference/errors/http-status-codes/horizon-specific/transaction-failed/, Extras=transaction: tx_fee_bump_inner_failed - inner transaction: tx_failed - operation codes: [ op_no_trust ] ```
**Diagnosis:** The receiver account exists but hasn't added a trustline for the asset you're trying to send (in this case, EURC). **Solution:** The receiver must add a trustline for the asset before the payment can succeed. Once they've done so, use the **Retry** button in the dashboard to resubmit the payment. For a complete list of operation result codes, see the [Stellar documentation](https://developers.stellar.org/docs/data/apis/horizon/api-reference/errors/result-codes/operation-specific/payment). --- ### Slow Payments Due to Insufficient Channel Accounts {/* #slow-payments-due-to-insufficient-channel-accounts */} If payments are processing slower than expected, you may not have enough channel accounts. Channel accounts allow the SDP to submit multiple transactions in parallel. Without enough of them, transactions queue up and process sequentially. #### Symptoms - Large disbursements take longer than expected to complete - Payments sit in "Pending" status longer than usual before being submitted #### 1. Check Current Channel Account Count View how many channel accounts are currently configured: ```bash ./stellar-disbursement-platform channel-accounts view ``` #### 2. Add More Channel Accounts Use the `ensure` command to increase the number of channel accounts. This command is idempotent—it only creates new accounts if you have fewer than the specified number: ```bash # Ensure you have at least 10 channel accounts ./stellar-disbursement-platform channel-accounts ensure 10 ``` :::tip[How many channel accounts do you need?] The optimal number depends on your disbursement volume: - **Low volume** (< 100 payments/day): 2–5 accounts - **Normal volume** (> 100 payments/day): 5–10 accounts Start with a conservative number and increase if you notice slow processing times. ::: #### 3. Verify TSS Configuration The TSS service also has a configuration for how many channel accounts it should utilize. Check that your `--num-channel-accounts` flag (or `NUM_CHANNEL_ACCOUNTS` environment variable) matches or is less than the number of accounts you created: ```bash # In your TSS configuration --num-channel-accounts=10 ``` If this value is higher than the actual number of channel accounts available, TSS will only use what exists. --- ## Receiver Communications Issues with invitations, OTPs, and other messages sent to receivers. ### Receiver Not Receiving the Invitation {/* #receiver-not-receiving-invitation */} When you trigger a disbursement targeting an unregistered receiver (via email, SMS, or WhatsApp), they should receive an invitation link to register. If they haven't received it, work through these checks. #### 1. Verify Scheduler Configuration The invitation job runs on a schedule controlled by an environment variable: ```bash SCHEDULER_RECEIVER_INVITATION_JOB_SECONDS=30 ``` **Check:** Is this set to a reasonable interval (10–60 seconds)? If it's set too high or missing, invitations may be significantly delayed. #### 2. Check SDP Logs for Submission Failures {/* #check-logs-submission-failures */} The SDP logs will show whether the message was sent and if the messaging provider accepted or rejected it. ```bash # Look for messaging-related entries docker logs 2>&1 | grep -iE "invitation|otp|message" ``` **Common provider issues:** | Provider | Typical Failure | What to Check | | --- | --- | --- | | AWS SES | Rate limiting, sandbox mode | Are you in production mode? Check sending limits in AWS console | | Twilio (SMS) | Geofencing, unverified numbers | Is the destination country enabled? Is your sender ID verified? | | Twilio (WhatsApp) | Template not approved, 24h window | Is your message template approved? Are you outside the 24h conversation window? | :::tip If you're testing, check spam/junk folders first—especially for email invitations. ::: #### 3. Verify Receiver Contact Info Double-check that the receiver's contact information (email, phone number) in the disbursement file is: - Correctly formatted (e.g., phone numbers include country code) - Valid and reachable - Not a duplicate that was already processed --- ### Receiver Not Receiving OTP During Registration {/* #receiver-not-receiving-otp */} During registration, receivers enter the contact details (phone number or email) that the payer used when submitting the disbursement. The SDP sends an OTP to verify ownership of that contact method. #### 1. Check for Mismatched Contact Info (Most Common) The most frequent cause is the receiver entering a different email or phone number than what the payer submitted—often without realizing it. **How to verify:** Check the `receiver_registration_attempts` table, which logs attempts from contacts that couldn't be matched to any receiver in the system. ```sql SELECT * FROM sdp_.receiver_registration_attempts ORDER BY attempt_ts DESC LIMIT 20; ``` If you see the receiver's attempted contact info here, it means: - They entered something different from what's on file - You may need to coordinate with the receiver to confirm which contact info is correct - If the payer made an error, you may need to update the receiver's contact info or create a new disbursement #### 2. Check for Provider Issues If the contact info matches but the OTP still isn't arriving, the issue is likely with the messaging provider. See [Check SDP Logs for Submission Failures](#check-logs-submission-failures) above for common provider issues and how to diagnose them. --- ## Receiver Registration ### Receiver Verification Locked Out {/* #receiver-verification-locked-out */} During SEP-24 registration, receivers must confirm their verification value (e.g., date of birth or PIN) to prove their identity. If a receiver enters the wrong value too many times, the system locks them out and displays: ``` The number of attempts to confirm the verification value exceeded the max attempts. ``` This is **not** related to OTP—it specifically means the receiver exceeded the maximum allowed attempts (15) for entering their verification value (PIN, date of birth, etc.). A common cause is receivers confusing the verification field with the OTP, entering the wrong type of value repeatedly. #### 1. Identify the Locked Receiver Query the `receiver_verifications` table to find locked accounts: ```sql SELECT * FROM sdp_.receiver_verifications WHERE attempts >= 15; ``` #### 2. Confirm the Receiver's Identity Cross-reference the receiver ID to make sure you're resetting the right account: ```sql SELECT * FROM sdp_.receivers WHERE id = ''; ``` #### 3. Reset the Verification Attempts Once you've confirmed the correct receiver, reset their attempt counter: ```sql UPDATE sdp_.receiver_verifications SET attempts = 0 WHERE attempts >= 15 AND receiver_id = ''; ``` After resetting, the receiver can try again. You may need to walk them through the registration flow to ensure they enter the correct verification value (not the OTP) in the right field. --- ## Channel Accounts ### Recreating Channel Accounts {/* #recreating-channel-accounts */} After a testnet reset, your channel accounts no longer exist on-chain but are still referenced in the database. You need to clean up invalid accounts and create new ones. Run these commands inside the TSS container: ```bash # Step 1: Remove invalid accounts from the database ./stellar-disbursement-platform channel-account verify --delete-invalid-accounts # Step 2: Create new channel accounts (adjust the count as needed) ./stellar-disbursement-platform channel-account ensure 10 ``` :::tip The `ensure` command is idempotent—it only creates accounts if you have fewer than the specified number. Running `ensure 10` when you already have 10 valid accounts does nothing. ::: --- ## Still Stuck? If you've worked through the relevant sections and the issue persists: 1. **Collect logs** from all relevant services (SDP, TSS, Anchor Platform) 2. **Note the exact error message** and when it started occurring 3. **Check for recent changes** to configuration, environment, or network (e.g., testnet reset) Contact us either by opening an issue on our [Backend GitHub repository](https://github.com/stellar/stellar-disbursement-platform-backend/issues) or [Frontend GitHub repository](https://github.com/stellar/stellar-disbursement-platform-frontend/issues) with the details above and the version of SDP you're running. We'll help you troubleshoot further! We're also available on Discord in the [#bulk-disbursements](https://discord.com/channels/897514728459468821/1310800776331006002) channel. --- ## User Interface A description and walkthrough of the various parts of the SDP user interface. --- ## Analytics The Analytics page provides comprehensive insights into various aspects of financial transactions, enabling the user to track and understand key payment-related metrics. As more metrics and statistics become available, additional tiles will be added to this screen. The page displays information such as the successful payment rate, the total number of successful payments, the number of failed payments, and the number of remaining payments. Additionally, it shows the total amount disbursed, the average amount per transaction, the total amount in USDC, and the number of individuals and wallets involved in the transactions. In more detail: - The "Successful payment rate" indicates the percentage of payments processed successfully out of total attempted payments. - "Successful payments" shows the count of all transactions that have been completed successfully. - "Failed payments" reveals the number of transactions that didn't go through, which can help identify issues with the payment process. - "Remaining payments" provides the number of transactions that are yet to be processed. - "Total disbursed" offers information on the total amount of funds that have been sent out. - The "Average amount" offers an average value of all the transactions that have taken place in USDC. - "USDC" reveals the total amount of funds in the system, denominated in USDC. - "Individuals" represents the number of people involved in these transactions. - "Wallets" indicates the number of unique digital wallets involved in the transactions. ![Analytics](/assets/SDP/SDP24.png) --- ## Circle Configuration If the tenant was created with a [Circle] distribution account, then the tenant owner will need to manually configure that account from within the SDP dashboard. Once a user with owner privileges logs in, they will see a banner at the top of the page saying that the Circle account is pending configuration: ![Circle Configuration Banner](/assets/SDP/SDP30.png) Clicking on the banner will take the user to the Distribution Account section, where they can enter the Circle API key and the Circle Wallet ID. ![Circle Configuration](/assets/SDP/SDP31.png) :::info The API key will get stored in the database encrypted by the key `DISTRIBUTION_ACCOUNT_ENCRYPTION_PASSPHRASE`, while the Wallet ID is stored in plain text. The Wallet ID is used to identify the Circle (internal) account when making disbursements. It's useful because a Circle account can have multiple wallets, each one with different currencies and balances. ::: [Circle]: https://www.circle.com --- ## Dashboard Home The main page of the dashboard contains a summary of recent disbursement activity and key performance metrics. This includes: - Successful payment rate: The percentage of payments completed successfully (pending payments are not counted as successful). - Successful payments: The total number of payments that have been successfully made. - Failed payments: The total number of payments that failed to process. - Remaining payments: The total number of payments that are scheduled but haven't been processed yet. - Total disbursed: The total amount of funds successfully sent to receivers by an organization over time. - Individuals: The total number of individuals who are set to receive disbursements. - Wallets: The total number of wallets used within the SDP. This usually equals the number of individuals but it is possible for each person to have more than one wallet. ![Dashboard Home](/assets/SDP/SDP19.png) On the left side of the Stellar Disbursement Platform dashboard is the organization logo and tabs to help you navigate through the platform. They include: - Home: This is the main dashboard that provides an overview of your organization’s activities. - Disbursements: This section shows you the history and details of all disbursements. - Receivers: Lists of individuals who are set to receive disbursements. - Payments: Here you can find the history and granular details of all payments. - Wallets: Information related to your organization’s wallet, the source of funds for your disbursements. - Analytics: Data visualization tools to help you analyze your disbursements and payments. - Profile: Manage your personal and organizational information. - Settings: Adjust the settings of the SDP according to your preference. The dashboard also shows a Recent Disbursements list, providing a quick snapshot of your most recent disbursements. Each entry shows: - Disbursement Name: The unique name given by your organization to the disbursement operation. - Total payments: Total number of payments within the specific disbursement. - Successful: Number of payments that have been successfully sent from the SDP Distribution Wallet to receiver wallets so far. - Failed: Number of payments that failed to process. - Remaining: Number of payments that are scheduled but haven't been processed yet, usually because the receiver has not yet set up a wallet. - Created at: The time and date the disbursement was created, displayed in your local timezone. - Total amount: The total value of the disbursement in the appropriate asset. - Amount disbursed: The amount of the disbursement that has been successfully paid out so far. --- ## Disbursements The Disbursements page provides a paginated list of all disbursements, detailing each disbursement's status and related payment information. ![Disbursements](/assets/SDP/SDP20.png) The page contains the following: - Drafts: Click the Drafts button at the top right to go to a list of disbursements that have been created but not yet submitted. - New disbursement: Click the New Disbursement button to start the process of creating a new disbursement. - Search by disbursement name: Input the name of a disbursement to quickly find specific details. - Filter: Allows you to narrow down the disbursement list based on specific criteria like status or creation date. - Export: Use this option to download the disbursement data in CSV format. - Disbursement detail: Each disbursement is displayed with the following details: - Disbursement name: The unique name assigned to the disbursement by your organization. - Total payments: The total number of payments within the disbursement. - Successful: The number of payments within the disbursement that have been processed successfully from the distribution account to registered wallets. - Failed: The number of payments that failed during processing. - Remaining: The number of payments that are yet to be processed. - Created at: The date and time when the disbursement was created. - Total amount: The total value of the disbursement in the appropriate asset. - Amount disbursed: The amount of the disbursement that has already been paid out. - You can click into an individual disbursement to see its details, including a full list of receivers and payments. --- ## Payments(User-interface) The Payments page provides a list of all payments, detailing each payment's status and related information. ![Payments](/assets/SDP/SDP22.png) The Payments page includes: - Search by payment ID: Enter a payment ID to find specific payment details quickly. - Filter: This tool allows you to narrow down the payment list based on specific criteria. - Export: This option lets you download payments data in CSV format. - Payment Details: Each payment is listed with the following details: - Payment ID: A unique identifier assigned to each payment. - Wallet address: The digital wallet address where the payment is sent. A dash ("-") signifies that the wallet address is not yet set. The payment cannot be made until the receiver wallet is created and linked in the SDP. - Disbursement name: The name of the disbursement associated with the payment. - Completed at: The date and time when the payment was completed. A dash ("-") signifies that the payment has not yet been completed. - Amount: The value of the payment in the appropriate asset. - Status: The current state of the payment. The options are: - `DRAFT`: Non-terminal state for payments that were registered in the database but their disbursement has not started yet. Payments in this state can be deleted or transitioned to `READY`. - `READY`: Non-terminal state for payments that are waiting for the receiver to register. As soon as the receiver registers, the state is transitioned to `PENDING`. - `PENDING`: Non-terminal state for payments that were marked by the distribution account's respective platform (currently either TSS or Circle) for submission to the Stellar network. They may or may not have been submitted to the network yet. - `PAUSED`: Non-terminal state for payments that were manually paused. Payments in this state can be resumed. - `SUCCESS`: Terminal state for payments that were successfully submitted to the Stellar network. - `FAILED`: Terminal state for payments that failed when submitted to the Stellar network. Payments in this state can be retried. - `CANCELED`: Terminal state for payments that were either manually or automatically canceled. - You can click into an individual payment to see its details, including a granular status history and Stellar blockchain details. --- ## Receivers The Receivers page displays a list of individuals set to receive payments, with wallet information and payment history. This information allows you to track and manage the payments made to each receiver, and provides a snapshot of each receiver's interaction with a disbursement. ![Receivers](/assets/SDP/SDP20x.png) The Receivers page includes the following: - Search and Filter - At the top of the Receivers page, there are several tools available to help you find specific information: - Search by phone number: Enter the phone number of a receiver to quickly find their information. - Filter: Use this tool to narrow down the list of receivers based on specific criteria. - Export: This allows you to download the receiver data in CSV format. - Receiver Details: Each receiver is listed with the following details: - Phone number: The receiver's phone number, which serves as a unique identifier within the SDP. - Wallet provider(s): The provider of the receiver's digital wallet (usually a non-custodial app on a person’s phone). - Wallets registered: The number of wallets registered and successfully linked to the SDP per receiver. A dash ("-") indicates no wallet has been registered. - Total payments: The total number of payments intended for the receiver. - Successful: The number of payments that have been successfully sent to the receiver’s wallet. - Created at: The time and date the receiver was created in the system, displayed in your local timezone. - Amount(s) received: The total amount the receiver has successfully received in the appropriate asset(s). - You can click into an individual receiver to see their details, including their linked wallets and full payments history. --- ## Wallets The Wallets page provides detailed information about your distribution account, which is the primary Stellar account from which your disbursements are made. ![Wallets](/assets/SDP/SDP23.png) The Wallets page includes the following: - Distribution account public key: This is your unique identifier for your distribution account on the Stellar network. You use this public key to receive funds in your distribution account. - Balance: This section displays the current balance of different digital assets in your distribution account: - USDC, EUROC, etc: This is the current balance available for making payments within a disbursement. - XLM: This is the balance of Stellar Lumens. This is used to fund the distribution account (base reserve) and transaction fees associated with making payments. This is for informational purposes and is not the source of funds for disbursements. In general, you do not need to worry about maintaining this, as Stellar network fees are very low. ### Adding Funds Add funds to your distribution account: You can deposit Stellar-based digital assets into your distribution account by sending them to the provided public key. Make sure your account has a trustline to the asset before you send funds. As a general principle, do not use your distribution account as a long-term holding place for money. It is meant to be a pass-through wallet to fund disbursements. --- ## Admin (Tenant Management) The Admin API oversees the management of tenants within the system, facilitating tasks such as provisioning new tenants, updating their information, and retrieving tenant data. ```mdx-code-block ``` --- ## API Keys API Keys functionality allows to create access key with granular permissions and resource management. ```mdx-code-block ``` --- ## Provide Multi-Factor Authentication Governs the multi-factor authentication process for SDP user login, including the ability to remember the device so MFA is not always required. Request --- ## Authentication Authentication controls the log in/log out process for all SDP users, as well as the token refresh process. Authentication uses a JWT approach signed with an ES256 private key. ```mdx-code-block ``` --- ## Balances Endpoints related to balances. A balance is an amount of a particular asset held by an organization, tenant, or account. ```mdx-code-block ``` --- ## Bridge Integration Bridge integration endpoints for connecting organizations with Bridge services. **Integration Flow:** 1. Organization opts into Bridge (OPTED_IN status) 2. Complete KYC verification process via Bridge 3. Create virtual account for USD deposits (READY_FOR_DEPOSIT status) 4. Receive USD deposits that are automatically converted to USDC on Stellar **Status Description:** - NOT_ENABLED → Bridge service not configured - NOT_OPTED_IN → Organization hasn't opted in - OPTED_IN → Organization opted in, KYC link created - READY_FOR_DEPOSIT → Virtual account created, ready for deposits - ERROR → Integration error occurred ```mdx-code-block ``` --- ## Create API Key Creates a new API Key to access SDP endpoints. API Key can be configured to have a granular read/write access, also API key can be restricted to the specific IP or range of the IPs. Request --- ## Create Asset This endpoint is used to create a new asset that can be used in a Disbursement. Note: the organization must hold a balance in a particular asset to use it in a disbursement. Request --- ## Create Direct Payment Creates a new direct payment that is immediately sent to the specified receiver if they have a valid registered wallet. Direct payments bypass the disbursement workflow and are processed instantly. Request --- ## Create Disbursement Creates a new disbursement in `draft ` state with basic details. Important: a disbursement is not triggered until the organization adds receivers through the Upload Disbursement Instructions endpoint and the status changes from `draft ` to `ready `. Request --- ## Create Receiver Creates a new Receiver. Allows clients to create a single receiver record, which can later be referenced in payments or disbursements. Request --- ## Create Tenant Create Tenant Request --- ## Create User This endpoint creates a new SDP user as the result of an SDP owner adding their information in the UI. It also handles sending the invite email. Request --- ## Create Wallet Creates a new wallet provider that can be used for disbursements. The wallet must be configured with supported assets and proper authentication domains. Request --- ## Default Tenant Sets the tenant specified in the request body as the default one, resolving all the incoming API request to that tenant when the env `SINGLE_TENANT_MODE` is set to true. Once set, the default tenant can be overwritten but never unset, although it is only effective when `SINGLE_TENANT_MODE` is set to true. Default tenant is useful for development purposes or when the SDP is used by a single organization. This allows the organization to skip specifying the tenant in every request and simplifies the SDP setup operationally by removing the need of providing wildcard TLS certificates for multi-tenant configurations. Request --- ## Deletes a Draft Disbursement Deletes a Disbursement in Draft or Ready Status by `id`. Request --- ## Delete API Key Permanently deletes an API key. This action cannot be undone. Once deleted, the API key will no longer be able to authenticate API requests. Request --- ## Delete Asset This endpoint is used to soft delete an asset. Request --- ## Delete Wallet Soft deletes a wallet provider. Request --- ## Disbursements(Api-reference) Endpoints related to disbursements. A disbursement is a group of payments sent to multiple individuals at once. An SDP user with the appropriate role triggers a new disbursement through the SDP dashboard by uploading a list of receivers and amounts. When the receiver has linked their wallet to the SDP, the payment automatically begins. SDP users can track their disbursements in real-time through the SDP dashboard. Each disbursement must have a unique name defined by the organization. ```mdx-code-block ``` --- ## Download Disbursement Instructions Allows an SDP user to download the raw CSV file that was uploaded when creating the disbursement. This will only return results after instructions have been attached to a draft disbursement. Request --- ## Export Disbursements Exports a CSV file of disbursements. Request --- ## Export Payments Exports a CSV file of payments. Request --- ## Export Receivers Exports a CSV file of receivers. Request --- ## Forgot Password Sends an email with a token to an SDP user who has gone through the Forgot Password process. Request --- ## Get All Assets Fetches the list of available assets to populate the dropdown box in the New Disbursement flow. Note: the organization must hold a balance in a particular asset to use it in a disbursement. --- ## Get All Roles Fetches available SDP roles, such as owner, financial controller, business user, and developer. --- ## Get All Tenants Get All Tenants --- ## Get All Users Fetches all SDP users within the organization, whether they are active yet or not. --- ## Get All Wallets Fetches the list of available wallet providers to populate the dropdown box in the New Disbursement flow. The organization should coordinate with the wallet provider before selecting a particular wallet provider for a disbursement. By default, soft-deleted wallets are excluded from the response. Use the `include_deleted` parameter to include them. Request --- ## Get API Key Details Retrieves a specific API key by its ID. Returns the API key details including permissions and restrictions, but does not include the actual key value for security reasons. Request --- ## Get Bridge Integration Status Retrieves the current Bridge integration status and information for the organization. Returns live data from the Bridge API including KYC status and virtual account details. --- ## Get Organization (Circle) Balances ATTENTION, this endpoint is only enabled when the tenant distribution account type is `CIRCLE`. --- ## Get Organization Info This endpoint returns the organization's info. It is used in many places across the UI. It returns the name in the navbar and the public key of the organization’s distribution account. --- ## Retrieve Organization Logo Retrieves the logo of the organization for display in the UI navbar. --- ## Get Profile Fetches the individual information of the logged in user to populate the Profile page. --- ## Get Receiver Registration Info Returns the registration context for the receiver, including organization details and reCAPTCHA configuration. --- ## SEP-24 Info Returns supported assets and feature flags for SEP-24 registration deposits. --- ## List All Disbursement Receivers Fetches a list of receivers within a specific disbursement using the disbursement `id `. This endpoint supports pagination and sorting. Request --- ## List All Disbursements Fetches all disbursements the organization has created. This endpoint supports pagination. The response includes basic aggregations on payments within the disbursement. Request --- ## List All Payments(Api-reference) Returns all individual payments matching the request criteria. This endpoint supports pagination, and filtering on payment status, type, receiver ID, and timestamp. Each payment has details on the transaction itself, receiver, disbursement, asset, status history, and blockchain information. Request --- ## List All Receivers Returns all receivers matching the request criteria. This endpoint supports pagination and filtering on receiver status, receiver attributes, and timestamp. Each payment has details on the receiver, high-level payments metrics, and wallets associated with the receiver. Request --- ## List API Keys Retrieves all API keys created by the current user. The results are ordered by creation date in descending order (most recent first). API key values are not included in the response for security reasons. --- ## List Verification Types Returns the supported receiver verification types. --- ## List Registration Contact Types Returns the supported registration contact types for disbursement instructions. --- ## Log In Allows credentialed SDP users to log in to the SDP dashboard with a password. Note: all passwords must be at least 8 characters long and a combination of uppercase letters, lowercase letters, numbers, and symbols. Request --- ## Organization Organization endpoints manage the process of getting and updating organizational profile information. The organization's profile has basic information set at the time of SDP deployment. It can be modified by the Owner. Organizations can also manage their preferences, like which assets to use, through these endpoints. ```mdx-code-block ``` --- ## Circle Account Setup Updates the Circle configuration for the organization. Only account owners have permission to do this. Note: in the first time configuration, all fields are mandatory, but after it is been configured at least once, you can replace a single field at a time. Request --- ## Payments(Api-reference) Endpoints related to payments. An SDP payment is an individual payment from an organization to a receiver. Each payment is part of a disbursement and occurs on the Stellar network. Granular payment status is stored in the SDP database and can be viewed in real-time on the SDP dashboard. ```mdx-code-block ``` --- ## Profile Profiles endpoints manage the process of getting and updating individual profile information. Profile information is set when the account is created and can be updated by the user on the SDP dashboard Profile page. Note: profiles never refer to receivers of funds. ```mdx-code-block ``` --- ## Provide Signed Challenge Transaction Allows the wallet to post the signed SEP-10 challenge transaction. More information [here](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#challenge). The endpoint url can change but is provided in the toml file under the `WEB_AUTH_ENDPOINT ` variable. Request --- ## Receivers(Api-reference) Endpoints related to receivers. A receiver is an individual receiving a payment in a disbursement. The receiver is tracked by phone number to reduce the need for personally identifiable information. Each receiver must be unique within the disbursement. Each receiver will have at least one wallet associated with them. The wallet public key will remain null until the receiver registers with a wallet provider and links the wallet to the SDP through SEP-24. Receivers must verify their identity through that process, which requires the SDP to store verification information on receivers like date of birth, national ID number, or personal PIN. This information can be updated by the organization through the receiver endpoints. ```mdx-code-block ``` --- ## Refresh Token A user’s token expires after 15 minutes. This endpoint handles refreshing the user’s token without disrupting their experience. It is triggered within the 30-second window before the token expires. --- ## Registration The registration endpoints guide the process for a receiver to verify their identity and link their wallet address to an SDP. The registration process only needs to happen once per receiver to link their wallet. Only SDP-compatible wallet providers can facilitate the registration process. These endpoints must be supported and hit by the wallet providers after the receiver gets the initial invite. After the wallet address is successfully linked, the payment automatically begins. There are two parts to the registration flow. First, the wallet must authenticate and initiate a registration flow using the Anchor Platform Endpoints defined below. Note that these endpoints are hosted on a different host than the Stellar Disursement Platform. The second part of the registration flow is handled by the webview that is opened within the wallet application. This webview uses the endpoints defined in the Stellar Disbursement Platfrom Endpoints section to complete the registration process. The wallet application can chose not to use the webview and intstead integrate directly with the API. ```mdx-code-block ``` --- ## Request Challenge Transaction Allows the wallet to get the SEP-10 challenge transaction to be signed. More information [here](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#challenge). The endpoint url can change but is provided in the Get Stellar toml endpoint response under the `WEB_AUTH_ENDPOINT` variable. Request --- ## Request Registration URL The deposit endpoint allows a wallet to get deposit information from an anchor, so a user has all the information needed to initiate a deposit. It also lets the anchor specify additional information that the user must submit interactively via a popup or embedded browser window to be able to deposit. Please check the detailed documentation [here](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#deposit-2). Request --- ## Reset Rassword Allows an SDP user who has gone through the Forgot Password process to set their new password with a token sent via email. Request --- ## Reset Password Updates the password for the logged in user. Request --- ## Retrieve a Disbursement Fetches information on a specific disbursement by `id `. Request --- ## Retrieve a Payment Fetches detailed information on a specific payment by `id `. Request --- ## Retrieve a Receiver Fetches detailed information on a specific receiver by `id `, including all associated wallets. Request --- ## Retrieve a Tenant Retrieve a Tenant Request --- ## Retrieve All Statistics Fetches all metrics on all disbursements the organization has created. The response includes basic aggregations on payments, receivers, receiver wallets, and assets. --- ## Retrieve Disbursement Statistics Fetches metrics on a specific disbursement by `id `. Request --- ## Retrieve Stellar Info File Allows the wallet to fetch the SEP-10 server url, SEP-10 signing public key and SEP-24 server url. --- ## Retry Payments Retries failed payments by ID. Request --- ## Send One-Time Passcode This endpoint generates a 6-digit OTP and sends it to the user contact (email or phone number SMS) provided in the request body, as long as it matches the receiver contact info stored in the SDP. A valid SEP-24 token should be included in this request's Authorization header. Request --- ## Soft delete a Tenant Soft delete a Tenant Request --- ## Start Wallet Registration Serves the SEP-24 interactive registration UI. Request --- ## Statistics Statistics endpoints return general aggregated data per organization, as well as disbursement-specific metrics. SDP users can use this data to monitor their disbursements over time. ```mdx-code-block ``` --- ## Update a Disbursement Status Updates the status of a disbursement according to the state machine. The disbursement must move from `draft ` to `ready ` in order to start the disbursement and trigger payments. Payments will start as soon as this endpoint is hit. A disbursement can also be moved into `paused ` state by an SDP user to prevent further payments from going out and restarted when they are ready. Request --- ## Update a Tenant This endpoint updates the Tenant data. Request --- ## Update API Key Updates an existing API key's permissions and IP restrictions. The API key name and expiration date cannot be modified after creation. Request --- ## Update Bridge Integration Status Updates the Bridge integration status. Supports two main operations: 1. Opt-in to Bridge (status: "OPTED_IN") - Creates KYC link for organization onboarding 2. Create Virtual Account (status: "READY_FOR_DEPOSIT") - Creates virtual account after KYC approval Request --- ## Update Organization Profile Updates the organization profile details. Only account owners have permission to do this. Note: both fields are optional but at least one should be sent in the request. It is not necessary to set the header Content-Type for this request. It is set automatically by the HTTP client. Request --- ## Update Payment Status Cancels a payment by setting its status to `CANCELED`. Request --- ## Update Receiver Wallet Status Updates a receiver wallet's status. Only `READY` is supported. Request --- ## Update Receiver Wallet Updates a receiver wallet's Stellar address and memo for user-managed wallets. Request --- ## Update a Receiver This endpoint allows an organization to add and update information on the receiver, including email address, external ID, date of birth, personal PIN, and national ID number. The response includes all information on the receiver. Request --- ## Update User Activation Status Updates the SDP user’s activation status. This endpoint is primarily used to move a user into active status when they accept the invite to join an SDP organization account and create a password. Request --- ## Update User Profile Updates the profile details of the logged in user. Note: all fields are optional but at least one should be sent in the request. Request --- ## Update User Role Updates an SDP user’s role by `user_ID `. Request --- ## Update Wallet This endpoint is used to enable or disable a wallet provider. Note: the organization should coordinate with the wallet provider before selecting a particular wallet provider for a disbursement. Request --- ## Upload Disbursement Instructions Adds a file containing a list of receivers to a `DRAFT` disbursement. This step is required before a disbursement can begin. The file must be a CSV and has a different possible formats according with the disbursement configuration, and they can be found at [public/resources/disbursementTemplates](https://github.com/stellar/stellar-disbursement-platform-frontend/tree/58873bbf36cff4614e603daf449079b1d9fad24a/public/resources/disbursementTemplates). The operation is idempotent, guaranteed by deleting and recreating the disbursement attributes when this endpoint is called. Request --- ## Users The users endpoints facilitate the creation of new SDP users - including setting the appropriate role, sending an email invitation, and activating a user - and managing roles. ```mdx-code-block ``` --- ## Verify Receiver Registration This endpoint verifies the receiver's registration by validating the OTP and other verification values provided in the request body. A valid SEP-24 token should be included in the Authorization header of the request. Request --- ## Issuing Assets vs. Creating Custom Tokens: Key Differences & Best Practices :::info The term "custom token" has been deprecated in favor of "contract token". View the conversation in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). ::: # Stellar Assets and Contract Tokens Tokens exist in two forms on Stellar: 1. Assets issued by Stellar accounts (`G...` addresses) and their built-in [Stellar Asset Contract (SAC)][sac] implementation, and 2. [Contract tokens][ti] issued by a deployed WASM contract (`C...` addresses). Several factors can help you determine whether to issue an asset on Stellar or create a contract token with a smart contract for your project. However: ### TL;DR If possible, we recommend issuing a Stellar asset and using the SAC to interact with that asset in smart contracts or to send to contract addresses. More on why below. ## Issuing assets on Stellar Stellar has first-class support for asset tokenization — issuing an asset can be done using a [built-in transaction](./quickstart.mdx) without the development of a smart contract. Stellar’s transactions are fast and cost-effective, making the network great for remittances and micropayments. It also has built-in features for compliance, asset management, and auditing. If you are looking to perform transfers of value, issuing assets on Stellar has all the needed capabilities. Stellar assets: - Are compatible with Stellar ecosystem products (such as Stellar wallets) and other ecosystem products (such as exchanges). - Benefit from [anchors](../learn/fundamentals/anchors.mdx), the bridges between the Stellar network and traditional financial systems. Explore the global [Stellar anchor directory](https://anchors.stellar.org) for further details. - Give the issuer granular control over asset management with features that allow the issuer to [name the asset](./control-asset-access.mdx#naming-an-asset), [determine access control](./control-asset-access.mdx#controlling-access-to-an-asset-with-flags), [limit asset supply](./control-asset-access.mdx#limiting-the-supply-of-an-asset), [publish asset information](./publishing-asset-info.mdx), and [ensure compliance](./anatomy-of-an-asset.mdx#compliance). :::note Note that while these items are also possible with smart contract tokens, it is more work to build the token contract rather than using the already-implemented features of Stellar asset tokens. ::: Assets issued on the Stellar network are accessible to smart contracts with the use of that asset’s Stellar Asset Contract (SAC). ### Stellar Asset Contract The Stellar Asset Contract (SAC) is compiled into the protocol layer and allows smart contracts to interact with assets issued on Stellar. An instance of the SAC can be deployed for every Stellar asset by anyone who wants to interact with the asset from a contract. The SAC has access to all account balances (for XLM) and trustline balances (for all other assets) as well as smart contract token balances. Read more about the SAC [here][sac]. Learn how to deploy a Stellar Asset Contract for an asset in [this How-To Guide](../tools/cli/cookbook/deploy-stellar-asset-contract.mdx). **Benefits of the SAC:** - Compatibility: the SAC benefits from Stellar assets' existing interoperability. - Cost and resource efficiency: the SAC is built into the protocol instead of being a contract that runs in a virtual machine. Each function within the SAC will be more resource-efficient than its contract-coded counterpart. - Less work: you don’t have to write an entirely new contract. A Stellar asset’s SAC already exists on the network and just needs to be deployed to be used. - Customization: Admin addresses can be contracts. Asset issuers can set a different smart contract as an admin for their asset’s SAC. Making the admin another smart contract allows the addition of custom and decentralized logic for the assets admin capabilities, such as authorizing balances and trust lines, minting tokens, etc. **Downside of the SAC:** - Other than the customization noted above, it is not possible to modify the behavior of Stellar assets or their SAC. If you’re looking to use assets in a way not supported by Stellar assets, you can create your own smart contract token using the token interface and all applications that interact with tokens using the token interface will be able to interact with the contract token. ## Contract tokens If you have a unique use case where the capabilities Stellar Assets are not sufficient, you can create a contract token that implements the [token interface][ti]. The token interface specifies the functions and events a contract must implement to be compatible with applications that use tokens. The SAC also implements the token interface and applications that interoperate with the token interface can seamlessly interact with Stellar assets and contract tokens. :::note Smart contracts cannot use Stellar assets unless that Stellar asset has a deployed SAC. Anyone can deploy the SAC for a Stellar asset to its reserved address. ::: **These example scenarios are not possible with the SAC and demonstrate what you could use the token interface for:** - As the creator of a new token, you decide to implement a feature within your token smart contract that enables you to receive a 1% fee from every transaction involving your token. Whenever someone transfers your token to another user, 1% of the transferred amount is automatically deducted and sent to a designated wallet address that you control. - You want to develop a factory contract that automates the creation of instances of a specific token. This contract serves as a centralized and standardized way to deploy new token contracts on demand without manual intervention each time a new instance is needed. ## Helpful links - [Issue an asset tutorial][how-to-issue] - [Stellar Asset Contract][sac] - [Token Interface][ti] [how-to-issue]: ./how-to-issue-an-asset.mdx [sac]: ./stellar-asset-contract.mdx [ti]: ./token-interface.mdx --- ## Assets Overview & Comparison :::info The term "custom token" has been deprecated in favor of "contract token". View the conversation in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). ::: This brief compares the three primary tokenization models available on the Stellar network: - Stellar Assets (with built-in Stellar Asset Contract), - [SEP-41][sep-41] Contract Tokens, and - [SEP-57][sep-57] T-REX Tokens for Regulated EXchanges to help issuers select the most appropriate model for their use case. ## Tokenization Model Comparison | Dimension | Stellar Asset (with SAC) | SEP-41 Contract Token | ERC-3643 (T-REX) | | --- | --- | --- | --- | | **Token Implementation** | Trustlines + Operations + SEP-41 via SAC | SEP-41 interface implemented by a smart contract, fully extensible | SEP-41 with extensible compliance logic | | **Programmability** | ❌ Protocol-defined | ✅ Fully customizable | ✅ Fully customizable + compliance rules | | **Interaction Method** | Native ops (accounts) and contract calls (via SAC) | Contract calls | Contract calls | | **Admin Control** | Issuer flags + optional admin via SAC | Custom contract logic | Built-in compliance and rule enforcement | | **Cost & Speed** | Very low (native ops) / Moderate (via SAC) | Moderate | Higher | | **Ecosystem** | Stellar payments, DEX, and smart contracts | Stellar DeFi & dApps / Interop with other L1s | Institutional DeFi / Interop with other L1s | | **Trustline Required** | ✅ Yes\* | ❌ No | ❌ No | | **Ledger Storage Fees/Expiry** | 0.5 XLM per trustline, no expiry\* | [Rent](../learn/fundamentals/lumens#rent) and [Archives](../learn/fundamentals/contract-development/storage/state-archival) | [Rent](../learn/fundamentals/lumens#rent) and [Archives](../learn/fundamentals/contract-development/storage/state-archival) | | **Ideal For** | Payments, simple assets (fiat based stablecoins), Stellar smart contracts integration | DeFi and custom tokenomics | Institutional RWAs with compliance | | **Source Code** | [Built-in Stellar Asset Contract](https://github.com/stellar/rs-soroban-env/tree/main/soroban-env-host/src/builtin_contracts) | [OpenZeppelin Fungible Token reference](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible) | [OpenZeppelin T-REX (RWA token) reference](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/rwa) | | **Relevant SEPs** | [SEP-1 (Stellar Info File)][sep-1] [SEP-14 (Dynamic Asset Metadata)][sep-14] [SEP-41 (Soroban Token Interface)][sep-41] | [SEP-41 (Soroban Token Interface)][sep-41] | [SEP-41 (Soroban Token Interface)][sep-41] [SEP-57 (T-REX / Token for Regulated EXchanges)][sep-57] | _\* Starting with Yardstick, Protocol 26, by assigning a smart contract (`C...` address) as the owner of a Stellar Asset Contract, balances and transfer rules can be fully managed within the contract. Additionally, smart contracts can create trustlines programmatically using the SAC's [`trust` function](./stellar-asset-contract.mdx#creating-trustlines-from-a-contract), so a missing trustline no longer needs to be set up in a separate, prior transaction._ ## 1. Stellar Assets (with Built-in [Stellar Asset Contracts][sac]) ### What this category encompasses All assets issued by Stellar accounts (G…` addresses) are **Stellar Assets**. These assets can be interacted with in two forms: - **Stellar Asset** Held in trustlines and transferred via payment operations. - **Built-in Smart Contract (Stellar Asset Contract)** A [Stellar Asset Contract (SAC)][sac] can be deployed to a contract address for the asset, implementing the SEP-41 token interface so the asset can be used in smart contracts. Anyone can deploy this contract to enable interacting with the asset in smart contracts. ### How they work - For protocol-level behavior and trustline semantics, see [Asset Design Considerations](./control-asset-access.mdx). - Implemented at the protocol level, the **Stellar Asset Contract (SAC)** provides a smart contract interface that enables Stellar assets to interoperate seamlessly with Stellar smart contracts. - Transfers between accounts and contracts using SAC resolve to the same trustline balance updates the Stellar protocol has always used. - Transfers to and from a `G...` account results in the same modifications to trustline ledger entries. - Transfers to and from contracts result in updates to the token contract's data entries. ### Strengths - Has the benefits of Stellar assets (low cost, less computationally expensive), making these great for remittances and micropayments. - Can be used in Stellar smart contracts via SAC with a [SEP-41][sep-41] interface. - Maintains issuer controls and trustline semantics even when used from contracts. - Wide ecosystem wallet and indexer support. Assets issued by `G...` accounts are compatible with Stellar ecosystem products (such as Stellar wallets) and other ecosystem products (such as exchanges). For example, benefit from [anchors](../learn/fundamentals/anchors.mdx), the bridges between the Stellar network and traditional financial systems. Explore the global [Stellar anchor directory](https://anchors.stellar.org) for further details. - Give the issuer granular control over asset management with features that allow the issuer to [name the asset](./control-asset-access.mdx#naming-an-asset), [determine access control](./control-asset-access.mdx#controlling-access-to-an-asset-with-flags), [limit asset supply](./control-asset-access.mdx#limiting-the-supply-of-an-asset), [publish asset information](./publishing-asset-info.mdx), and [ensure compliance](./anatomy-of-an-asset.mdx#compliance). Stellar assets provide built-in issuer control mechanisms by default. In contrast, contract tokens offer flexibility but require all control and compliance features to be explicitly designed and implemented in the contract. - Native operations on Stellar Assets emit standardized events through the Stellar Asset Contract, allowing off-chain systems and indexers to observe asset activity in a uniform way. Event semantics are defined at the protocol level according to [CAP-67](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md). - Operations on Stellar Assets emit the same standardized events defined by SEP-41, because the Stellar Asset Contract implements the SEP-41 token interface, Payment operations on Stellar Assets surface identical transfer events, allowing off-chain systems and indexers to observe asset activity through a uniform way. - By default, the admin of a Stellar Asset Contract (SAC) is the asset issuer (a `G...` address). However, the admin can be changed to a smart contract. Assigning a contract as the SAC admin enables custom, onchain policy for administrative actions for privileged operations. - As accounts must have an active trustline to receive, hold, and transact assets, accounts can choose which asset they receive. This opt-in property holds even when a trustline is created by a smart contract via the SAC's `trust` function, because the account holder must still authorize the trustline's creation. ### Tradeoffs - Accounts must have an active trustline to receive, hold, and transact. - As of Yardstick, Protocol 26, trustlines can be added programmatically through contract invocations using the SAC's [`trust` function](./stellar-asset-contract.mdx#creating-trustlines-from-a-contract), enabling assets that rely on bridging or chain abstraction patterns ([Learn more about CAP-73](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0073.md)). On earlier protocol versions, the trustline must be created with a separate `changeTrust` operation before a contract can interact with the account's balance. ### When to use - Simple payment rails with optional smart contract integration. - Trustline-governed asset flows that also need Stellar smart contract composability ## 2. SEP-41 Contract Tokens ### What these are Fully customizable contract tokens that implement the **[SEP-41][sep-41] token standard**, with their own storage and logic. [Contract tokens][ti] issued by a deployed Wasm contract (`C...` addresses). ### How they work - Issued and managed entirely in contract code. - Can be held by both `G…` accounts and `C…` accounts (no trustlines required). - Balances are always stored in contract data entries. - Commonly used when custom logic is required: - Mint/burn rules - Vesting - Hooks - Custom transfer logic - Fee structures ### Strengths - Fully programmable token logic beyond what Stellar Asset Contract expresses. - [SEP-41][sep-41] standard ensures broad tooling compatibility. - Can be integrated into existing applications and DeFi protocols. - Some ecosystem wallet support. ### Tradeoffs - Higher execution costs compared to Stellar Asset operations. - Less widespread ecosystem support compared to Stellar Assets, including centralized exchange support. ### When to use - DeFi primitives requiring complex tokenomics. - Asset behaviors not representable by Stellar Assets alone. - When extending SEP-41 with custom functionality. ## 3. ERC-3643 / SEP-57 (T-REX) Tokens ### What these are A permissioned token standard for regulated, compliance-aware assets that extends [SEP-41][sep-41] with identity and rules enforcement. T-REX tokens are issued by a deployed Wasm contract (`C...` addresses) and implement compliance logic directly in the contract suite. ### How they work - Extend [SEP-41][sep-41] with on-chain identity verification and compliance rules. - Enforce transfer restrictions based on identity status and compliance policies. - Support [role-based access control (RBAC)](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/access) tailored for institutional use cases. - Balances are stored in contract data entries. ### Strengths - On-chain compliance. - On-chain identity. - Role/agent-based structure tailored for institutions. ### Tradeoffs - Higher complexity. - Higher execution costs due to on-chain compliance and identity checks. - Current state: - Balance display supported in some ecosystem wallets via [SEP-41][sep-41]. - Transfers and control functions not yet supported in wallets. - Indexer support is not provided out of the box and must be self-implemented. ### When to use - Institutional RWAs with strict on-chain compliance requirements. - Interoperability with Ethereum’s ERC-3643 ecosystem. ## Decision Guide Choose the right token implementation based on the use case: - **Assets Issued by `G...` Accounts (with SAC)**: - Simple payment rails or fiat-backed stablecoins - Maximum ecosystem compatibility (wallets, exchanges, DEX) - Low transaction costs and fast settlement - Optional smart contract integration via SAC - Regulatory controls through protocol flags - **SEP-41 Contract Tokens**: - Custom token logic (transfer fees, vesting, hooks) - DeFi primitives and complex tokenomics - No trustline requirements - Full programmability for innovative use cases - Integration with Stellar smart contracts - **SEP-57 / ERC-3643 (T-REX) Tokens**: - Onchain compliance and identity verification - Institutional-grade access controls - Regulatory requirements for Real World Assets (RWAs) - Interoperability with Ethereum ERC-3643 :::note[Summary: TLDR] - Stellar Assets are best for simple payments and fiat-backed assets, with optional smart contract interoperability via the Stellar Asset Contract. - SEP-41 Contract Tokens are ideal for DeFi and advanced tokenomics requiring full programmability. - SEP-57 ERC-3643 Tokens are designed for regulated Real World Assets, offering on-chain compliance and identity with institutional-grade controls. ::: ## More Info about Stellar Asset Contract (SAC) The Stellar Asset Contract (SAC) is a contract built-in to the Stellar protocol that implements SEP-41 for assets issued by Stellar accounts (`G...`), and enables smart contracts to interact with assets issued on Stellar. For any Stellar asset, an instance of the SAC can be deployed (by anyone) to a deterministic, reserved address. Once deployed, smart contracts can interact with that asset using standard contract calls. The SAC has read/write access to: - Account balances (for XLM) - Trustline balances (for issued assets) - Smart-contract token balances This allows Stellar assets to interoperate seamlessly with smart contracts. - [Read more about the Stellar Asset Contract](./stellar-asset-contract.mdx) - [How to deploy a Stellar Asset Contract](../tools/cli/cookbook/deploy-stellar-asset-contract.mdx) ### Benefits of the SAC #### Compatibility The SAC preserves full compatibility with the existing Stellar asset model, including trustlines and authorization flags. For Stellar DEX, compatibility does not currently exist for SAC, but it does exist for the underlying assets via classic operations. #### Cost & Resource Efficiency Because the SAC is compiled into the protocol (rather than implemented as a user-deployed contract running in a VM), its functions are more resource-efficient than equivalent contract-coded logic. #### Minimal Setup You don’t need to write or deploy a custom token contract. A Stellar asset’s SAC already exists at the protocol level and only needs to be deployed to be used. #### Extensible Administration The asset admin can be a smart contract. Issuers may delegate administrative capabilities—such as authorization, minting, or clawbacks—to another contract, enabling custom or decentralized admin logic without replacing the asset itself. ### Limitations of the SAC Aside from delegating admin logic, the behavior of a Stellar asset and its SAC **cannot be modified**. Core asset semantics (balances, transfers, trustlines) are fixed at the protocol level. If your use case requires token behavior not supported by Stellar assets, you should use a contract token instead. ### Contract Tokens For advanced or non-standard use cases, you can create a contract token that implements the **[SEP-41 token interface][ti]**. This interface defines the required functions and events for compatibility with applications that work with tokens. Key points: - The SAC itself implements the token interface ([SEP-41][sep-41]) - Applications built against the token interface can interact with both Stellar assets (via SAC) and contract tokens - Contract tokens allow full customization of token logic - Contract tokens provide the flexibility to implement features not available in Stellar Assets, such as transfer fees, vesting schedules, or custom mint/burn rules :::note Smart contracts cannot use Stellar assets unless that Stellar asset has a deployed SAC. Anyone can deploy the SAC for a Stellar asset to its reserved address. ::: ### When You Need a Contract Token (Examples) The following scenarios are not possible using the SAC, but can be implemented with a contract token: - **Transfer Fees**: Implement a token that automatically deducts a 1% fee on every transfer and routes it to a designated address. - **Token Factory Pattern**: Build a factory contract that programmatically deploys new instances of a token contract, enabling standardized, on-demand token creation. Issuing assets is a core feature of Stellar: any asset can be tokenized (or minted) on the network and then tracked, held, and traded quickly and cheaply. Assets can represent many things: cryptocurrencies (such as bitcoin or ether), fiat currencies (such as dollars or pesos), other tokens of value (such as NFTs), pool shares, or even bonds and equity. Any Stellar account can issue an asset, and since anyone can set up an account, anyone can issue assets: banks, payment processors, money service businesses, for-profit enterprises, nonprofits, local communities, and individuals. It’s a self-serve process with no permission needed. Issuing an asset on Stellar is easy and only takes a few operations. However, there are additional considerations you may need to think about depending on your use case, such as publishing asset information, compliance, and asset supply, which we’ll cover in this documentation. Assets on Stellar have two identifying characteristics: the asset code and the issuer. Since more than one organization can issue a credit representing the same asset, asset codes often overlap (for example, multiple companies offer a USD token on Stellar). Assets are uniquely identified by the combination of their asset code and issuer. Assets issued on the Stellar network are accessible to smart contracts. Every Stellar asset has reserved a [Stellar Asset Contract](./stellar-asset-contract.mdx) that can be deployed by anyone who wants to be able to interact with the asset from a contract. ## Stablecoins One major category of assets is the stablecoin. A stablecoin is a blockchain-based token whose value is tied to another asset, such as the US dollar, other fiat currencies, commodities like gold, or even cryptocurrencies. There are two types of stablecoin: 1) reserve-backed stablecoins that must have a mechanism for redeeming the asset backing them, and 2) algorithmic stablecoins that don’t have assets backing them and instead rely on an algorithm to control the stablecoin supply. When discussing stablecoins, our documentation will focus on reserve-backed stablecoins. Reserve-backed stablecoins are pegged to a real-world asset at a 1:1 ratio. Because the underlying asset is maintained as collateral, users should be able to trade their stablecoin for the asset at any time. Asset reserves can be maintained by independent custodians and should be regularly audited. Currently, one of Stellar's most significant use cases is the tokenization of fiat currency for processes like cross-border payments. With anchors, users can connect Stellar tokens to existing rails that allow for the deposit of real-world assets in exchange for digital currency and vice versa. Learn more about anchors in our [Anchors section](../learn/fundamentals/anchors.mdx). ### Treasury management When issuing a reserve-backed stablecoin, you must set up its off-chain reserve, which securely stores the asset backing the stablecoin. When users wish to redeem their stablecoin, they can receive an equivalent amount of the underlying reserve asset from the issuer. ## Compliance As an asset issuer, you may need to comply with regulatory requirements that vary based on jurisdiction. Stellar has built-in features that can help meet these requirements, such as: - [Controlling access to an asset with flags](./control-asset-access.mdx#controlling-access-to-an-asset-with-flags) - [SEP-8: Regulated Assets](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0008.md) - regulated assets are assets that require an issuer’s approval (or a delegated third party’s approval) on a per-transaction basis. Check out this Stellar Ecosystem Proposal to learn how to implement regulated assets into your use case. [sac]: ./stellar-asset-contract.mdx [ti]: ./token-interface.mdx [sep-1]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md [sep-14]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0014.md [sep-41]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md [sep-57]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0057.md --- ## Asset Design Considerations ## Issuing and distribution accounts It is best practice on the Stellar network to create two accounts when issuing an asset: 1) the issuing account and 2) the distribution account. The **issuing account** creates (or mints) the asset on the network by executing a payment operation. The issuing account will always be linked to the asset’s identity. Any account wanting to hold the asset must first establish a trustline with the issuing account. Read about trustlines in our [Trustlines section](../learn/fundamentals/stellar-data-structures/accounts.mdx#trustlines). The **distribution account** is the first recipient of the issued asset and handles all other transactions. Note that you can also issue an asset by creating an offer or liquidity pool deposit with the issuing account. It is best practice to issue an asset by sending it from the issuing account to a distribution account for two main reasons: security and auditing. ### Security The distribution account will be a hot account, meaning that some web service out there has direct access to sign its transactions. For example, if the account you're distributing from is also the issuing account and it is compromised by a malicious actor, the actor can now issue as much of the asset as they want. If the malicious actor redeems the newly issued tokens with an anchor service, the anchor may not have the liquidity to support the customer withdrawals. Stakes are lower if you use a distribution account- if the distribution account is compromised, you can freeze the account’s asset balance and start with a new distribution account without changing the issuing account. ### Auditing Using a distribution account is better for auditing because an issuing account can’t actually hold a balance of its own asset. Sending an asset back to its issuing account burns (deletes) the asset. If you have a standing inventory of the issued asset in a separate account, it’s easier to track and can help with bookkeeping. ## Naming an asset One thing you must decide when issuing an asset is what to call it. An asset code is the asset’s identifying code. There are three possible formats: Alphanumeric 4, Alphanumeric 12, and liquidity pool shares. Learn about liquidity pool shares in the [Liquidity on Stellar section](../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx). - Alphanumeric 4-character maximum: Any characters from the set a-z, A-Z, 0-9 are allowed. The code can be shorter than 4 characters, but the trailing characters must all be empty. - Alphanumeric 12-character maximum: Any characters from the set a-z, A-Z, 0-9 are allowed. The code can be any number of characters from 5 to 12, but the trailing characters must all be empty. - The pool share asset is defined by the liquidity pool identifier (PoolID), which in turn is defined by the two assets its reserves are composed of. Provided it falls into one of these buckets, you can choose any asset code you like. That said, if you’re issuing a currency, you should use the appropriate ISO 4217 code, and if you’re issuing a stock or bond, the appropriate ISIN number. Doing so makes it easier for Stellar interfaces to properly display and sort your token in their listings and allows potential token holders to understand what your token represents. ## Controlling access to an asset with flags When you issue an asset on Stellar, anyone can hold it by default. In general, that’s a good thing: easy access means better reach and better liquidity. However, if you need to control access to an asset to comply with regulations (or for any other reason), you can easily do so by enabling flags on your issuing account. Flags are created on the account level using a `set_options` operation. They can be set at any time in the life cycle of an asset, not just when you issue it. ### Flag types The (0xn) next to each flag type denotes the bit settings for each flag. #### Authorization Required (0x1) When `AUTH_REQUIRED_FLAG` is enabled, an issuer must approve an account before that account can hold its asset. This setting allows issuers to vet potential token holders and to approve trustlines. To allow access, the user creates a trustline, and the issuer approves it by changing the `AUTHORIZE` flag with the `Set_Trust_Line_Flag` operation. There are two levels of authorization an asset issuer can grant using the `Set_Trust_Line_Flag` operation: - `AUTHORIZED_FLAG`: signifies complete authorization allowing an account to transact freely with the asset to make and receive payments, place orders, and deposit into a liquidity pool. - `AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG`: denotes limited authorization that allows an account to maintain current orders, withdraw from a liquidity pool, or cancel current orders - but not to otherwise transact with the asset. #### Authorization Revocable (0x2) When `AUTH_REVOCABLE_FLAG` is enabled, an issuer can revoke an existing trustline’s authorization, thereby freezing the asset held by an account. Doing so prevents that account from transferring or trading the asset and cancels the account’s open orders for the asset. `AUTH_REVOCABLE_FLAG` also allows an issuer to reduce authorization from complete to limited, which prevents the account from transferring or trading the asset but does not cancel the account’s open orders for the asset. This setting is useful for issuers of regulated assets who need to authorize transactions on a case-by-case basis to ensure each conforms to certain requirements. All changes to asset authorization are performed with the Set Trustline Flags operation. There are three levels of authorization an asset issuer can remove using the `Set_Trust_Line_Flag` operation: - `AUTHORIZED_FLAG`: signifies complete authorization allowing an account to transact freely with the asset to make and receive payments and place orders. - `AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG`: denotes limited authorization that allows an account to maintain current orders but not to otherwise transact with the asset. - `CLAWBACK_ENABLED`: enables the issuing account to take back (burning) all of the asset. See our [section on Clawbacks](../build/guides/transactions/clawbacks.mdx) for more information. #### Clawback Enabled (0x8) With the `AUTH_CLAWBACK_ENABLED_FLAG` flag set, any _subsequent_ trustlines established with this account will have clawbacks enabled. You can read more about clawbacks (and selectively controlling them on a per-trustline basis) [here](../build/guides/transactions/clawbacks.mdx). Note that this flag requires that revocable is also set. #### Authorization Immutable (0x4) With this setting, none of the other authorization flags (`AUTH_REQUIRED_FLAG`, `AUTH_REVOCABLE_FLAG`) can be set, and the issuing account can’t be merged. You set this flag to signal to potential token holders that your issuing account and its assets will persist on the ledger in an open and accessible state. ### Set Trustline Flag operation The issuing account can configure various authorization and trustline flags for individual trustlines to an asset. The asset parameter is of the TrustLineAsset type. If you are modifying a trustline to a regular asset (i.e. one in a Code:Issuer format), this is equivalent to the asset type. If you are modifying a trustline to a pool share, this is the liquidity pool’s unique ID. ### Example flow Let’s look at how an issuer of a regulated asset might use the `AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG` flag. If the issuer wants to approve transactions on a case-by-case basis while allowing accounts to maintain offers, they can leave an account in the `AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG` state. That account can own offers but cannot do anything else with the asset. To initiate a new operation, the holding account requests that the issuer approve and sign a transaction. Once the issuer inspects the operation and decides to approve it, they sandwich it between a set of operations, first granting authorization, then reducing it. Here’s a payment from A to B sandwiched between `set_trust_line_flags` operations: - Operation 1: Issuer uses `SetTrustLineFlags` to fully authorize account A, asset X - Operation 2: Issuer uses `SetTrustLineFlags` to fully authorize account B, asset X - Operation 3: Payment from A to B - Operation 4: Issuer uses `SetTrustLineFlags` to set account B, asset X to `AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG` state - Operation 5: Issuer uses `SetTrustLineFlags` to set account A, asset X to `AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG` state The authorization sandwich allows the issuer to inspect the specific payment and to grant authorization for it and it alone. Since operations bundled in a transaction are simultaneous, A and B are only authorized for the specific, pre-approved payment operation. Complete authorization does not extend beyond the specific transaction. ### Sample code In the following code samples, proper error checking is omitted. However, you should always validate your results, as there are many ways that requests can fail. Refer to the [Error Handling](../data/apis/horizon/api-reference/errors/error-handling.mdx) page for tips on error management strategies. The following example sets authorization to be both required and revocable: ```js var StellarSdk = require("@stellar/stellar-sdk"); var server = new StellarSdk.Horizon.Server( "https://horizon-testnet.stellar.org", ); // Keys for issuing account var issuingKeys = StellarSdk.Keypair.fromSecret( "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4", ); server .loadAccount(issuingKeys.publicKey()) .then(function (issuer) { var transaction = new StellarSdk.TransactionBuilder(issuer, { fee: 100, networkPassphrase: StellarSdk.Networks.TESTNET, }) .addOperation( StellarSdk.Operation.setOptions({ setFlags: StellarSdk.AuthRevocableFlag | StellarSdk.AuthRequiredFlag, }), ) // setTimeout is required for a transaction .setTimeout(100) .build(); transaction.sign(issuingKeys); return server.submitTransaction(transaction); }) .then(console.log) .catch(function (error) { console.error("Error!", error); }); ``` ```java Server server = new Server("https://horizon-testnet.stellar.org"); // Keys for issuing account KeyPair issuingKeys = KeyPair .fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"); AccountResponse sourceAccount = server.accounts().account(issuingKeys.getAccountId()); Transaction setAuthorization = new TransactionBuilder(sourceAccount, Network.TESTNET) .addOperation(SetOptionsOperation.builder() .setFlags( AccountFlag.AUTH_REQUIRED_FLAG.getValue() | AccountFlag.AUTH_REVOCABLE_FLAG.getValue()) .build()) .setBaseFee(Transaction.MIN_BASE_FEE) .setTimeout(300) .build(); setAuthorization.sign(issuingKeys); server.submitTransaction(setAuthorization); ``` ```python from stellar_sdk import Keypair, Network, Server, TransactionBuilder, AuthorizationFlag from stellar_sdk.exceptions import BaseHorizonError # Configure Stellar SDK to talk to the horizon instance hosted by Stellar.org # To use the live network, set the hostname to horizon_url for mainnet server = Server(horizon_url="https://horizon-testnet.stellar.org") # Use the test network, if you want to use the live network, please set it to `Network.PUBLIC_NETWORK_PASSPHRASE` network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE # Keys for accounts to issue and receive the new asset issuing_keypair = Keypair.from_secret( "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" ) issuing_public = issuing_keypair.public_key # Transactions require a valid sequence number that is specific to this account. # We can fetch the current sequence number for the source account from Horizon. issuing_account = server.load_account(issuing_public) transaction = ( TransactionBuilder( source_account=issuing_account, network_passphrase=network_passphrase, base_fee=100, ) .append_set_options_op( set_flags=AuthorizationFlag.AUTH_REVOCABLE_FLAG | AuthorizationFlag.AUTHORIZATION_REQUIRED ) .build() ) transaction.sign(issuing_keypair) try: transaction_resp = server.submit_transaction(transaction) print(f"Transaction Resp:\n{transaction_resp}") except BaseHorizonError as e: print(f"Error: {e}") ``` ## Limiting the supply of an asset :::danger[Warning] This section details how to lock your account with the purpose of limiting the supply of your issued asset. However, locking your account means you’ll never be able to do anything with it ever again- whether that’s adjusting signers, changing the home domain, claiming any held XLM, or any other operation. Your account will be completely frozen. ::: With that warning in mind, it is possible to lock down the issuing account of an asset so that the asset’s supply cannot increase. To do this, first set the issuing account’s master weight to 0 using the Set Options operation. This prevents the issuing account from being able to sign transactions and therefore, making the issuer unable to issue any more assets. Be sure to do this only after you’ve issued all desired assets to the distribution account. If the asset has a Stellar Asset Contract, also make sure the admin for the contract was not updated from the default (which is the issuer) using the `set_admin` contract call. If the admin was not the issuer, then the admin would be able to mint the asset even with the issuing account locked. Learn more about signature weights in the [Signatures and Multisig section](../learn/fundamentals/transactions/signatures-multisig.mdx). See how to do this in the optional steps of the [Issuing an Asset Tutorial](./how-to-issue-an-asset.mdx#configure-maximum-supply). --- ## Cross-Chain USDC Transfers with CCTP Circle's **Cross-Chain Transfer Protocol (CCTP)** supports native USDC transfers between Stellar and other CCTP-enabled chains — no wrapped assets, no third-party bridges. :::info[Full documentation] CCTP on Stellar is documented and maintained by Circle. For contract addresses, supported chains, SDKs, code samples, and the attestation API: [CCTP on Stellar — Circle developer docs](https://developers.circle.com/cctp/references/stellar) ::: --- ## Issue an Asset on Stellar: Set Trustlines, Manage Supply & Distribution # Issue an Asset Tutorial In this tutorial, we will walk through the steps to issue an asset on the Stellar test network. :::note If you'd like to interact with an asset issued on the Stellar network in smart contracts, you can create or deploy the [Stellar Asset Contract](./stellar-asset-contract.mdx) for that asset. ::: ## Prerequisites You must ensure you have the required amount of XLM to create your issuing and distribution accounts and cover the minimum balance and transaction fees. If you’re issuing an asset on the testnet, you can fund your account by getting test XLM from friendbot. If you’re issuing an asset in production, you will need to acquire XLM from another wallet or exchange. If you’d like to avoid your users having to deal with transaction fees, consider using fee-bump transactions. Read more in our [Fee-Bump Transaction Guide](../build/guides/transactions/fee-bump-transactions.mdx). Learn about the testnet and mainnet in our [Networks section](../networks/README.mdx). Learn more about fees in our [Fees, Resource Limits, and Metering section](../learn/fundamentals/fees-resource-limits-metering.mdx). ## Foundational tools ### Issuer account keypair First, you must generate a unique keypair. The public key will act as your [issuing identity](../learn/fundamentals/stellar-data-structures/assets.mdx#issuer) on the network, while you use the secret key to sign transactions. ```js const issuerKeypair = StellarSdk.Keypair.random(); console.log("Issuer Public Key:", issuerKeypair.publicKey()); console.log("Issuer Secret Key:", issuerKeypair.secret()); ``` ```python from stellar_sdk import Keypair issuer_keypair = Keypair.random() print("Issuer Public Key:", issuer_keypair.public_key) print("Issuer Secret Key:", issuer_keypair.secret) ``` ```java KeyPair issuerKeypair = KeyPair.random(); System.out.println("Issuer Public Key: " + issuerKeypair.getAccountId()); System.out.println("Issuer Secret Key: " + issuerKeypair.getSecretSeed()); ``` ```go "github.com/stellar/go-stellar-sdk/keypair" "fmt" ) issuerKeypair := keypair.MustRandom() fmt.Println("Issuer Public Key:", issuerKeypair.Address()) fmt.Println("Issuer Secret Key:", issuerKeypair.Seed()) ``` :::info Your account address will not change once you issue your asset, even if you modify its [signers](../learn/fundamentals/transactions/signatures-multisig.mdx). Many issuers employ [vanity public keys](https://github.com/JFWooten4/stellar-vanity-toolkit) related to their asset. For instance, you might configure a custom signer in [base32](../learn/glossary.mdx#account-id) like `GASTRO...USD`. ::: :::note Many users secure their issuing account with cold storage techniques, such as a hardware wallet or multisignature setup. This adds an extra layer of protection by keeping your secret key offline or requiring multiple approvals for transactions. ::: ### Distribution account keypair Your asset can be issued and transferred between accounts through a payment, contract, or claimable balance. Although it is not required to create a distribution account, it is best practice, so we will do so in this example. Read more in our [Issuing and Distribution Accounts section](./control-asset-access.mdx#issuing-and-distribution-accounts). #### Three Operations ##### Generate a new keypair ```js const distributorKeypair = StellarSdk.Keypair.random(); ``` ```python distributor_keypair = Keypair.random() ``` ```java KeyPair distributorKeypair = KeyPair.random(); ``` ```go distributorKeypair := keypair.MustRandom() ``` ##### Import an existing keypair ```js const distributorKeypair = StellarSdk.Keypair.fromSecret(‘SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4’) ``` ```python distributor_keypair = Keypair.from_secret("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4") ``` ```java KeyPair distributorKeypair = KeyPair.fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"); ``` ```go distributorKeypair := keypair.MustParseFull("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4") ``` ##### Employ [multiple signatures](../learn/fundamentals/transactions/signatures-multisig.mdx) :::danger Be careful when working with raw secret keys. If you don't have issuer trustline [clawback](../build/guides/transactions/clawbacks.mdx) enabled, any misstep here could permanently render assets lost. Many users put their first few projects on [testnet](../networks/README.mdx#testnet) or try out [Quests](../learn/interactive/quest.mdx) which provide a low-stakes introductory sandbox. ::: ### Local asset object The asset object is a combination of your [code](./control-asset-access.mdx#naming-an-asset) and your issuing public key. After your issuance, anyone can search the network for your unique asset. ```js const astroDollar = new StellarSdk.Asset( "AstroDollar", issuerKeypair.publicKey(), ); ``` ```python from stellar_sdk import Keypair, Asset astro_dollar = Asset("AstroDollar", issuer_keypair.public_key) ``` ```java Asset astroDollar = Asset.createNonNativeAsset("AstroDollar", issuerKeypair.getAccountId()); ``` ```go "github.com/stellar/go-stellar-sdk/keypair" "github.com/stellar/go-stellar-sdk/txnbuild" ) astroDollar := txnbuild.CreditAsset{Code: "AstroDollar", Issuer: issuerKeypair.Address()} ``` :::info While anyone can create an asset, there may be real-world [compliance](./anatomy-of-an-asset.mdx#compliance) implications relevant to your use case. ::: :::note You’ll want to make sure you publish information about your asset to establish trust with your users and prevent errors. Learn how to do so with our [Publish Information About Your Asset section](./publishing-asset-info.mdx). ::: ## Network transactions ### Build transaction to establish distributor trustline Accounts must establish a [trustline](../learn/fundamentals/stellar-data-structures/accounts.mdx#trustlines) with the issuing account to hold that issuer’s asset. This is true for all assets except for the network’s native token, [Lumens](../learn/fundamentals/lumens.mdx). :::note If you’d like to avoid your users having to deal with trustlines or XLM, consider using sponsored reserves. Read more in our [Sponsored Reserves guide](../build/guides/transactions/sponsored-reserves.mdx). ::: ```js const StellarSdk = require("@stellar/stellar-sdk"); const server = new StellarSdk.Horizon.Server( "https://horizon-testnet.stellar.org", ); const account = await server.loadAccount(distributorKeypair.publicKey()); // This step only builds the transaction. It still needs to be signed. const transaction = new StellarSdk.TransactionBuilder(account, { fee: StellarSdk.BASE_FEE, networkPassphrase: StellarSdk.Networks.TESTNET, }) // The `changeTrust` operation creates (or alters) a trustline .addOperation( StellarSdk.Operation.changeTrust({ asset: astroDollar, limit: "1000", // optional source: distributorKeypair.publicKey(), }), ) .setTimeout(100) .build(); ``` ```python from stellar_sdk import Keypair, Asset, Network, Server, TransactionBuilder server = Server("https://horizon-testnet.stellar.org") distributor_account = server.load_account(distributor_keypair.public_key) transaction = ( TransactionBuilder( source_account=distributor_account, network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, base_fee=100 ) .append_change_trust_op(asset=astro_dollar, limit="1000") .set_timeout(100) .build() ) ``` ```java Server server = new Server("https://horizon-testnet.stellar.org"); AccountResponse distributorAccount = server.accounts().account(distributorKeypair.getAccountId()); Transaction transaction = new TransactionBuilder(distributorAccount, Network.TESTNET) .addOperation( ChangeTrustOperation.builder() .asset(new ChangeTrustAsset(astroDollar)) .limit(new BigDecimal("1000")) .sourceAccount(distributorKeypair.getAccountId()) .build() ) .setBaseFee(Transaction.MIN_BASE_FEE) .setTimeout(100) .build(); ``` ```go "github.com/stellar/go-stellar-sdk/clients/horizonclient" "github.com/stellar/go-stellar-sdk/keypair" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/txnbuild" ) client := horizonclient.DefaultTestNetClient distributorAccountRequest := horizonclient.AccountRequest{AccountID: distributorKeypair.Address()} distributorAccount, _ := client.AccountDetail(distributorAccountRequest) transaction, _ := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: &distributorAccount, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Operations: []txnbuild.Operation{ &txnbuild.ChangeTrust{ Line: astroDollar, Limit: "1000", }, }, Timebounds: txnbuild.NewInfiniteTimeout(), }, ) ``` ### Issuer payment to distributor Payments are the most popular operation to actually issue (or mint) your asset, compared to [other issuances](../learn/fundamentals/transactions/list-of-operations.mdx#path-payment-strict-send). A payment creates the amount of an asset specified, up to [the maximum 64-bit integer](../learn/fundamentals/stellar-data-structures/assets.mdx#amount-precision). Relevantly, you do not need to scale up the issuing amount of your asset by the XDR [minimum increment](../learn/fundamentals/stellar-data-structures/assets.mdx#amount-precision). ```js // We're using TransactionBuilder(...) as a short-hand here // to show that these operations can be "chained" together. const transaction = new StellarSdk.TransactionBuilder(...) // The `payment` operation sends the `amount` of the specified // `asset` to our distributor account .addOperation(StellarSdk.Operation.payment({ destination: distributorKeypair.publicKey(), asset: astroDollar, amount: '1000', source: issuerKeypair.publicKey() })) ``` ```python # We're using TransactionBuilder(...) as a short-hand here # to show that these operations can be "chained" together. transaction = ( TransactionBuilder(...) .append_payment_op( destination=distributor_keypair.public_key, asset=astro_dollar, amount="1000" ) ) ``` ```java // We're using TransactionBuilder(...) as a short-hand here // to show that these operations can be "chained" together. Transaction transaction = new TransactionBuilder(...) .addOperation(PaymentOperation.builder() .destination(distributorKeypair.getAccountId()) .asset(astroDollar) .amount(new BigDecimal("1000")) .sourceAccount(issuerKeypair.getAccountId()) .build()) ``` ```go // We're using TransactionBuilder(...) as a short-hand here // to show that these operations can be "chained" together. transaction, _ := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: &issuerAccount, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Operations: []txnbuild.Operation{ &txnbuild.Payment{ Destination: distributorKeypair.Address(), Amount: "1000", Asset: astroDollar, }, }, Timebounds: txnbuild.NewInfiniteTimeout(), }, ) ``` :::note You can also create a market directly from the issuing account and issue tokens via trading. ::: ### Optional transactions #### Configure maximum supply :::danger This section details how to lock your account with the purpose of limiting the supply of your issued asset. However, locking your account means you’ll never be able to do anything with it ever again—whether that’s adjusting signers, changing the home domain, claiming any held XLM, or any other operation. Your account will be completely frozen. ::: You can permanently configure the exact number of an asset that will ever exist. Learn more about asset supply in our section on [Limiting the Supply of an Asset](./control-asset-access.mdx#limiting-the-supply-of-an-asset) ```js const lockAccountTransaction = new StellarSdk.TransactionBuilder(...) // This `setOptions` operation locks the issuer account // so there can never be any more of the asset minted .addOperation(StellarSdk.Operation.setOptions({ masterWeight: 0, source: issuerKeypair.publicKey() })) ``` ```python lock_account_transaction = ( TransactionBuilder(...) .append_set_options_op(master_weight=0) ) ``` ```java Transaction lockAccountTransaction = new TransactionBuilder(...) .addOperation(SetOptionsOperation.builder() .masterKeyWeight(0) .sourceAccount(issuerKeypair.getAccountId()) .build() ) ``` ```go lockAccountTransaction, _ := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: &issuerAccount, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Operations: []txnbuild.Operation{ &txnbuild.SetOptions{ MasterWeight: txnbuild.NewThreshold(0), }, }, Timebounds: txnbuild.NewInfiniteTimeout(), }, ) ``` #### Approve distributor trustline If you enable the [authorization flag](./control-asset-access.mdx#authorization-required-0x1), the issuing account also needs to approve the distributor account's trustline request before the issuing payment. You will need to do this for all new accounts when set for your asset. ```js const issuingAccount = await server.loadAccount(issuerKeypair.publicKey()); const transaction = new StellarSdk.TransactionBuilder(...) .addOperation( StellarSdk.Operation.setTrustLineFlags({ trustor: distributorKeypair.publicKey(), asset: astroDollar, flags: { authorized: true, }, }), ) .setTimeout(100) .build(); ``` ```python from stellar_sdk import Server, Keypair, Network, TransactionBuilder, SetTrustLineFlags server = Server("https://horizon-testnet.stellar.org") issuer_account = server.load_account(issuer_keypair.public_key) transaction = ( TransactionBuilder(...) .append_set_trust_line_flags_op( trustor=distributor_keypair.public_key, asset=astro_dollar, flags=SetTrustLineFlags.AUTHORIZED_FLAG ) .set_timeout(100) .build() ) ``` ```java AccountResponse issuerAccount = server.accounts().account(issuerKeys.getAccountId()); Transaction transaction = new TransactionBuilder(...) .addOperation(SetTrustlineFlagsOperation.builder() .trustor(distributorKeys.getAccountId()) .asset(astroDollar) .setFlags(EnumSet.of(TrustLineFlags.AUTHORIZED_FLAG)) .clearFlags(EnumSet.noneOf(TrustLineFlags.class)) .build()) .setBaseFee(Transaction.MIN_BASE_FEE) .setTimeout(100) .build(); ``` ```go issuerAccountRequest := horizonclient.AccountRequest{AccountID: issuerKeypair.Address()} issuerAccount, _ := client.AccountDetail(issuerAccountRequest) transaction, _ := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: &issuerAccount, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Operations: []txnbuild.Operation{ &txnbuild.SetTrustLineFlags{ Trustor: distributorKeypair.Address(), Asset: astroDollar, Authorized: txnbuild.AuthorizeFlag, }, }, Timebounds: txnbuild.NewInfiniteTimeout(), }, ) ``` ## Full Code Sample ```js var StellarSdk = require("@stellar/stellar-sdk"); var server = new StellarSdk.Horizon.Server( "https://horizon-testnet.stellar.org", ); // Keys for accounts to issue and receive the new asset var issuerKeys = StellarSdk.Keypair.fromSecret( "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4", ); var receivingKeys = StellarSdk.Keypair.fromSecret( "SDSAVCRE5JRAI7UFAVLE5IMIZRD6N6WOJUWKY4GFN34LOBEEUS4W2T2D", ); // Create an object to represent the new asset var astroDollar = new StellarSdk.Asset("AstroDollar", issuerKeys.publicKey()); // First, the receiving account must trust the asset server .loadAccount(receivingKeys.publicKey()) .then(function (receiver) { var transaction = new StellarSdk.TransactionBuilder(receiver, { fee: 100, networkPassphrase: StellarSdk.Networks.TESTNET, }) // The `changeTrust` operation creates (or alters) a trustline // The `limit` parameter below is optional .addOperation( StellarSdk.Operation.changeTrust({ asset: astroDollar, limit: "1000", }), ) // setTimeout is required for a transaction .setTimeout(100) .build(); transaction.sign(receivingKeys); return server.submitTransaction(transaction); }) .then(console.log) // Second, the issuing account actually sends a payment using the asset .then(function () { return server.loadAccount(issuerKeys.publicKey()); }) .then(function (issuer) { var transaction = new StellarSdk.TransactionBuilder(issuer, { fee: 100, networkPassphrase: StellarSdk.Networks.TESTNET, }) .addOperation( StellarSdk.Operation.payment({ destination: receivingKeys.publicKey(), asset: astroDollar, amount: "10", }), ) // setTimeout is required for a transaction .setTimeout(100) .build(); transaction.sign(issuerKeys); return server.submitTransaction(transaction); }) .then(console.log) .catch(function (error) { console.error("Error!", error); }); ``` ```python from stellar_sdk import Asset, Keypair, Network, Server, TransactionBuilder # Configure Stellar SDK to talk to the Horizon instance hosted by SDF # To use the live network, set the hostname to horizon_url for mainnet server = Server(horizon_url="https://horizon-testnet.stellar.org") # Use test network, if you need to use public network, please set it to `Network.PUBLIC_NETWORK_PASSPHRASE` network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE # Keys for accounts to issue and receive the new asset issuerKeypair = Keypair.from_secret( "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" ) issuer_public = issuerKeypair.public_key distributor_keypair = Keypair.from_secret( "SDSAVCRE5JRAI7UFAVLE5IMIZRD6N6WOJUWKY4GFN34LOBEEUS4W2T2D" ) distributor_public = distributor_keypair.public_key # Transactions require a valid sequence number that is specific to this account. # We can fetch the current sequence number for the source account from Horizon. distributor_account = server.load_account(distributor_public) # Create an object to represent the new asset astro_dollar = Asset("AstroDollar", issuer_public) # First, the receiving account must trust the asset trust_transaction = ( TransactionBuilder( source_account=distributor_account, network_passphrase=network_passphrase, base_fee=100, ) # The `changeTrust` operation creates (or alters) a trustline # The `limit` parameter below is optional .append_change_trust_op(asset=astro_dollar, limit="1000") .set_timeout(100) .build() ) trust_transaction.sign(distributor_keypair) trust_transaction_resp = server.submit_transaction(trust_transaction) print(f"Change Trust Transaction Resp:\n{trust_transaction_resp}") issuer_account = server.load_account(issuer_public) # Second, the issuing account actually sends a payment using the asset. payment_transaction = ( TransactionBuilder( source_account=issuer_account, network_passphrase=network_passphrase, base_fee=100, ) .append_payment_op( destination=distributor_public, asset=astro_dollar, amount="10", ) .build() ) payment_transaction.sign(issuerKeypair) payment_transaction_resp = server.submit_transaction(payment_transaction) print(f"Payment Transaction Resp:\n{payment_transaction_resp}") ``` ```java Server server = new Server("https://horizon-testnet.stellar.org"); // Keys for accounts to issue and receive the new asset KeyPair issuerKeys = KeyPair .fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"); KeyPair receivingKeys = KeyPair .fromSecretSeed("SDSAVCRE5JRAI7UFAVLE5IMIZRD6N6WOJUWKY4GFN34LOBEEUS4W2T2D"); // Create an object to represent the new asset Asset astroDollar = Asset.createNonNativeAsset("AstroDollar", issuerKeys.getAccountId()); // First, the receiving account must trust the asset AccountResponse receiving = server.accounts().account(receivingKeys.getAccountId()); Transaction allowAstroDollars = new TransactionBuilder(receiving, Network.TESTNET) .addOperation( // The `ChangeTrust` operation creates (or alters) a trustline // The second parameter limits the amount the account can hold ChangeTrustOperation.builder().asset(new ChangeTrustAsset(astroDollar)).limit(new BigDecimal("1000")).build()) .setBaseFee(Transaction.MIN_BASE_FEE) .setTimeout(180) .build(); allowAstroDollars.sign(receivingKeys); server.submitTransaction(allowAstroDollars); // Second, the issuing account actually sends a payment using the asset AccountResponse issuer = server.accounts().account(issuerKeys.getAccountId()); Transaction sendAstroDollars = new TransactionBuilder(issuer, Network.TESTNET) .addOperation( PaymentOperation.builder().destination(receivingKeys.getAccountId()).asset(astroDollar).amount(new BigDecimal("10")).build()) .setBaseFee(Transaction.MIN_BASE_FEE) .setTimeout(180) .build(); sendAstroDollars.sign(issuerKeys); server.submitTransaction(sendAstroDollars); ``` ```go package main "github.com/stellar/go-stellar-sdk/clients/horizonclient" "github.com/stellar/go-stellar-sdk/keypair" "github.com/stellar/go-stellar-sdk/network" "github.com/stellar/go-stellar-sdk/txnbuild" "log" ) func main() { client := horizonclient.DefaultTestNetClient // Remember, these are just examples, so replace them with your own seeds. issuerSeed := "SDR4C2CKNCVK4DWMTNI2IXFJ6BE3A6J3WVNCGR6Q3SCMJDTSVHMJGC6U" distributorSeed := "SBUW3DVYLKLY5ZUJD5PL2ZHOFWJSVWGJA47F6FLO66UUFZLUUA2JVU5U" /* * We omit error checks here for brevity, but you should always check your * return values. */ // Keys for accounts to issue and distribute the new asset. issuer, err := keypair.ParseFull(issuerSeed) distributor, err := keypair.ParseFull(distributorSeed) request := horizonclient.AccountRequest{AccountID: issuer.Address()} issuer_account, err := client.AccountDetail(request) request = horizonclient.AccountRequest{AccountID: distributor.Address()} distributorAccount, err := client.AccountDetail(request) // Create an object to represent the new asset astroDollar := txnbuild.CreditAsset{Code: "AstroDollar", Issuer: issuer.Address()} // First, the receiving (distribution) account must trust the asset from the // issuer. tx, err := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: distributorAccount.AccountID, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Preconditions: txnbuild.Preconditions{ TimeBounds: txnbuild.NewInfiniteTimeout(), }, Operations: []txnbuild.Operation{ &txnbuild.ChangeTrust{ Line: astroDollar, Limit: "5000", }, }, }, ) signedTx, err := tx.Sign(network.TestNetworkPassphrase, distributor) resp, err := client.SubmitTransaction(signedTx) if err != nil { log.Fatal(err) } else { log.Printf("Trust: %s\n", resp.Hash) } // Second, the issuing account actually sends a payment using the asset tx, err = txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: issuer_account.AccountID, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Preconditions: txnbuild.Preconditions{ TimeBounds: txnbuild.NewInfiniteTimeout(), }, Operations: []txnbuild.Operation{ &txnbuild.Payment{ Destination: distributor.Address(), Asset: astroDollar, Amount: "10", }, }, }, ) signedTx, err = tx.Sign(network.TestNetworkPassphrase, issuer) resp, err = client.SubmitTransaction(signedTx) if err != nil { log.Fatal(err) } else { log.Printf("Pay: %s\n", resp.Hash) } } ``` --- ## Publish Information About An Asset Stellar assets are defined by who issued them, what they represent, and the terms and conditions for their use. These should all be defined and made public by the issuer in a [Stellar info file](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md). It’s crucial to provide clear information about what it represents. On Stellar, you do that by linking your issuing account to a home domain, publishing a Stellar info file on that domain, and making sure that file is complete and accurate. The most successful asset issuers give exchanges, wallets, and potential buyers lots of information about themselves in order to establish trust. Completing your Stellar info file is not a step you can skip. ## What is a Stellar info file? A Stellar info file is a common place where the Internet can find information about your organization’s Stellar integration. You write it in TOML, a simple and widely used configuration file format designed to be readable by both humans and machines, and publish it at `https://YOUR_DOMAIN/.well-known/stellar.toml`. That way, everyone knows where to find it, anyone can look it up, and it proves that the owner of the HTTPS domain hosting the `stellar.toml` claims responsibility for the accounts and assets listed in it. Using a `set_options` operation, you can link your Stellar account to the domain that hosts your Stellar info file, thereby creating a definitive on-chain connection between this information and that account. ## Completing your `stellar.toml` The first Stellar Ecosystem Proposal (SEP) is [SEP-1: Stellar Info File](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) and specifies everything you would ever need to include in your Stellar info file. This section will walk through the sections of SEP-1 that relate to asset issuers. Use this section in conjunction with the SEP to ensure you complete your `stellar.toml` correctly. The four sections we’ll cover are: 1. General Information 2. Organization Documentation 3. Point of Contact Documentation 4. Currency Documentation For each of those sections, we’ll let you know which fields are required, meaning all asset issuers must include them to be listed by exchanges and wallets, and which fields are suggested. Completing suggested fields is a good way to make your asset stand out. **Note:** it's a good idea to keep the sections in the order presented in SEP-1: Stellar Info File, which is also the order they're presented here. TOML requires arrays to be at the end, so if you scramble the order, you may cause errors for TOML parsers. ### 1. General Information Required field for all asset issuers: - `ACCOUNTS`: A list of public keys for all the Stellar accounts associated with your asset. Listing your public keys lets users confirm that you own them. For example, when [google.com](https://google.com) hosts a `stellar.toml` file, users can be sure that only the accounts listed on it belong to Google. If someone then says, "You need to pay your Google bill this month, send payment to address GIAMGOOGLEIPROMISE", but that key is not listed on Google's `stellar.toml`, then users know not to trust it. There are several fields where you list information about your Stellar integration to aid in discoverability. If you are an anchor service, and you have set up infrastructure to interoperate with wallets and allow for in-app deposit and withdrawal of assets, make sure to include the locations of your servers on your `stellar.toml` file so those wallets know where to find relevant endpoints to query. In particular, list these: #### Suggested fields for asset issuers: - `TRANSFER_SERVER` if you support [SEP-6: Deposit and Withdrawal API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md) - `TRANSFER_SERVER_SEP0024` if you support [SEP-24: Interactive Deposit and Withdrawal](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md) - `KYC_SERVER` if you support [SEP-12: KYC API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) - `WEB_AUTH_ENDPOINT` if you support [SEP-10: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) - `DIRECT_PAYMENT_SERVER` if you support [SEP-31: Cross-Border Payments API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md) If you support other Stellar Ecosystem Proposals — such as federation or delegated signing — or host a public Horizon instance that other people can use to query the ledger, you should also add the location of those resources to [General Information](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md#general-information) so they're discoverable. ### 2. Organization Documentation Basic information about your organization goes into a TOML table called [`DOCUMENTATION`]. Organization Documentation is your chance to inform exchanges and buyers about your business and to demonstrate that your business is legitimate and trustworthy. #### Required field for all asset issuers: - `ORG_NAME` The legal name of your organization, and if your business has one, its official ORG_DBA. - `ORG_URL` The HTTPS URL of your organization's official website. In order to prove the website is yours, you must host your `stellar.toml` on the same domain you list here. That way, exchanges and buyers can view the SSL certificate on your website and feel reasonably confident that you are who you say you are. - `ORG_LOGO` A URL to a company logo, which will show up next to your organization on exchanges. This image should be a square aspect ratio transparent PNG, ideally of size 128x128. If you fail to provide a logo, the icon next to your organization will appear blank on many exchanges. - `ORG_PHYSICAL_ADDRESS` The physical address of your organization. We understand you might want to keep your work address private. At the very least, you should put the city and country in which you operate. A street address is ideal and provides a higher level of trust and transparency to your potential asset holders. - `ORG_OFFICIAL_EMAIL` The best business email address for your organization. This should be hosted at the same domain as your official website. - `ORG_SUPPORT_EMAIL` The best email for support requests. #### Suggested fields for asset issuers: - `ORG_GITHUB` Your organization's official Github account. - `ORG_KEYBASE` Your organization's official Keybase account. Your Keybase account should contain proof of ownership of any public online accounts you list here, including your organization's domain. - `ORG_TWITTER` Your organization's official Twitter handle. - `ORG_DESCRIPTION` A description of your organization. This is fairly open-ended, and you can write as much as you want. It's a great place to distinguish yourself by describing what it is that you do. Issuers that list verified information including phone/address attestations and Keybase verifications are prioritized by Stellar clients. ### 3. Point of Contact Documentation Information about the primary point(s) of contact for your organization goes into a TOML [array of tables](https://github.com/toml-lang/toml#array-of-tables) called `[[PRINCIPALS]]`. You need to put contact information for at least one person in your organization. If you don't, exchanges can't verify your offering, and it is unlikely that buyers will be interested. Multiple principals can be added with additional `[[PRINCIPALS]]` entries. #### Required field for all asset issuers: - `name` The name of the primary contact. - `email` The primary contact's official email address. This should be hosted at the same domain as your organization's official website. #### Suggested fields for asset issuers: - `github` The personal Github account of the point of contact. - `twitter` The personal Twitter handle of the point of contact. - `keybase` The personal Keybase account for the point of contact. This account should contain proof of ownership of any public online accounts listed here and may contain proof of ownership of your organization's domain. ### 4. Currency Documentation Information about the asset(s) you issue goes into a TOML [array of tables](https://github.com/toml-lang/toml#array-of-tables) called `[[CURRENCIES]]`. If you issue multiple assets, you can include them all in one `stellar.toml`. Each asset should have its own `[[CURRENCIES]]` entry. (These entries are also used for assets you support but don’t issue, but as this section focuses on issuing assets the language will reflect that.) #### Required field for all asset issuers: - `code` The asset code. This is one of two key pieces of information that identify your token. Without it, your token cannot be listed anywhere. - `issuer` The Stellar public key of the issuing account. This is the second key piece of information that identifies your token. Without it, your token cannot be listed anywhere. - `is_asset_anchored` An indication of whether your token is anchored or native: true if your token can be redeemed for an asset outside the Stellar network, false if it can’t. Exchanges use this information to sort tokens by type in listings. If you fail to provide it, your token is unlikely to show up in filtered market views. If you're issuing anchored (tethered, stablecoin, asset-backed) tokens, there are several additional required fields: - `anchor_asset_type` The type of asset your token represents. The possible categories are fiat, crypto, stock, bond, commodity, realestate, and other. - `anchor_asset` The name of the asset that serves as the anchor for your token. - `redemption_instructions` Instructions to redeem your token for the underlying asset. - `attestation_of_reserve` A URL to attestation or other proof, evidence, or verification of reserves, such as third-party audits, which all issuers of stablecoins should offer to adhere to best practices. #### Suggested fields for asset issuers: - `desc` A description of your token and what it represents. This is a good place to clarify what your token does and why someone might want to own it. - `conditions` Any conditions you place on the redemption of your token. - `image` A URL to a PNG or GIF image with a transparent background representing your token. Without it, your token will appear blank on many exchanges. ## How to publish your Stellar info file After you've followed the steps above to complete your Stellar info file, post it at the following location: `https://YOUR_DOMAIN/.well-known/stellar.toml` Enable CORS so people can access this file from other sites, and set the following header for an HTTP response for a `/.well-known/stellar.toml` file request. `Access-Control-Allow-Origin:` \* Set a `text/plain` content type so that browsers render the contents rather than prompting for a download. `content-type: text/plain` You should also use the `set_options` operation to set the home domain on your issuing account. ```nginx title="Configure stellar.toml for nginx" server { server_name my.example.com; root /var/www/my.example.com; location = /.well-known/stellar.toml { types { } default_type "text/plain; charset=utf-8"; allow all; if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; add_header 'Content-Length' 0; return 204; } if ($request_method = 'GET') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; } } // CertBot SSL configuration // ... } ``` ## Sample code to set the home domain of your issuing account ```js var StellarSdk = require("@stellar/stellar-sdk"); var server = new StellarSdk.Horizon.Server( "https://horizon-testnet.stellar.org", ); // Keys for issuing account var issuingKeys = StellarSdk.Keypair.fromSecret( "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4", ); server .loadAccount(issuingKeys.publicKey()) .then(function (issuer) { var transaction = new StellarSdk.TransactionBuilder(issuer, { fee: 100, networkPassphrase: StellarSdk.Networks.TESTNET, }) .addOperation( StellarSdk.Operation.setOptions({ homeDomain: "yourdomain.com", }), ) // setTimeout is required for a transaction .setTimeout(100) .build(); transaction.sign(issuingKeys); return server.submitTransaction(transaction); }) .then(console.log) .catch(function (error) { console.error("Error!", error); }); ``` ```python from stellar_sdk import Keypair, Network, Server, TransactionBuilder from stellar_sdk.exceptions import BaseHorizonError # Configure Stellar SDK to talk to the horizon instance hosted by Stellar.org # To use the live network, set the hostname to horizon_url for mainnet server = Server(horizon_url="https://horizon-testnet.stellar.org") # Use the test network, if you want to use the live network, please set it to `Network.PUBLIC_NETWORK_PASSPHRASE` network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE # Keys for accounts to issue and receive the new asset issuing_keypair = Keypair.from_secret("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4") issuing_public = issuing_keypair.public_key # Transactions require a valid sequence number that is specific to this account. # We can fetch the current sequence number for the source account from Horizon. issuing_account = server.load_account(issuing_public) transaction = ( TransactionBuilder( source_account = issuing_account, network_passphrase = network_passphrase, base_fee = 100, ) .append_set_options_op( home_domain = "yourdomain.com" ) .build() ) transaction.sign(issuing_keypair) try: transaction_resp = server.submit_transaction(transaction) print(f"Transaction Resp:\n{transaction_resp}") except BaseHorizonError as e: print(f"Error: {e}") ``` ```java Server server = new Server("https://horizon-testnet.stellar.org"); // Keys for issuing account KeyPair issuingKeys = KeyPair .fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"); AccountResponse sourceAccount = server.accounts().account(issuingKeys.getAccountId()); Transaction setHomeDomain = new TransactionBuilder(sourceAccount, Network.TESTNET) .addOperation(SetOptionsOperation.builder() .homeDomain("yourdomain.com").build()) .setBaseFee(Transaction.MIN_BASE_FEE) .setTimeout(180) .build(); setHomeDomain.sign(issuingKeys); server.submitTransaction(setHomeDomain); ``` ```go func main() { client := horizonclient.DefaultTestNetClient // Keys for issuing account issuingKeypair := keypair.MustParseFull("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4") issuingAccount, err := client.AccountDetail(horizonclient.AccountRequest{AccountID: issuingKeypair.Address()}) if err != nil { log.Fatal(err) } // Build the transaction tx, err := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: &issuingAccount, IncrementSequenceNum: true, BaseFee: txnbuild.MinBaseFee, Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(100)}, Operations: []txnbuild.Operation{ &txnbuild.SetOptions{ HomeDomain: "yourdomain.com", }, }, }, ) if err != nil { log.Fatal(err) } tx, err = tx.Sign(network.TestNetworkPassphrase, issuingKeypair) if err != nil { log.Fatal(err) } resp, err := client.SubmitTransaction(tx) if err != nil { log.Fatal(err) } fmt.Println("Transaction response:", resp) } ``` ## Sample `stellar.toml` ```toml NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" FEDERATION_SERVER="https://api.domain.com/federation" AUTH_SERVER="https://api.domain.com/auth" TRANSFER_SERVER="https://api.domain.com" SIGNING_KEY="GBBHQ7H4V6RRORKYLHTCAWP6MOHNORRFJSDPXDFYDGJB2LPZUFPXUEW3" HORIZON_URL="https://horizon.domain.com" ACCOUNTS=[ "GD5DJQDDBKGAYNEAXU562HYGOOSYAEOO6AS53PZXBOZGCP5M2OPGMZV3", "GAENZLGHJGJRCMX5VCHOLHQXU3EMCU5XWDNU4BGGJFNLI2EL354IVBK7", "GAOO3LWBC4XF6VWRP5ESJ6IBHAISVJMSBTALHOQM2EZG7Q477UWA6L7U" ] VERSION="2.0.0" [DOCUMENTATION] ORG_NAME="Organization Name" ORG_DBA="Organization DBA" ORG_URL="https://www.domain.com" ORG_LOGO="https://www.domain.com/awesomelogo.png" ORG_DESCRIPTION="Description of issuer" ORG_PHYSICAL_ADDRESS="123 Sesame Street, New York, NY 12345, United States" ORG_PHYSICAL_ADDRESS_ATTESTATION="https://www.domain.com/address_attestation.jpg" ORG_PHONE_NUMBER="1 (123)-456-7890" ORG_PHONE_NUMBER_ATTESTATION="https://www.domain.com/phone_attestation.jpg" ORG_KEYBASE="accountname" ORG_TWITTER="orgtweet" ORG_GITHUB="orgcode" ORG_OFFICIAL_EMAIL="support@domain.com" [[PRINCIPALS]] name="Jane Jedidiah Johnson" email="jane@domain.com" keybase="crypto_jane" twitter="crypto_jane" github="crypto_jane" id_photo_hash="be688838ca8686e5c90689bf2ab585cef1137c999b48c70b92f67a5c34dc15697b5d11c982ed6d71be1e1e7f7b4e0733884aa97c3f7a339a8ed03577cf74be09" verification_photo_hash="016ba8c4cfde65af99cb5fa8b8a37e2eb73f481b3ae34991666df2e04feb6c038666ebd1ec2b6f623967756033c702dde5f423f7d47ab6ed1827ff53783731f7" [[CURRENCIES]] code="USD" issuer="GCZJM35NKGVK47BB4SPBDV25477PZYIYPVVG453LPYFNXLS3FGHDXOCM" display_decimals=2 [[CURRENCIES]] code="BTC" issuer="GAOO3LWBC4XF6VWRP5ESJ6IBHAISVJMSBTALHOQM2EZG7Q477UWA6L7U" display_decimals=7 anchor_asset_type="crypto" anchor_asset="BTC" redemption_instructions="Use SEP6 with our federation server" collateral_addresses=["2C1mCx3ukix1KfegAY5zgQJV7sanAciZpv"] collateral_address_signatures=["304502206e21798a42fae0e854281abd38bacd1aeed3ee3738d9e1446618c4571d10"] # asset with meta info [[CURRENCIES]] code="GOAT" issuer="GD5T6IPRNCKFOHQWT264YPKOZAWUMMZOLZBJ6BNQMUGPWGRLBK3U7ZNP" display_decimals=2 name="goat share" desc="1 GOAT token entitles you to a share of revenue from Elkins Goat Farm." conditions="There will only ever be 10,000 GOAT tokens in existence. We will distribute the revenue share annually on Jan. 15th" image="https://static.thenounproject.com/png/2292360-200.png" fixed_number=10000 ``` [sep-0001]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md [sep-0020]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md [seps]: https://github.com/stellar/stellar-protocol/tree/master/ecosystem [twilio-guide]: https://support.twilio.com/hc/en-us/articles/223183008-Formatting-International-Phone-Numbers --- ## Quickstart Issue your first asset on the Stellar network in **one, single transaction**! ```js Keypair, Horizon, TransactionBuilder, Networks, Operation, Asset, BASE_FEE, } from "@stellar/stellar-sdk"; const horizonUrl = "https://horizon-testnet.stellar.org"; const friendbotUrl = "https://friendbot.stellar.org"; const issuerKeypair = Keypair.random(); const destinationKeypair = Keypair.random(); console.log( `issuer keys:\n${issuerKeypair.publicKey()}\n${issuerKeypair.secret()}\n`, ); console.log( `destination account keys:\n${issuerKeypair.publicKey()}\n${issuerKeypair.secret()}\n`, ); await fetch(friendbotUrl + `?addr=${issuerKeypair.publicKey()}`); // pre-fund the `issuer` account using friendbot await fetch(friendbotUrl + `?addr=${destinationKeypair.publicKey()}`); // pre-fund the `destination` account using friendbot const server = new Horizon.Server(horizonUrl); const account = await server.loadAccount(issuerKeypair.publicKey()); const abcAsset = new Asset("ABC", issuerKeypair.publicKey()); const transaction = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: Networks.TESTNET, }) .addOperation( Operation.changeTrust({ asset: abcAsset, source: destinationKeypair.publicKey(), }), ) .addOperation( Operation.payment({ destination: destinationKeypair.publicKey(), asset: abcAsset, amount: "100", }), ) .setTimeout(30) .build(); transaction.sign(issuerKeypair, destinationKeypair); const res = await server.submitTransaction(transaction); console.log(`transaction hash:\n${res.hash}`); ``` ```python from stellar_sdk import Asset, Keypair, Network, Server, TransactionBuilder horion_url = "https://horizon-testnet.stellar.org" friendbot_url = "https://friendbot.stellar.org" issuer_keypair = Keypair.random() destination_keypair = Keypair.random() print(f"issuer keys:\n{issuer_keypair.public_key}\n{issuer_keypair.secret}\n") print(f"issuer keys:\n{destination_keypair.public_key}\n{destination_keypair.secret}\n") requests.get(f"{friendbot_url}?addr={issuer_keypair.public_key}") requests.get(f"{friendbot_url}?addr={destination_keypair.public_key}") server = Server('https://horizon-testnet.stellar.org') account = server.load_account(issuer_keypair.public_key) abc_asset = Asset('ABC', issuer_keypair.public_key) transaction = ( TransactionBuilder( source_account=account, network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, base_fee=100, ) .append_change_trust_op( asset=abc_asset, source=destination_keypair.public_key, ) .append_payment_op( destination=destination_keypair.public_key, amount='100', asset=abc_asset, ) .set_timeout(30) .build() ) transaction.sign(issuer_keypair) transaction.sign(destination_keypair) res = server.submit_transaction(transaction) print(f"Transaction hash:\n{res['hash']}") ``` --- ## Use Issued Assets in Smart Contracts with the Stellar Asset Contract (SAC) :::info The term "custom token" has been deprecated in favor of "contract token". View the conversation in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). ::: # Stellar Asset Contract (SAC) The Stellar Asset Contract (SAC) is an implementation of [CAP-46-6 Smart Contract Standardized Asset] and [SEP-41 Token Interface] for Stellar [assets]. See examples of how to use the SAC in the [Tokens How-To Guides](../build/guides/tokens/README.mdx). ## Overview :::note Stellar assets are issued by Stellar accounts. Issue an asset on Stellar by following the [Issue an Asset Tutorial](./how-to-issue-an-asset.mdx). ::: The Stellar Asset Contract allows users and contracts to make payments with, and interact with, assets. The SAC can interact with assets held by Stellar accounts or contracts. The SAC is a special built-in contract that has access to functionality of the Stellar network that allows it to use Stellar assets directly. Each Stellar asset has an instance of the SAC reserved on the network. To use the SAC reserved for an asset, the instance just needs to be deployed. When the SAC transfers assets between accounts, the same debit and credits occur as they do when a Stellar payment operation is used, because the SAC interacts directly with Stellar account trust lines. When the SAC transfers assets between contracts, it uses Contract Data ledger entries to store the balances for contracts. Stellar account balances for the native asset are always stored on the account, and Stellar contract balances for the native asset are always stored in a contract data entry. Stellar account balances for issued assets are always stored in trust lines, and Stellar contract balances for issued assets are always stored in a contract data entry. For example, when transferring from a Stellar account to a Stellar contract, the Stellar account's trust line entry is debited, and a contract data entry is credited. And for example, when transferring from a Stellar contract to a Stellar account, a contract data entry is debited, and the account's trust line entry is credited. In both those examples it is a single asset that is transferring from the account to the contract and back again. No bridging is required and no intermediary tokens are needed. An asset on Stellar and its Stellar Asset Contract represent the same asset. The SAC for an asset is simply an API for interacting with the asset. The SAC implements the [SEP-41 Token Interface], which is similar to the widely used ERC-20 token standard. Contracts that depend on only the SEP-41 portion of the SAC's interface, are also compatible with any contract token that implements SEP-41. Some functionality available on the Stellar network in transaction operations, such as the order book, do not have any functions exposed on the Stellar Asset Contract in the current protocol. ## Deployment Every Stellar asset on Stellar has reserved a contract address that the Stellar Asset Contract can be deployed to. Anyone can initiate the deploy and the Stellar asset issuer does not need to be involved. It can be deployed using the [Stellar CLI] as shown [here](../tools/cli/cookbook/deploy-stellar-asset-contract.mdx). Or the [Stellar SDK] can be used as shown [here](../learn/fundamentals/contract-development/contract-interactions/stellar-transaction.mdx#xdr-usage) by calling `InvokeHostFunctionOp` with `HOST_FUNCTION_TYPE_CREATE_CONTRACT` and a `CONTRACT_ID_PREIMAGE_FROM_ASSET` contract ID preimage. The resulting token will have a deterministic identifier, which will be the sha256 hash of `HashIDPreimage::ENVELOPE_TYPE_CONTRACT_ID` xdr specified [here][contract_id]. Anyone can deploy the instances of Stellar Asset Contract. Note, that the initialization of the Stellar Asset Contracts happens automatically during the deployment. The asset issuer becomes the _initial_ administrator once the contract has been deployed; administrative authority is mutable and can be transferred afterwards with the SAC's `set_admin` function. A deployed SAC can be identified on-chain from its contract instance: its `SCContractInstance.executable` is `CONTRACT_EXECUTABLE_STELLAR_ASSET` rather than a Wasm hash. Once a contract is verified as a SAC this way, its built-in `name()` and `symbol()` methods report the wrapped asset's identity — there is no generic SEP-41 `asset()`/`issuer()` accessor to rely on. Note that a contract-address hash and the current `admin` value are not, by themselves, proof of an asset's provenance: verify the executable first, since the administrator may have been changed via `set_admin`. [contract_id]: https://github.com/stellar/stellar-xdr/blob/curr/Stellar-transaction.x [stellar cli]: ../tools/cli/stellar-cli.mdx [stellar sdk]: ../tools/sdks/README.mdx ## Interacting with classic Stellar assets The Stellar Asset Contract is the only way for contracts to interact with Stellar assets, either the native XLM asset, or those issued by Stellar accounts. The issuer of the asset will be the initial administrator of the deployed contract (administrative authority can later be moved with `set_admin`). Because the Native Stellar token doesn't have an issuer, it will not have an administrator either. It also cannot be burned. After the contract has been deployed, users can use their classic account (for lumens) or trustline (for other assets) balance. There are some differences depending on if you are using a classic account `Address` vs a contract `Address` (corresponding either to a regular contract or to a custom account contract). The following section references some issuer and trustline flags from Stellar classic, which you can learn more about [here](./control-asset-access.mdx#controlling-access-to-an-asset-with-flags). - Using `Address::Account` - The balance must exist in a trustline (or an account for the native balance). This means the contract will not store the balance in ContractData. If the trustline or account is missing, any function that tries to interact with that balance will fail (as of Yardstick, Protocol 26, a contract can create the missing trustline itself by first calling the SAC's [`trust` function](#creating-trustlines-from-a-contract)). - Classic trustline semantics will be followed. - Transfers will only succeed if the corresponding trustline(s) have the `AUTHORIZED_FLAG` set. - A trustline balance can only be clawed back using the `clawback` contract function if the trustline has `TRUSTLINE_CLAWBACK_ENABLED_FLAG` set. - Transfers to the issuer account will burn the token, while transfers from the issuer account will mint. - Trustline balances are stored in a 64-bit signed integer even though the interface accepts 128-bit signed integers. Any operation that attempts to send or receive an amount more than the maximum amount that can be represented by a 64-bit signed integer will fail. - Using `Address::Contract` - The balance and authorization state will be stored in contract storage, as opposed to a trustline. - Balances are stored in a 128-bit signed integer. - A balance can only be clawed back if the issuer account had the `AUTH_CLAWBACK_ENABLED_FLAG` set when the balance was created. A balance is created when either an `Address::Contract` is on the receiving end of a successful transfer, or if the admin sets the authorization state. Read more about `AUTH_CLAWBACK_ENABLED_FLAG` [here](./control-asset-access.mdx#clawback-enabled-0x8). ### Balance Authorization Required In the `Address::Contract` case, if the issuer has `AUTH_REQUIRED_FLAG` set, then the specified `Address::Contract` will need to be explicitly authorized with `set_auth` before it can receive a balance. This logic lines up with how trustlines interact with the `AUTH_REQUIRED_FLAG` issuer flag, allowing asset issuers to have the same control in Soroban as they do in Stellar classic. Read more about `AUTH_REQUIRED_FLAG` [here](./control-asset-access.mdx#authorization-required-0x1). ### Revoking Authorization The admin can only revoke authorization from an `Address`, if the issuer of the asset has `AUTH_REVOCABLE_FLAG` set. The deauthorization will fail if the issuer is missing. This requirement is true for both the trustline balances of `Address::Account` and contract balances of `Address:Contract`. Note that when a trustline is deauthorized from Soroban, `AUTHORIZED_FLAG` is cleared and `AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG` is set to avoid having to pull offers and redeeming pool shares. ### Creating trustlines from a contract As of Yardstick, Protocol 26 ([CAP-73]), the SAC's `trust` function allows a contract to create an asset's trustline for a `G...` address as part of a contract invocation. Before Yardstick, Protocol 26, a missing trustline could only be created with a separate `changeTrust` operation, so any contract function touching a trustline balance would fail until the account holder set up the trustline in a separate transaction. The `trust` function is useful any time a contract distributes an asset to accounts that may not hold it yet: for example, an airdrop or payout contract calling `trust` before `mint` or `transfer`, or bridging and chain abstraction flows that deliver assets to freshly created accounts. See [setting a custom SAC admin](../build/guides/tokens/custom-sac-admin.mdx) for a worked example of a contract calling `trust` before minting to a recipient. A few things to keep in mind: - The call is a no-op if `addr` is a contract address, or if the trustline already exists, so it's safe to call unconditionally before a transfer or mint. - When a trustline is actually created, the SAC requires authorization from `addr`, preserving the opt-in nature of trustlines. - The account holding the new trustline must satisfy the network's [base reserve](../learn/fundamentals/lumens.mdx#base-reserves) requirement for the additional ledger entry. ## Authorization semantics See the [authorization overview](../learn/fundamentals/contract-development/authorization.mdx) and [auth example](../build/smart-contracts/example-contracts/auth.mdx) for general information about authorization in Soroban. The token contract contains three kinds of operations that follow the token [interface](./token-interface.mdx#code): - getters, such as `balance`, which do not change the state of the contract - unprivileged mutators, such as `incr_allow` and `xfer`, which change the state of the contract but do not require special privileges - privileged mutators, such as `clawback` and `set_admin`, which change the state of the contract but require special privileges Getters require no authorization because they do not change the state of the contract and all contract data is public. For example, `balance` simply returns the balance of the specified `Address` without changing it. Unprivileged mutators require authorization from the `Address` that spends or allows spending their balance. The exceptions are `xfer_from` and `burn_from` operations where the `Address` that require authorization from the 'spender' entity that has got an allowance from another `Address` beforehand. Priviliged mutators require authorization from a specific privileged identity, known as the "administrator". For example, only the administrator can `mint` more of the token. Similarly, only the administrator can appoint a new administrator. ## Contract Interface The [`token` module] of the Rust SDK contains two traits, and corresponding client structs, for interacting with SACs. 1. The [`TokenInterface` trait] and [`TokenClient` struct] can be used to invoke the "basic subset" of the SAC functionality. They implement the common [SEP-41 Token Interface], and include functions like `transfer`, `burn`, `allowance`, etc. 2. The [`StellarAssetInterface` trait] and [`StellarAssetClient` struct] can be used to invoke the "extended subset" of SAC functionality. They _extend_ the common SEP-41 token interface with administrative functionality. In addition to all the `TokenInterface` functions, these traits contain admin functions like `set_admin`, `clawback`, `set_authorized`, etc. ```rust pub trait StellarAssetInterface { // All this is available in any SEP-41-compliant contract fn allowance(env: Env, from: Address, spender: Address) -> i128; fn approve(env: Env, from: Address, spender: Address, amount: i128, live_until_ledger: u32); fn balance(env: Env, id: Address) -> i128; fn transfer(env: Env, from: Address, to: MuxedAddress, amount: i128); fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128); fn burn(env: Env, from: Address, amount: i128); fn burn_from(env: Env, spender: Address, from: Address, amount: i128); fn decimals(env: Env) -> u32; fn name(env: Env) -> String; fn symbol(env: Env) -> String; //! Everything below is specifically available to SAC instances /// Sets the administrator to the specified address `new_admin`. /// /// # Arguments /// /// * `new_admin` - The address which will henceforth be the administrator /// of this token contract. /// /// # Events /// /// Emits an event with topics `["set_admin", admin: Address, /// sep0011_asset: String], data = new_admin: Address` fn set_admin(env: Env, new_admin: Address); /// Returns the admin of the contract. /// /// # Panics /// /// If the admin is not set. fn admin(env: Env) -> Address; /// Sets whether the account is authorized to use its balance. If /// `authorized` is true, `id` should be able to use its balance. /// /// # Arguments /// /// * `id` - The address being (de-)authorized. /// * `authorize` - Whether or not `id` can use its balance. /// /// # Events /// /// Emits an event with topics `["set_authorized", id: Address, /// sep0011_asset: String], data = authorize: bool` fn set_authorized(env: Env, id: Address, authorize: bool); /// Returns true if `id` is authorized to use its balance. /// /// # Arguments /// /// * `id` - The address for which token authorization is being checked. fn authorized(env: Env, id: Address) -> bool; /// Mints `amount` to `to`. /// /// # Arguments /// /// * `to` - The address which will receive the minted tokens. /// * `amount` - The amount of tokens to be minted. /// /// # Events /// /// Emits an event with topics `["mint", to: Address, /// sep0011_asset: String], data = amount: i128` fn mint(env: Env, to: Address, amount: i128); /// Clawback `amount` from `from` account. `amount` is burned in the /// clawback process. /// /// # Arguments /// /// * `from` - The address holding the balance from which the clawback will /// take tokens. /// * `amount` - The amount of tokens to be clawed back. /// /// # Events /// /// Emits an event with topics `["clawback", from: Address, /// sep0011_asset: String], data = amount: i128` fn clawback(env: Env, from: Address, amount: i128); /// Creates this contract asset's unlimited trustline for the provided /// address. /// /// This is a no-op if the input address is a C-address, or if the /// provided G-address already has the respective trustline. /// /// If the trustline is actually created, this will require authorization /// from `addr` (i.e. `addr.require_auth()` will be called). /// /// # Arguments /// /// * `addr` - The address for which a trustline will be created. /// /// # Panics /// /// If the asset issuer does not exist, or if a new trustline cannot be /// created. fn trust(env: Env, addr: Address); } ``` ## Contract Errors All built-in smart contracts on the Stellar network share the same error types, outlined below. ```rust #[derive(Debug, FromPrimitive, PartialEq, Eq)] pub(crate) enum ContractError { // Indicates an internal error in protocol implementation, such as invalid // ledger state. This may not happen in the real networks, but might appear // when using malformed test data (such as malformed ledger snapshots). InternalError = 1, // Indicates an impossible function has been invoked, such as clawback for // an asset that does not have clawback enabled. Or, an operation has been // called affecting the issuer's trustline. OperationNotSupportedError = 2, // Indicates the SAC has already been initialized. This error may only occur // during initialization of an asset's SAC instance. AlreadyInitializedError = 3, // Unused = 4, - this error code is not used by SAC // Unused = 5, - this error code is not used by SAC // An account that would be modified by this transaction does not exist on // the network. AccountMissingError = 6, // Unused = 7, - this error code is not used by SAC // Indicates an amount less than zero was provided for a transfer amount. NegativeAmountError = 8, // Indicates an insufficient spender's available allowance amount. Also used // to indicate a problem with expiration ledger when creating an allowance. AllowanceError = 9, // Indicates too low of a balance to spend the requested amount, or a // balance as a result of this transaction would be too low or high, or a // problem with attempting a clawback on a non-clawback-enabled trustline. BalanceError = 10, // Indicates an address has had its balance authorization revoked by the // asset issuer. BalanceDeauthorizedError = 11, // Indicates this transaction would result in a spender's allowance // overflowing. OverflowError = 12, // Indicates a trustline entry does not exist for this address to hold this // asset. TrustlineMissingError = 13, } ``` Source: https://github.com/stellar/rs-soroban-env/blob/main/soroban-env-host/src/builtin_contracts/contract_error.rs [assets]: ../learn/fundamentals/stellar-data-structures/assets.mdx [`token` module]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/index.html [`TokenInterface` trait]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/trait.TokenInterface.html [`TokenClient` struct]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/struct.TokenClient.html [`StellarAssetInterface` trait]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/trait.StellarAssetInterface.html [`StellarAssetClient` struct]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/struct.StellarAssetClient.html [cap-46-6 smart contract standardized asset]: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-06.md [sep-41 token interface]: ./token-interface.mdx [CAP-73]: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0073.md --- ## Create Contract Tokens on Stellar: Standards & Integration :::info The term "custom token" has been deprecated in favor of "contract token". View the conversation in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). ::: # Token Interface Token contracts, including the Stellar Asset Contract and example token implementations expose the following common interface. Tokens deployed on Soroban can implement any interface they choose, however, they should satisfy the following interface to be interoperable with contracts built to support Soroban's built-in tokens. Note, that in the specific cases the interface doesn't have to be fully implemented. For example, the contract token may not implement the administrative interface compatible with the Stellar Asset Contract - it won't stop it from being usable in the contracts that only perform the regular user operations (transfers, allowances, balances etc.). ### Compatibility Requirements For any given contract function, there are three requirements that should be consistent with the interface described here: - Function interface (name and arguments) - if not consistent, then the users simply won't be able to use the function at all. This is the hard requirement. - Authorization - the users have to authorize the token function calls with all the arguments of the invocation (see the interface comments). If this is inconsistent, then the contract token may have issues with getting the correct signatures from the users and may also confuse the wallet software. - Events - the token has to emit the events in the specified format. If inconsistent, then the token may not be handled correctly by the downstream systems such as block explorers. ### Code The interface below uses the Rust [soroban-sdk](../tools/sdks/contract-sdks.mdx#soroban-rust-sdk) to declare a trait that complies with the [SEP-41](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md) token interface. ```rust pub trait TokenInterface { /// Returns the allowance for `spender` to transfer from `from`. /// /// The amount returned is the amount that spender is allowed to transfer /// out of from's balance. When the spender transfers amounts, the allowance /// will be reduced by the amount transferred. /// /// # Arguments /// /// * `from` - The address holding the balance of tokens to be drawn from. /// * `spender` - The address spending the tokens held by `from`. fn allowance(env: Env, from: Address, spender: Address) -> i128; /// Set the allowance by `amount` for `spender` to transfer/burn from /// `from`. /// /// The amount set is the amount that spender is approved to transfer out of /// from's balance. The spender will be allowed to transfer amounts, and /// when an amount is transferred the allowance will be reduced by the /// amount transferred. /// /// # Arguments /// /// * `from` - The address holding the balance of tokens to be drawn from. /// * `spender` - The address being authorized to spend the tokens held by /// `from`. /// * `amount` - The tokens to be made available to `spender`. /// * `live_until_ledger` - The ledger number where this allowance expires. Cannot /// be less than the current ledger number unless the amount is being set to 0. /// An expired entry (where live_until_ledger < the current ledger number) /// should be treated as a 0 amount allowance. /// /// # Events /// /// Emits an event with topics `["approve", from: Address, /// spender: Address], data = [amount: i128, live_until_ledger: u32]` fn approve(env: Env, from: Address, spender: Address, amount: i128, live_until_ledger: u32); /// Returns the balance of `id`. /// /// # Arguments /// /// * `id` - The address for which a balance is being queried. If the /// address has no existing balance, returns 0. fn balance(env: Env, id: Address) -> i128; /// Transfer `amount` from `from` to `to`. /// /// # Arguments /// /// * `from` - The address holding the balance of tokens which will be /// withdrawn from. /// * `to` - The address which will receive the transferred tokens. /// * `amount` - The amount of tokens to be transferred. /// /// # Events /// /// Emits an event with: /// * topics `["transfer", from: Address, to: Address]` /// * data `{ to_muxed_id: Option, amount: i128 }: Map` /// /// Legacy implementations may emit an event with: /// * topics `["transfer", from: Address, to: Address]` /// * data `amount: i128` fn transfer(env: Env, from: Address, to: MuxedAddress, amount: i128); /// Transfer `amount` from `from` to `to`, consuming the allowance that /// `spender` has on `from`'s balance. Authorized by spender /// (`spender.require_auth()`). /// /// The spender will be allowed to transfer the amount from from's balance /// if the amount is less than or equal to the allowance that the spender /// has on the from's balance. The spender's allowance on from's balance /// will be reduced by the amount. /// /// # Arguments /// /// * `spender` - The address authorizing the transfer, and having its /// allowance consumed during the transfer. /// * `from` - The address holding the balance of tokens which will be /// withdrawn from. /// * `to` - The address which will receive the transferred tokens. /// * `amount` - The amount of tokens to be transferred. /// /// # Events /// /// Emits an event with topics `["transfer", from: Address, to: Address], /// data = amount: i128` fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128); /// Burn `amount` from `from`. /// /// Reduces from's balance by the amount, without transferring the balance /// to another holder's balance. /// /// # Arguments /// /// * `from` - The address holding the balance of tokens which will be /// burned from. /// * `amount` - The amount of tokens to be burned. /// /// # Events /// /// Emits an event with topics `["burn", from: Address], data = amount: /// i128` fn burn(env: Env, from: Address, amount: i128); /// Burn `amount` from `from`, consuming the allowance of `spender`. /// /// Reduces from's balance by the amount, without transferring the balance /// to another holder's balance. /// /// The spender will be allowed to burn the amount from from's balance, if /// the amount is less than or equal to the allowance that the spender has /// on the from's balance. The spender's allowance on from's balance will be /// reduced by the amount. /// /// # Arguments /// /// * `spender` - The address authorizing the burn, and having its allowance /// consumed during the burn. /// * `from` - The address holding the balance of tokens which will be /// burned from. /// * `amount` - The amount of tokens to be burned. /// /// # Events /// /// Emits an event with topics `["burn", from: Address], data = amount: /// i128` fn burn_from(env: Env, spender: Address, from: Address, amount: i128); /// Returns the number of decimals used to represent amounts of this token. /// /// # Panics /// /// If the contract has not yet been initialized. fn decimals(env: Env) -> u32; /// Returns the name for this token. /// /// # Panics /// /// If the contract has not yet been initialized. fn name(env: Env) -> String; /// Returns the symbol for this token. /// /// # Panics /// /// If the contract has not yet been initialized. fn symbol(env: Env) -> String; } ``` :::caution[CAUTION WHEN MODIFYING ALLOWANCES] The `approve` function overwrites the previous value with `amount`, so it is possible for the previous allowance to be spent in an earlier transaction before `amount` is written in a later transaction. The result of this is that `spender` can spend more than intended. This issue can be avoided by first setting the allowance to 0, verifying that the spender didn't spend any portion of the previous allowance, and then setting the allowance to the new desired amount. You can read more about this issue here - https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729. ::: ### Events: SEP-41 Tokens vs. Stellar Asset Contracts The event shapes in the interface above are the ones a SEP-41 contract token emits. A [Stellar Asset Contract](./stellar-asset-contract.mdx) — the built-in contract every Stellar asset has — emits the same event names with **one extra topic**: the asset's [SEP-11](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0011.md) identifier, always last. Code written against the shapes above will therefore mis-parse an event emitted by a Stellar Asset Contract, reading the asset identifier as a missing field or ignoring it entirely. [CAP-67](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md) is the normative source for the Stellar Asset Contract shapes below, and for the unification of classic events described further down. | Event | SEP-41 contract token | Stellar Asset Contract | | --- | --- | --- | | `approve` | `["approve", from, spender]` | `["approve", from, spender, sep0011_asset]` | | `transfer` | `["transfer", from, to]` | `["transfer", from, to, sep0011_asset]` | | `burn` | `["burn", from]` | `["burn", from, sep0011_asset]` | | `mint` | not part of this interface | `["mint", to, sep0011_asset]` | | `clawback` | not part of this interface | `["clawback", from, sep0011_asset]` | | `set_authorized` | not part of this interface | `["set_authorized", id, sep0011_asset]` | | `set_admin` | not part of this interface | `["set_admin", admin, sep0011_asset]` | On the Stellar Asset Contract side, the extra topic does not change the data payload: `amount: i128` for `transfer`, `mint`, `burn`, and `clawback`; `[amount: i128, live_until_ledger: u32]` for `approve`; `authorize: bool` for `set_authorized`; and `new_admin: Address` for `set_admin`. When the destination of a Stellar Asset Contract `transfer` or `mint` carries a multiplexing id, the data becomes `{ amount: i128, to_muxed_id }: Map` instead — see [Monitoring Payments as event stream](../build/guides/transactions/send-and-receive-payments.mdx#monitoring-payments-as-event-stream) for a worked example. A SEP-41 contract token keeps whatever payload the interface above documents for it, so do not carry these payload shapes back across the table. :::caution[A Stellar Asset Contract transfer does not always emit a transfer event] When the asset issuer is one side of the transfer, the contract emits a different event: `mint` if `from` is the issuer, `burn` if `to` is the issuer — and the `burn` drops any multiplexing id. You only get a `transfer` event when neither side is the issuer, or when both are. See [Interacting with classic Stellar assets](./stellar-asset-contract.mdx#interacting-with-classic-stellar-assets). ::: #### Telling a classic operation from a contract invocation Since protocol 23, classic operations emit these same events, published under the asset's Stellar Asset Contract address whether or not that contract has been deployed — see [Tracking the movement of value](../learn/fundamentals/stellar-data-structures/events.mdx#tracking-the-movement-of-value). Not all of them, though. `approve` and `set_admin` are never emitted by a classic operation, because no classic operation is equivalent to them: allowances and contract administration exist only on the contract side. So those two always come from a contract invocation, and the rest — `transfer`, `mint`, `burn`, `clawback`, and `set_authorized` — can come from either path. The two paths produce byte-identical events, so nothing inside an event distinguishes them. To tell them apart, join the event back to its operation using `txHash` and `operationIndex`, then read the operation's type. Note that this separates a classic operation from a contract invocation, not a direct call to the asset contract from one made by another contract on a user's behalf — both of those are `InvokeHostFunction` operations emitting the same event. If you would rather not do that join yourself, the [Token Transfer Processor](../data/indexers/build-your-own/processors/token-transfer-processor/README.mdx) normalizes both paths into a single stream of typed events. One event in the unified set has no counterpart here: `fee` is transaction-level, carries only two topics and no asset identifier, and is never emitted by a contract function. ### Metadata Another requirement for complying with the token interface is to write the standard metadata (`decimal`, `name`, and `symbol`) for the token in a specific format. This format allows users to directly read constant data from the ledger instead of invoking a Wasm function. The [token example](https://github.com/stellar/soroban-examples/blob/main/token/src/metadata.rs) demonstrates how to use the Rust [soroban-token-sdk](https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-token-sdk/src/lib.rs) to write the metadata, and we strongly encourage token implementations to follow this approach. ### Handling Failure Conditions In the token interface, there are several instances where function calls can fail due to various reasons such as lack of proper authorization, insufficient allowance or balance, etc. To handle these failure conditions, it is important to specify the expected behavior when such situations arise. Its important to note the that the token interface not only incorporates the authorization concept for matching asset authorization in Stellar Classic, but it also utilizes the Soroban authorization mechanism. So, if you try to make a token call and it fails, it could be because of either token authorization processes. To provide more context, when you use the token interface, there is a function called `authorized` that returns "true" if an address has token authorization. More details on Authorization can be found [here](../learn/fundamentals/contract-development/authorization.mdx). For the functions in the token interface, [trapping](https://doc.rust-lang.org/book/ch09-00-error-handling.html) should be used as the standard way to handle failure conditions since the interface is not designed to return error codes. This means that when a function encounters an error, it will halt execution and revert any state changes that occurred during the function call. ### Failure Conditions Here is a list of basic failure conditions and their expected behavior for functions in the token interface: #### Admin functions: - If the admin did not authorize the call, the function should trap. - If the admin attempts to perform an invalid action (e.g., minting a negative amount), the function should trap. #### Token functions: - If the caller is not authorized to perform the action (e.g., transferring tokens without proper authorization), the function should trap. - If the action would result in an invalid state (e.g., transferring more tokens than available in the balance or allowance), the function should trap. ### Example: Handling Insufficient Allowance in `burn_from` function In the `burn_from` function, the token contract should check whether the spender has enough allowance to burn the specified amount of tokens from the `from` address. If the allowance is insufficient, the function should trap, halting execution and reverting any state changes. Here's an example of how the `burn_from` function can be modified to handle this failure condition: ```rust fn burn_from( env: soroban_sdk::Env, spender: Address, from: Address, amount: i128, ) { // Check if the spender has enough allowance let current_allowance = allowance(env, from, spender); if current_allowance < amount { // Trap if the allowance is insufficient panic!("Insufficient allowance"); } // Proceed with burning tokens // ... } ``` By clearly outlining how to handle failures and incorporating the right error management techniques in the token interface, we can make token contracts stronger and safer. --- ## Blockchain Development Tools: SDKs, APIs & Utilities for Building # Tools Overview This section of the docs will provide links to the SDKs and developer tools for use when developing on Stellar as well as documentation for various SDF-maintained platforms such as the Anchor Platform and Stellar Disbursement Platform (SDP). ## Developer Tools ### [SDKs](./sdks/README.mdx) Stellar’s Software Development Kits (SDKs) provide devs with the tools, libraries, and documentation to interact with and develop on the blockchain. They simplify tasks such as creating and deploying smart contracts and sending transactions while also offering APIs to access data and integrate functionalities into applications. ### [Stellar CLI](./cli/README.mdx) The command line interface to Soroban smart contracts. It allows you to build, deploy, and interact with smart contracts; configure identities; generate key pairs; manage networks; and more. ### [Lab](./lab/README.mdx) Stellar Lab is our new go-to tool for development, experimenting, and testing, as well as exploring APIs developers use to interact with the Stellar network. ### [Quickstart](./quickstart/README.mdx) Quickstart is a local Stellar network environment (node) that allows developers to run a local version of the Stellar network for development and testing. ### [OpenZeppelin Relayer](./openzeppelin-relayer.mdx) OpenZeppelin Relayer, also known as Stellar Channels Service, is a managed infrastructure for submitting Stellar Soroban transactions with automatic parallel processing and fee management. The service handles all the complexity of transaction submission, allowing you to focus on building your application. ### [OpenZeppelin Contracts](./openzeppelin-contracts.mdx) OpenZeppelin Stellar Contracts is a collection of audited contracts and utilities for Stellar. The contracts are developed by OpenZeppelin in collaboration with the Stellar community and the Stellar Development Foundation (SDF), in an effort to bring a library of high-quality and audited contracts that can be used to build applications on the Stellar network. ### [Scaffold Stellar](./scaffold-stellar.mdx) Scaffold Stellar is a developer toolkit for building decentralized applications (dApps) and smart contracts on Stellar. It provides CLI tools, reusable contract templates, a smart contract registry, and a modern frontend. ### [More Developer Tools](./developer-tools/README.mdx) Find other SDF and ecosystem-maintained developer tools that help streamline the development process for applications and smart contracts on Stellar. ## SDF Platforms ### [Anchor Platform](../platforms/anchor-platform/README.mdx) The Anchor Platform is a set of tools and APIs that enable developers and businesses to build their own on and off-ramp services for the Stellar network. It provides a standardized interface, including the implementation of several Stellar Ecosystem Proposals (SEPs), to make it easy for businesses to integrate with Stellar-based wallets and exchanges. ### [Stellar Disbursement Platform (SDP)](../platforms/stellar-disbursement-platform/README.mdx) The Stellar Disbursement Platform (SDP) is a tool built for organizations to make bulk payments to a group of recipients over the Stellar network. --- ## Stellar CLI The command line interface to Soroban smart contracts. It allows you to build, deploy, and interact with smart contracts; configure identities; generate key pairs; manage networks; and more. Install Stellar CLI as explained in [Setup](../../build/smart-contracts/getting-started/setup.mdx#install-the-stellar-cli). For examples on how to use the Stellar CLI, please see [Stellar CLI Guides](./cookbook/README.mdx). The auto-generated comprehensive reference documentation is available [here](stellar-cli.mdx). --- ## Cookbook Guides that will help you use the Stellar CLI. --- ## Asset Management The [stellar-cli] can be used to issue and administer assets on the Stellar network. The following guide assumes that the [stellar-cli] has been configured to use testnet, or a local test network. To use testnet, run the following command. ```bash stellar network use testnet ``` ### Asset Issuance To create a Stellar Asset using the [stellar-cli] several steps take place: - An issuer account must exist. - An asset code selected. - Optionally, an admin is configured. #### Issuer Creation To create an issuer, generate a key and fund it. Funding uses friendbot, the testnet faucet, and is only available on testnet. ```bash stellar keys generate issuer ``` ```bash stellar keys fund issuer ``` View the public key with: ```bash stellar keys public-key issuer ``` Configure the issuer key to be used for signing transactions: ```bash stellar keys use issuer ``` #### Issuer Config Issuer accounts have several configuration flags that can be toggled and affect all assets issued by the issuer. In this guide the issuer account will be setup with the revocable and clawback-enabled features will be enabled. These features allow the asset admin to revoke authorisation to use an asset, which has the effect of freezing their balance, and to clawback, which has the effect of forcibly burning the asset. ```bash stellar tx new set-options \ --set-revocable \ --set-clawback-enabled ``` For other options, see the manual for the [stellar tx new set-options] command. To view the tx before sending, use the `--build-only` option and pipe the transaction through the tx edit command: ```bash cookbooktest.ignore="because piping isn't supported" stellar tx new set-options \ --set-revocable \ --set-clawback-enabled \ --build-only \ | stellar tx edit \ | stellar sign \ | stellar send ``` #### Choosing the Asset Code Choose an asset code 1 to 12 characters long. The asset code along with the issuer account address will uniquely identify the asset. An issuer can issue multiple assets, distinguished by their asset code. For the rest of this guide the asset code `ABC` will be used. #### Asset Contract Setup A built-in contract exists on network for every asset. It's address is reserved and can be calculated using the following command. ```bash stellar contract asset id --asset ABC:issuer ``` The contract is not deployed automatically. Anyone can deploy the contract, it is not an action requiring authorisation. ##### Contract Deployment Deploy the built-in asset contract. The `--asset` option specifies the name of the asset, which is in the format `:`. The `--alias` option stores locally an alias that can be used in subsequent commands to reference the contract address. ```bash stellar contract asset deploy --asset ABC:issuer --alias mycontract ``` Run the following command to get some read only data from the contract to confirm it's deployed. ```bash stellar contract invoke --id mycontract -- name ``` ##### Admin Setup At this point the issuer is the admin of the contract. Let's create another account that'll be the admin. ```bash stellar keys generate admin ``` ```bash stellar keys public-key admin ``` ```bash stellar keys fund admin ``` Using the issuer account, invoke the asset contract's `set_admin` function. ```bash stellar keys use issuer ``` ```bash stellar contract invoke --source issuer --id mycontract -- set_admin --new_admin admin ``` ### Actions Once the asset contract is deployed and setup, the following actions can take place. #### Mint (Admin Only) Generate and fund a user account that the minted asset will be transferred to. ```bash stellar keys generate user ``` ```bash stellar keys fund user ``` ```bash stellar keys public-key user ``` Using the user account, trust the asset so that the user account can hold the asset. ```bash stellar keys use user ``` ```bash stellar tx new change-trust --line ABC:issuer ``` To mint the asset to the user, use the admin account, invoke the asset contract's `mint` function. ```bash stellar keys use admin ``` ```bash stellar contract invoke --id mycontract -- mint --to user --amount 25 ``` Check the balance after the mint. ```bash stellar contract invoke --id mycontract -- balance --id user ``` #### Transfer Generate and fund a second user account that the user account from the previous step can transfer the asset to. ```bash stellar keys generate user2 ``` ```bash stellar keys fund user2 ``` ```bash stellar keys public-key user2 ``` Using the user2 account, trust the asset so that the new user account can hold the asset. ```bash stellar keys use user2 ``` ```bash stellar tx new change-trust --line ABC:issuer ``` Using the user account, invoke the asset contract's `transfer` function. ```bash stellar keys use user ``` ```bash stellar contract invoke --id mycontract -- transfer --from user --to user2 --amount 3 ``` Check the balances after the transfer. ```bash stellar contract invoke --id mycontract -- balance --id user ``` ```bash stellar contract invoke --id mycontract -- balance --id user2 ``` #### Burn Using the user account, invoke the asset contract's `burn` function. ```bash stellar keys use user ``` ```bash stellar contract invoke --id mycontract -- burn --from user --amount 5 ``` Check the balance after the burn. ```bash stellar contract invoke --id mycontract -- balance --id user ``` #### Revoke Authorization / Freeze (Admin Only) An admin can revoke a user's authorisation to use the asset. The user will continue to hold the balance but will be unable to transfer it or create new payments. Any existing liabilities, such as those created by offers created on Stellar, will continue to exist and could still be executed. Using the admin, invoke the asset contract's `set_authorized` function. ```bash stellar keys use admin ``` ```bash stellar contract invoke --id mycontract -- set_authorized --id user --authorize false ``` Using the user account, invoke the asset contract's `transfer` function. The invocation will fail because they are no longer authorized to transfer. ```bash stellar keys use user ``` ```bash cookbooktest.fail cookbooktest.stderr="balance is deauthorized" stellar contract invoke --id mycontract -- transfer --from user --to user2 --amount 3 ``` #### Clawback (Admin Only) An admin can clawback a user's balance. The amount clawed back will be burned. Using the admin, invoke the asset contract's `clawback` function. ```bash stellar keys use admin ``` ```bash stellar contract invoke --id mycontract -- clawback --from user --amount 1 ``` Check the balance after the clawback. ```bash stellar contract invoke --id mycontract -- balance --id user ``` [stellar-cli]: ../stellar-cli.mdx [stellar tx new set-options]: ../stellar-cli.mdx#stellar-tx-new-set-options --- ## Add meta data to contract WASM on build To build a smart contract, use the `stellar contract build` command. The `--meta` option will allow you to add additional meta data entries to the `contractmetav0` custom section of the resulting WASM file at build time. ```bash cookbooktest.ignore export SHA=$(git rev-parse HEAD) ``` ```bash stellar contract build \ --manifest-path ./cmd/crates/soroban-test/tests/fixtures/test-wasms/hello_world/Cargo.toml \ --meta source_repo=https://github.com/stellar/stellar-cli \ --meta commit_sha=$SHA ``` You can then use `stellar contract info meta` to see the meta data: ```bash stellar contract info meta \ --wasm target/wasm32v1-none/release/test_hello_world.wasm ``` ```bash cookbooktest.ignore Contract meta: • rsver: 1.91.0 (Rust version) • rssdkver: 23.0.1#510d3feb724c2b01d7e7ab7652f03b9f8efc3f35 (Soroban SDK version and its commit hash) • cliver: 23.1.4#v20.0.0-646-g70895dd66fb7dbf2f4c2a81cf38546512ba4198e-dirty • source_repo: https://github.com/stellar/stellar-cli • commit_sha: c6f881689d3f4936cb3262089eb70142e9ca9c91 ``` --- ## Contract Invoke: Argument Types When using `stellar contract invoke`, each `--arg-name ` flag is parsed according to the contract's type spec. This guide shows the correct format for each supported type. ### Integers (`u32`, `i32`, `u64`, `i64`) Pass bare numeric literals directly on the command line. ```bash cookbooktest.ignore stellar contract invoke --id mycontract -- my_function \ --count 42 \ --offset -7 ``` ### Large Integers (`u128`, `i128`, `u256`, `i256`) Pass bare numeric literals, just like small integers. The CLI reads the raw value as a string internally, bypassing JSON number precision limits entirely. ```bash cookbooktest.ignore stellar contract invoke --id mycontract -- my_function \ --amount 340282366920938463463374607431768211455 ``` ### Boolean Pass `true` or `false` as unquoted literals. ```bash cookbooktest.ignore stellar contract invoke --id mycontract -- my_function \ --is_active true \ --is_paused false ``` ### String Bare words work for simple strings. Numbers and booleans are also accepted and are automatically converted to their string representation, so you do not need to add extra quoting. Explicit JSON quoting is also accepted. ```bash cookbooktest.ignore # Bare word stellar contract invoke --id mycontract -- my_function \ --label hello # Number auto-converted to string "42" stellar contract invoke --id mycontract -- my_function \ --label 42 # Boolean auto-converted to string "true" stellar contract invoke --id mycontract -- my_function \ --label true # Explicitly quoted JSON string stellar contract invoke --id mycontract -- my_function \ --label '"hello world"' ``` ### Symbol Symbols follow the same rules as `String`: bare words, numbers, and booleans are all accepted, and non-string JSON values are auto-converted. ```bash cookbooktest.ignore stellar contract invoke --id mycontract -- my_function \ --status active ``` ### Address You can pass either a locally known identity name (e.g. `alice`) or a raw Stellar strkey (`G…` for accounts, `C…` for contracts). ```bash cookbooktest.ignore # Using an identity alias stellar contract invoke --id mycontract -- my_function \ --owner alice # Using a raw strkey stellar contract invoke --id mycontract -- my_function \ --owner GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN ``` ### Bytes / BytesN Pass a hex-encoded string representing the byte sequence. ```bash cookbooktest.ignore stellar contract invoke --id mycontract -- my_function \ --hash "deadbeef01020304" ``` ### Optional Arguments Simply omit the flag. The CLI will supply a `void` / null value for any `Option` parameter that is not provided. ```bash cookbooktest.ignore # If `--memo` is optional, leaving it out passes None stellar contract invoke --id mycontract -- my_function \ --amount 100 ``` ### Vec Pass a JSON array, quoting the whole value so the shell does not split it. ```bash cookbooktest.ignore stellar contract invoke --id mycontract -- my_function \ --recipients '["alice", "bob", "carol"]' ``` ### Map Pass a JSON object with string keys. ```bash cookbooktest.ignore stellar contract invoke --id mycontract -- my_function \ --metadata '{"key": "value", "version": "1"}' ``` ### Tuple Pass a JSON array whose elements match the tuple's field order. ```bash cookbooktest.ignore stellar contract invoke --id mycontract -- my_function \ --point '[10, 20]' ``` ### Struct (User-Defined Type) Pass a JSON object with the struct's named fields. ```bash cookbooktest.ignore stellar contract invoke --id mycontract -- my_function \ --config '{"max_supply": "1000000", "decimals": 7}' ``` ### Enum / Union (User-Defined Type) For unit variants, pass the variant name as a plain string. For variants that carry a value, pass a JSON object with the variant name as the key. ```bash cookbooktest.ignore # Unit variant stellar contract invoke --id mycontract -- my_function \ --status "Active" # Variant with a value stellar contract invoke --id mycontract -- my_function \ --result '{"Ok": 42}' ``` ### File-Based Arguments For long or complex JSON values, use the `--arg-file-path` flag variant to load the value from a file instead of inlining it on the command line. The flag name is derived from the argument name by appending `-file-path`. ```bash cookbooktest.ignore # Write the JSON to a file first echo '{"max_supply": "1000000", "decimals": 7}' > config.json # Then reference the file stellar contract invoke --id mycontract -- my_function \ --config-file-path config.json ``` For `Bytes` and `BytesN` parameters, the file's raw binary content is read directly — no hex encoding is required. ```bash cookbooktest.ignore stellar contract invoke --id mycontract -- my_function \ --hash-file-path /path/to/binary.bin ``` --- ## Contract Lifecycle(Cookbook) To manage the lifecycle of a Stellar smart contract using the CLI, follow these steps: 1. Set your preferred network. For this guide, we will use `testnet`. A list of available networks can be found [here](../../../networks/README.mdx) ```bash stellar network use testnet ``` 2. Create an identity for Alice: ```bash stellar keys generate alice -q ``` 3. Fund the identity: ```bash stellar keys fund alice ``` 4. Deploy a contract: ```bash stellar keys use alice ``` ```bash stellar contract deploy --wasm /path/to/contract.wasm --alias mycontract ``` This will display the resulting contract ID, e.g.: ``` CBB65ZLBQBZL5IYHDHEEPCVUUMFOQUZSQKAJFV36R7TZETCLWGFTRLOQ ``` To learn more about how to build contract `.wasm` files, take a look at our [getting started tutorial](../../../build/smart-contracts/getting-started/setup.mdx). 5. Invoke a contract function: ```bash stellar contract invoke --id mycontract -- ``` 6. View the contract's state: ```bash stellar contract read --id mycontract --durability --key ``` Note: `` is either `persistent` or `temporary`. `KEY` provides the key of the storage entry being read. 7. Manage expired states: ```bash stellar contract extend --id mycontract --ledgers-to-extend 1000 --durability --key ``` This extends the state of the instance provided by the given key to at least 1000 ledgers from the current ledger. --- ## Deploy a contract from uploaded Wasm bytecode To deploy an instance of a compiled smart contract that has already been uploaded onto the Stellar network, use the `stellar contract deploy` command: ```bash stellar contract deploy \ --source S... \ --network testnet \ --wasm-hash \ --alias ``` :::tip Optionally assign an alias by replacing `` with your desired alias name for the contract. The alias is a locally stored mapping to the contract address, and can be used in other stellar-cli commands in place of the address. ::: --- ## Deploy the Stellar Asset Contract for a Stellar asset The Stellar CLI can deploy a [Stellar Asset Contract] for a Stellar asset so that any Stellar smart contract can interact with the asset. Every Stellar asset has reserved a contract that anyone can deploy. Once deployed any contract can interact with that asset by holding a balance of the asset, receiving the asset, or sending the asset. Deploying the Stellar Asset Contract for a Stellar asset enables that asset for use in smart contracts. The Stellar Asset Contract can be deployed for any possible Stellar asset, either assets already in use on Stellar or assets that have never seen any activity. This means that the issuer doesn't need to have been created, and no one needs to be yet holding the asset on Stellar. To perform the deploy, use the following command: ```bash cookbooktest.ignore stellar contract asset deploy \ --source S... \ --network testnet \ --asset USDC:GCYEIQEWOCTTSA72VPZ6LYIZIK4W4KNGJR72UADIXUXG45VDFRVCQTYE ``` The `asset` argument corresponds to the symbol and it's issuer address, which is how assets are identified on Stellar. The same can be done for the native [Lumens] asset: ```bash cookbooktest.fail cookbooktest.stderr="contract already exists" stellar contract asset deploy \ --source S... \ --network testnet \ --asset native ``` :::note Deploying the native asset will fail on testnet or mainnet as a Stellar Asset Contract already exists. ::: For any asset, the contract address can be fetched with: ```bash stellar contract id asset \ --network testnet \ --asset native ``` [stellar asset contract]: ../../../tokens/stellar-asset-contract.mdx [lumens]: ../../../learn/fundamentals/lumens.mdx [sep-41]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md [`soroban_sdk::token`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/ [`token::tokenclient`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/struct.TokenClient.html [`token::stellarassetclient`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/struct.StellarAssetClient.html --- ## Extend a deployed contract instance's TTL You can use the Stellar CLI to extend the TTL of a contract instance like so: ```bash stellar contract extend \ --source S... \ --network testnet \ --id C... \ --ledgers-to-extend 535679 \ --durability persistent ``` This example uses 535,679 ledgers as the new archival TTL. This is the maximum allowable value for this argument on the CLI. This corresponds to roughly 30 days (averaging 5 second ledger close times). When you extend a contract instance, this includes: - the contract instance itself - any `env.storage().instance()` entries in the contract - the contract's Wasm code --- ## Extend a deployed contract's storage entry TTL You can use the Stellar CLI to extend the TTL of a contract's persistent storage entry. For a storage entry that uses a simple `Symbol` as its storage key, you can run a command like so: ```bash stellar contract extend \ --source S... \ --network testnet \ --id C... \ --key COUNTER \ --ledgers-to-extend 535679 \ --durability persistent ``` This example uses 535,679 ledgers as the new archival TTL. This is the maximum allowable value for this argument on the CLI. This corresponds to roughly 30 days (averaging 5 second ledger close times). If your storage entry uses a more advanced storage key, such as `Balance(Address)` in a token contract, you'll need to provide the key in a base64-encoded XDR form: ```bash stellar contract extend \ --source S... \ --network testnet \ --id C... \ --key-xdr AAAABgAAAAHXkotywnA8z+r365/0701QSlWouXn8m0UOoshCtNHOYQAAAA4AAAAHQmFsYW5jZQAAAAAB \ --ledgers-to-extend 535679 \ --durability persistent ``` :::info Be sure to check out our [guide on creating XDR ledger keys](../../../build/guides/rpc/generate-ledger-keys-python.mdx) for help generating them. ::: --- ## Extend a deployed contract's Wasm code TTL You can use the Stellar CLI to extend the TTL of a contract's Wasm bytecode. This can be done in two forms: if you do or do not have the compiled contract locally. If you do have the compiled binary on your local machine: ```bash stellar contract extend \ --source S... \ --network testnet \ --wasm ../relative/path/to/soroban_contract.wasm \ --ledgers-to-extend 535679 \ --durability persistent ``` This example uses 535,679 ledgers as the new archival TTL. This is the maximum allowable value for this argument on the CLI. This corresponds to roughly 30 days (averaging 5 second ledger close times). If you do not have the compiled binary on your local machine, you can still use the CLI to extend the bytecode TTL. You'll need to know the Wasm hash of the installed contract code: ```bash stellar contract extend \ --source S... \ --network testnet \ --wasm-hash \ --ledgers-to-extend 535679 \ --durability persistent ``` :::info You can learn more about finding the correct Wasm hash for a contract instance [here (JavaScript)](../../../build/guides/rpc/retrieve-contract-code-js.mdx) and [here (Python)](../../../build/guides/rpc/retrieve-contract-code-python.mdx). ::: --- ## Payments and Assets To send payments and work with assets using the Stellar CLI, follow these steps: 1. Set your preferred network. For this guide, we will use `testnet`. A list of available networks can be found [here](../../../networks/README.mdx) ```bash stellar network use testnet ``` 2. Fund the accounts: ```bash stellar keys generate alice ``` ```bash stellar keys fund alice ``` ```bash stellar keys generate bob ``` ```bash stellar keys fund bob ``` 3. Obtain the stellar asset contract ID: ```bash stellar contract id asset --asset native ``` 4. Get Bob's public key: ```bash stellar keys address bob ``` 5. Send 100 XLM from Alice to Bob: ```bash stellar keys use alice ``` ```bash stellar contract invoke --id -- transfer --to bob --from alice --amount 100 ``` 6. Check account balance: ```bash stellar contract invoke --id -- balance --id bob ``` For more information on the functions available to the stellar asset contract, see the [token interface code](../../../tokens/token-interface.mdx#code) --- ## Restore an archived contract using the Stellar CLI If your contract instance has been archived, it can easily be restored using the Stellar CLI. ```bash stellar contract restore \ --source S... \ --network testnet \ --id C... \ --durability persistent ``` --- ## Restore archived contract data using the Stellar CLI If a contract's persistent storage entry has been archived, you can restore it using the Stellar CLI. For a storage entry that uses a simple `Symbol` as its storage key, you can run a command like so: ```bash stellar contract restore \ --source S... \ --network testnet \ --id C... \ --key COUNTER \ --durability persistent ``` If your storage entry uses a more advanced storage key, such as `Balance(Address)` in a token contract, you'll need to provide the key in a base64-encoded XDR form: ```bash stellar contract restore \ --source S... \ --network testnet \ --id C... \ --key-xdr AAAABgAAAAHXkotywnA8z+r365/0701QSlWouXn8m0UOoshCtNHOYQAAAA4AAAAHQmFsYW5jZQAAAAAB \ --durability persistent ``` :::info Be sure to check out our [guide on creating XDR ledger keys](../../../build/guides/rpc/generate-ledger-keys-python.mdx) for help generating them. ::: --- ## Stellar Keys This guide walks you through generating, inspecting, and removing a Stellar identity using the CLI. ### 1. Generate a New Identity Run the following command to create a new keypair and save it under the alias named `carol`: ```bash stellar keys generate carol ``` Output: ``` ✅ Key saved with alias carol in ".config/soroban/identity/carol.toml" ``` The CLI stores this identity in a TOML file. ### 2. Verify the Identity File Navigate to the configuration directory and list the contents: ```bash cookbooktest.ignore cd .config/soroban/identity && ls ``` Output: ``` carol.toml ``` The file carol.toml contains the seed phrase for your identity. ### 3. View the Seed Phrase ```bash cookbooktest.ignore cat carol.toml ``` Output: ``` seed_phrase = "patrol clean public grocery roof aim have valve cherry dismiss lunar tail duty license capable little version banana amount often cover dice couple party" ``` :::danger Note: The seed phrase is sensitive information. Handle it with care and never expose it publicly. ::: ### 4. Derive the Secret Key To display the secret key for the carol identity: ```bash stellar keys secret carol ``` Output: ``` SCJP663VYFZPYN75H2DYJA3FYUBP5UR23HZ4ZDHDMDY6TXVYUYMWNKTI ``` :::danger Note: The secret key is sensitive information. Handle it with care and never expose it publicly. ::: ### 5. Derive the Public Key To display the corresponding public key for the carol identity: ```bash stellar keys public-key carol ``` Output: ``` GD3BFFX7DTNJAGDVVM5RYGGQQNURZTH4VSBLWF55YXY3L6T2WWZK57EI ``` This is the public address of your key. ### 6. Fund this account ```bash stellar keys fund carol ``` Output: ``` ✅ Account carol funded on "Test SDF Network ; September 2015" ``` You can also fund the account while creating the key by using `stellar keys generate --fund`. ### 7. Remove the Identity When you no longer need this identity, remove it using: ```bash stellar keys rm carol --force ``` Output: ``` ℹ️ Removing the key's cli config file ``` At this point, the identity file carol.toml is deleted, and the alias is no longer available in the CLI. --- ## Create Claimable Balance(Cookbook) Claimable balances allow you to send payments that can be claimed by the recipient based on specific conditions. This is useful for escrow-like functionality, conditional payments, or time-locked transfers. ## Setup For the following examples we will use these accounts: ```bash stellar network use testnet ``` ```bash stellar keys generate --fund alice ``` ```bash stellar keys generate bob --fund ``` ```bash stellar keys generate charlie --fund ``` ## Basic Examples ### Unconditional Claimable Balance Create a claimable balance that can be claimed immediately by the recipient: ```bash stellar tx new create-claimable-balance \ --source alice \ --amount 50_000_000 \ --asset native \ --claimant bob ``` You can also explicitly specify unconditional: ```bash stellar tx new create-claimable-balance \ --source alice \ --amount 50_000_000 \ --asset native \ --claimant 'bob:"unconditional"' ``` ### Multiple Claimants Create a claimable balance that can be claimed by any of multiple recipients: ```bash stellar tx new create-claimable-balance \ --source alice \ --amount 75_000_000 \ --asset native \ --claimant bob \ --claimant charlie ``` ## Time-Based Predicates ### Before Absolute Time Create a claimable balance that must be claimed before a specific date: ```bash stellar tx new create-claimable-balance \ --source alice \ --amount 30_000_000 \ --asset native \ --claimant 'bob:{"before_absolute_time":"1735689599"}' ``` ### Before Relative Time Create a claimable balance that must be claimed within 1 hour (3600 seconds) from creation: ```bash stellar tx new create-claimable-balance \ --source alice \ --amount 25_000_000 \ --asset native \ --claimant 'bob:{"before_relative_time":"3600"}' ``` ## Logical Predicates ### Not Predicate Create a claimable balance that can be claimed after a certain time (NOT before relative time): ```bash stellar tx new create-claimable-balance \ --source alice \ --amount 40_000_000 \ --asset native \ --claimant 'bob:{"not":{"before_relative_time":"7200"}}' ``` This means: "Can be claimed, but NOT before 2 hours have passed" (effectively: can be claimed after 2 hours). ### And Predicate Create a claimable balance with multiple conditions that must ALL be true: ```bash stellar tx new create-claimable-balance \ --source alice \ --amount 60_000_000 \ --asset native \ --claimant 'bob:{"and":[{"before_absolute_time":"1735689599"},{"not":{"before_relative_time":"86400"}}]}' ``` This means: "Can be claimed before Dec 31, 2024 AND after 1 day has passed". ### Or Predicate Create a claimable balance where either condition can be true: ```bash stellar tx new create-claimable-balance \ --source alice \ --amount 35_000_000 \ --asset native \ --claimant 'bob:{"or":[{"before_relative_time":"3600"},{"not":{"before_absolute_time":"1717200000"}}]}' ``` This means: "Can be claimed within 1 hour OR after June 1, 2024". ## Complex Examples ### Mixed Claimants with Different Predicates Create a claimable balance with multiple claimants having different claim conditions: ```bash stellar tx new create-claimable-balance \ --source alice \ --amount 100_000_000 \ --asset native \ --claimant 'bob:{"before_relative_time":"3600"}' \ --claimant 'charlie:{"not":{"before_relative_time":"86400"}}' ``` This creates a balance where: - Bob can claim within 1 hour - Charlie can claim after 1 day has passed ### Escrow-Style Payment Create an escrow where the recipient has 30 days to claim, otherwise it returns to sender: ```bash stellar tx new create-claimable-balance \ --source alice \ --amount 200_000_000 \ --asset native \ --claimant 'bob:{"before_relative_time":"2592000"}' \ --claimant 'alice:{"not":{"before_relative_time":"2592000"}}' ``` This means: - Bob can claim within 30 days - Alice can reclaim after 30 days if Bob hasn't claimed ### Custom Asset Claimable Balance Create a claimable balance for a custom asset: ```bash cookbooktest.ignore stellar tx new create-claimable-balance \ --source alice \ --amount 1000_000_000 \ --asset USDC:GBUG7QTBTT47XVDVE6RZYWRUZBPLOIO57INE6LYZDMIXMMDCREQRUQKI \ --claimant 'bob:{"before_absolute_time":"1735689599"}' ``` ## Understanding Predicates **Note**: Predicates must be valid JSON, so you must use proper quoting and escaping. ### Predicate Types | Type | Description | Example | | --- | --- | --- | | `unconditional` | Can be claimed anytime | `"unconditional"` | | `before_absolute_time` | Must claim before specific timestamp | `{"before_absolute_time":"1735689599"}` | | `before_relative_time` | Must claim within X seconds from creation | `{"before_relative_time":"3600"}` | | `not` | Negates another predicate | `{"not":{...}}` | | `and` | Both predicates must be true | `{"and":[{...},{...}]}` | | `or` | Either predicate can be true | `{"or":[{...},{...}]}` | ### Time Format Notes - **Absolute times**: Use Unix timestamps (seconds since epoch) - **Relative times**: Seconds from claimable balance creation time - **Timestamps**: Must be in the future when creating the balance ## Common Use Cases ### 1. Simple Payment with Deadline ```bash # Bob has 7 days to claim stellar tx new create-claimable-balance \ --source alice \ --amount 50_000_000 \ --claimant 'bob:{"before_relative_time":"604800"}' ``` ### 2. Delayed Payment ```bash # Bob can only claim after 1 day stellar tx new create-claimable-balance \ --source alice \ --amount 100_000_000 \ --claimant 'bob:{"not":{"before_relative_time":"86400"}}' ``` ### 3. Emergency Fund ```bash # Bob can claim immediately, but if he doesn't claim within 48 hours, Alice can reclaim stellar tx new create-claimable-balance \ --source alice \ --amount 500_000_000 \ --claimant 'bob:{"before_relative_time":"172800"}' \ --claimant 'alice:{"not":{"before_relative_time":"172800"}}' ``` ## Notes - `--amount`: Amount in stroops (1 XLM = 10,000,000 stroops) - `--asset`: Use "native" for XLM or "CODE:ISSUER" format for other assets - `--claimant`: Account with optional predicate (can be specified multiple times) - Predicates are evaluated when the claimable balance is claimed, not when created - `And` and `Or` predicates must have exactly 2 sub-predicates - Complex predicates can be nested to create sophisticated claim conditions --- ## tx Commands So far the examples of the CLI interacting with the blockchain have been through the `contract` command. Uploading contracts, deploying contracts, and invoking them. Each of these are different types of transactions, which must be signed and submitted to the network (and in the case of contract related transactions simulated first). Technically these three are different operations, of which a transaction can contain up to 100 operations. However, in the case of contract related operations a transaction is limited to just one. So for all other transactions the CLI provides the `tx` subcommands. These are: - `new` - `sign` - `send` - `simulate` ## `tx new` For the following examples we will use the following accounts: ```sh stellar keys generate --fund alice --network testnet stellar keys generate bob # You can add a public key to the keys stellar keys add --public-key GBUG7QTBTT47XVDVE6RZYWRUZBPLOIO57INE6LYZDMIXMMDCREQRUQKI charlie ## and use testnet stellar network use testnet ``` ### Create Account Creates and funds a new Stellar account. Above `alice` was funded by [friendbot](../../../networks/README.mdx#friendbot). However, `bob` and `charlie` were not. So we can use the `create-account` command to fund them. `bob` will receive 10 XLM and `charlie` will get 1 XLM. ```sh stellar tx new create-account \ --source alice \ --destination bob \ --starting-balance 100_000_000 stellar tx new create-account \ --source alice \ --destination charlie \ --starting-balance 10_000_000 ``` Notes: - `--starting-balance`: Initial balance in stroops to fund the account with (1 XLM = 10,000,000 stroops) ### Payment `bob` feels bad that `charlie` only got 1 XLM, so they will send 4 more XLM to `charlie`. ```sh stellar tx new payment \ --source bob \ --destination charlie \ --asset native \ --amount 40_000_000 ``` Notes: - `--asset`: The asset to send - either "native" for XLM or "CODE:ISSUER" format for other assets ### Bump Sequence Bump an account's sequence number forward: ```sh stellar tx new bump-sequence \ --source alice \ --bump-to 123450 ``` ### Account Merge Merge one account into another, transferring all XLM. `bob` decides to continue spreading the wealth and merges their account into `charlie`'s. ```sh stellar tx new account-merge \ --source bob \ --account charlie ``` Notes: - `--source`: The account to remove from the ledger, thus this is its final tranaction ### Set Trustline Flags Modify authorization flags on a trustline: ```sh stellar tx new set-trustline-flags \ --source alice \ --asset USDC:GBUG7QTBTT47XVDVE6RZYWRUZBPLOIO57INE6LYZDMIXMMDCREQRUQKI \ --trustor charlie \ --set-authorize \ --set-authorize-to-maintain-liabilities \ --set-trustline-clawback-enabled ``` Arguments: - `--source`: The issuing account setting the flags (must be the asset issuer) - `--asset`: The asset in CODE:ISSUER format - `--trustor`: The account whose trustline flags to modify - `--set-authorize`: Enable full authorization - `--set-authorize-to-maintain-liabilities`: Enable limited authorization - `--set-trustline-clawback-enabled`: Enable clawback for this trustline - `--clear-*`: Corresponding clear flags to remove each setting ### Set Options Configure account settings: ```sh stellar tx new set-options \ --source alice \ --inflation-dest GBUG7QTBTT47XVDVE6RZYWRUZBPLOIO57INE6LYZDMIXMMDCREQRUQKI \ --home-domain "example.com" \ --master-weight 100 \ --med-threshold 100 \ --low-threshold 100 \ --high-threshold 100 \ --signer GBXSGN5GX4PZOSBHB4JJF67CEGSGT7DGBGGUGWXI4WOQMQEA4SFV2HTJ \ --signer-weight 1 \ --set-required \ --set-revocable \ --set-clawback-enabled \ --set-immutable ``` Notes: - `--source`: Account to modify settings for - `--inflation-dest`: Set inflation destination account - `--home-domain`: Set home domain for federation/compliance - `--master-weight`: Weight of the account's master key (0-255) - `--low-threshold`: Weight threshold for low security operations - `--med-threshold`: Weight threshold for medium security operations - `--high-threshold`: Weight threshold for high security operations - `--signer`: Add a new signer public key - `--signer-weight`: Weight for the new signer (0 removes the signer) - `--set-required`: Enable requiring authorization for new trustlines - `--set-revocable`: Enable revoking of trustlines - `--set-clawback-enabled`: Enable clawback for asset issuing account - `--set-immutable`: Make account settings immutable - `--clear-*`: Corresponding clear flags to remove each setting ### Change Trust Create or modify a trustline: ```sh stellar tx new change-trust \ --source alice \ --line USDC:ISSUER \ --limit 100000000 ``` Arguments: - `--source`: Account creating/modifying the trustline - `--line`: Asset to create trustline for in CODE:ISSUER format - `--limit`: Maximum amount that can be held (0 removes trustline) ### Manage Data Manage account data entries: ```sh stellar tx new manage-data \ --source alice \ --data-name config \ --data-value 7465737476616c7565 # hex encoded ``` Notes: - `--data-name`: Name of the data entry (up to 64 bytes) - `--data-value`: Hex encoded value to store (up to 64 bytes, omit to delete) --- ## tx op add As seen before you can use pipes to pass a transaction envelope between commands. Before we have only been looking at transactions with one operation, however, as mentioned there can be up to 100 operations in a single transaction. To add an operation to a transaction you can use the `tx op add` command. This command takes the transaction envolope from the previous command and adds an operation to it. Let's consider a more complicated example. Consider issuing an asset, here `USDC` with the requirement that only the issuer can transfer funds to the distrubtor. ```sh stellar keys generate --fund issuer stellar keys generate --fund distributor ISSUER_PK=$(stellar keys address issuer) ASSET="USDC:$ISSUER_PK" # Issue the asset by setting its options, establishing a trustline, and # transferring the smallest amount possible to the distributor. Then # deauthorize the distributor so that people can only send Claimable Balances, # rather than transferring assets directly. # first the issuer sets the options for being able to clawback and revoke the asset stellar tx new set-options --inclusion-fee 1000 --source issuer --set-clawback-enabled --set-revocable --build-only \ # next the distributor establishes a trustline with the asset. Note that here the distributor the source account for the operation, not the issuer | stellar tx op add change-trust --op-source distributor --line $ASSET \ # then the issuer sends the smallest amount possible to the distributor | stellar tx op add payment --destination distributor --asset $ASSET --amount 1 \ # finally the issuer deauthorizes the distributor from being able to send the asset | stellar tx op add set-trustline-flags --asset $ASSET --trustor distributor --clear-authorize \ # Then both accounts need to sign the transaction | stellar tx sign --sign-with-key issuer \ | stellar tx sign --sign-with-key distributor \ | stellar tx send # Next is an example of sandwiching an operation. That is giving permission in one operation, preforming the operation, and then removing the permission in a third operation. # Here is an example of minting new assets to the distributor with a sandwich transaction # First authorize the distributor to receive the asset stellar tx new set-trustline-flags --inclusion-fee 1000 --build-only --source issuer --asset $ASSET --trustor $distributor_PK --set-authorize \ # Then mint the asset to the distributor | stellar tx op add payment --destination distributor --asset $ASSET --amount 1_000_000_000_000 \ # Finally remove the authorization | stellar tx op add set-trustline-flags --asset $ASSET --trustor distributor --clear-authorize \ | stellar tx sign --sign-with-key issuer \ | stellar tx send ``` --- ## tx sign and tx send The previous examples of using `tx new` showed how to create transactions. However, these transactions were immediately ready to be signed and submitted to the network. To avoid this each of the subcommands has the `--build-only` argument, which as the name suggests only builds the transaction and prints the transaction envelope. ## `tx sign` Let's return to the first example of creating `bob`s account: ```sh stellar tx new create-account \ --source alice \ --destination bob \ --starting-balance 100_000_000 \ --build-only ``` would output something like: ```sh AAAAAgAAAADwSUp9CwmVlPN40mKX1I1j39y6DmYc36QS1aK2x6eYVQAAAGQAEcMsAAAAAQAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAACTMkzn1TwPo8SIhnKvnyuv9K2/aWjpX9NTYfyiA7vXaAAAAAAX14QAAAAAAAAAAAA== ``` You can inspect it with [stellar lab!](https://lab.stellar.org/xdr/view?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&xdr$blob=AAAAAgAAAADwSUp9CwmVlPN40mKX1I1j39y6DmYc36QS1aK2x6eYVQAAAGQAEcMsAAAAAQAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAACTMkzn1TwPo8SIhnKvnyuv9K2//aWjpX9NTYfyiA7vXaAAAAAAX14QAAAAAAAAAAAA==;;) Where you can also sign and send the transaction. However, you can also sign the transaction with the `tx sign` command. To do this you can pipe the output of the `tx new` command to the `tx sign` command: ```sh stellar tx new create-account \ --source alice \ --destination bob \ --starting-balance 100_000_000 \ --build-only \ | stellar tx sign --sign-with-key alice ``` This should output something like: ```sh AAAAAgAAAAAebE8Ewg9uzvHRAl+UmvP0kixsyp238kkz0zOy+91FeQAAAGQAFJk4AAAAAQAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAPpiy8qfSw4pLYG/Bav78FfrFWlte7YQfiHX41DQ+nGWAAAAAAX14QAAAAAAAAAAAfvdRXkAAABA4kjz9Yeub/IrzogjMr57U4nYwCmSJAXxIW+7Xyjan/UweIByF7uEhVS4gEl1N138uq07njVxZwRMtugWyMleCg== ``` You can again [view it in lab and see that there is now a signature attached to the transaction envelope](https://lab.stellar.org/xdr/view?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&xdr$blob=AAAAAgAAAAAebE8Ewg9uzvHRAl+UmvP0kixsyp238kkz0zOy+91FeQAAAGQAFJk4AAAAAQAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAPpiy8qfSw4pLYG//Bav78FfrFWlte7YQfiHX41DQ+nGWAAAAAAX14QAAAAAAAAAAAfvdRXkAAABA4kjz9Yeub//IrzogjMr57U4nYwCmSJAXxIW+7Xyjan//UweIByF7uEhVS4gEl1N138uq07njVxZwRMtugWyMleCg==;;). :::tip Or sign with lab! Though currently you must send it from lab and cannot return to the CLI (a work in progress!). ```sh stellar tx new create-account \ --source alice \ --destination bob \ --starting-balance 100_000_000 \ --build-only \ | stellar tx sign --sign-with-lab ``` ::: ## `tx send` Finally, to submit the transaction to the network you can use the `tx send` command. This command will submit the transaction to the network. ```sh stellar tx new create-account \ --source alice \ --destination bob \ --starting-balance 100_000_000 \ --build-only \ | stellar tx sign --sign-with-key alice \ | stellar tx send ``` --- ## Upload and deploy a smart contract You can combine the `upload` and `deploy` commands of the Stellar CLI to accomplish both tasks: ```bash stellar contract deploy \ --source S... \ --network testnet \ --wasm ../relative/path/to/soroban_contract.wasm ``` --- ## Upload Wasm bytecode To use the Stellar CLI to upload a compiled smart contract on the ledger, use the `stellar contract upload` command: ```bash stellar contract upload \ --source S... \ --network testnet \ --wasm ../relative/path/to/soroban_contract.wasm ``` :::note Note this command will return the hash ID of the Wasm bytecode, rather than an address for a contract instance. ::: --- ## Install the CLI # Install the Stellar CLI ### Stellar CLI There are a few ways to install the latest released version of Stellar CLI. Install with script (macOS, Linux, WSL): ```text curl -fsSL https://github.com/stellar/stellar-cli/raw/main/install.sh | sh ``` Install with Homebrew (macOS, Linux, WSL): ```text brew install stellar-cli ``` Install with winget (Windows): ```text winget install --id Stellar.StellarCLI ``` Install with cargo from source ([github.com/stellar/stellar-cli](https://github.com/stellar/stellar-cli)): ```text cargo install --locked stellar-cli ``` :::note Installing from source requires Rust and C build systems. To install Rust, see: - https://www.rust-lang.org/tools/install To install a C build system on Debian/Ubuntu, use: ``` sudo apt update && sudo apt install -y build-essential ``` ::: Install in your GitHub action (this is a preferred option of installing cli in your GitHub actions) :::note You can also use the third-party tool [SVM (Stellar Version Manager)](https://www.npmjs.com/package/svm-cli), a version manager for Stellar CLI that allows you to install and switch between different versions of stellar-cli. ::: ## Set up Autocomplete The Stellar CLI supports some autocompletion. To set up, run the following commands: ```text stellar completion --shell ``` Possible SHELL values are `bash`, `elvish`, `fish`, `powershell`, `zsh`, etc. To enable autocomplete in the current bash shell, run: ```text source <(stellar completion --shell bash) ``` To enable autocomplete permanently, run: ```text echo "source <(stellar completion --shell bash)" >> ~/.bashrc ``` ## Stellar CLI Cookbook To understand how to get the most of the Stellar CLI, see the [Stellar CLI Cookbook](./cookbook/README.mdx) for recipes and a collection of resources to teach you how to use the CLI. Examples of recipes included in the CLI cookbook include: send payments, manage contract lifecycle, extend contract instance/storage/wasm, and more. ## Video Tutorials - Video Tutorial on `network container`, `keys`, and `contract init` from the [2024-06-27 developers meeting](/meetings/2024/06/27) - Video Tutorial on `alias` and `snapshot` from the [2024-09-12 developers meeting](/meetings/2024/09/12) --- ## Plugins List This is a list of all plugins made available by the community, so please review with care before using them. ### [stellar-scaffold/cli](https://github.com/stellar-scaffold/cli) The Stellar app lifecycle streamlined. `stellar scaffold` CLI: init, learn, & build ambitious apps. `stellar registry` CLI: author, publish, deploy & upgrade Wasm binaries + smart contracts (or use someone else's) [https://github.com/stellar-scaffold/cli](https://github.com/stellar-scaffold/cli) ### [lightsail-network/stellar-contract-bindings](https://github.com/lightsail-network/stellar-contract-bindings) CLI tool designed to generate language bindings for Stellar Soroban smart contracts. [https://github.com/lightsail-network/stellar-contract-bindings](https://github.com/lightsail-network/stellar-contract-bindings) ### [OpenZeppelin/stellar-upgrader-cli](https://github.com/OpenZeppelin/stellar-upgrader-cli) CLI that help developers to upgrade stellar contracts [https://github.com/OpenZeppelin/stellar-upgrader-cli](https://github.com/OpenZeppelin/stellar-upgrader-cli) ### [brozorec/smart-account-sign](https://github.com/brozorec/smart-account-sign) CLI tool for interacting with Stellar Smart Accounts [https://github.com/brozorec/smart-account-sign](https://github.com/brozorec/smart-account-sign) ### [fnando/stellar-hello-plugin](https://github.com/fnando/stellar-hello-plugin) This is just a sample plugin for the Stellar CLI [https://github.com/fnando/stellar-hello-plugin](https://github.com/fnando/stellar-hello-plugin) --- ## Plugins Plugins extend the functionality of the Stellar CLI by adding custom commands. The Stellar CLI automatically detects and loads any executable (which doesn't have to be a binary) in your `PATH` that starts with `stellar-`. ## How Plugins Work Plugins are executables (scripts or binaries) that follow a simple naming convention: they must start with `stellar-`. When you run a command like `stellar hello`, the CLI first checks for a built-in command. If none is found, it searches for a plugin named `stellar-hello` in your `PATH` and executes it if found. ## Installing Plugins You can install plugins that are made available publicly. For example, you can install [rs-stellar-strkey](https://github.com/stellar/rs-stellar-strkey) using Rust's cargo: ```sh cargo install --locked stellar-strkey --features cli ``` This installs the `stellar-strkey` binary in your `~/.cargo/bin` directory. Make sure this directory is in your `$PATH` for the CLI to detect it. :::danger[Security Warning] Be careful! Plugins have the same access to your system as the Stellar CLI itself. Only install plugins from sources you trust. ::: ## Listing Available Plugins To list available plugins on your system, use the command `stellar plugin ls`: ```console $ stellar plugin ls Installed Plugins: strkey ``` ## Creating a New Plugin To create a plugin, you need an executable file in your `PATH` that starts with `stellar-`. This example shows how to create a simple plugin using `bash` (works on Unix-based systems or Windows' WSL). ### Step 1: Create the Plugin File Create a new file named `stellar-hello` in a directory that's in your `PATH` (for example, `~/.bin`): ```sh touch ~/.bin/stellar-hello ``` ### Step 2: Add the Plugin Code Add the following content to the file: ```bash #!/usr/bin/env bash echo "hello from stellar plugin" ``` ### Step 3: Make It Executable Make the file executable: ```sh chmod +x ~/.bin/stellar-hello ``` ### Step 4: Verify Installation If everything is set up correctly (the directory is in your `PATH` and the file is executable), you should see your plugin listed: ```console $ stellar plugin ls Installed Plugins: hello strkey ``` ### Step 5: Use Your Plugin You can now execute your plugin by calling `stellar hello`: ```console $ stellar hello hello from stellar plugin ``` ## Plugin Naming and Subcommands The Stellar CLI automatically searches for plugins when no built-in command matches. This works for both simple commands and subcommands. For example, if you create a plugin named `stellar-contract-bindings-ruby`, users can execute it using either: - `stellar contract-bindings-ruby` (with dashes) - `stellar contract bindings ruby` (with spaces as subcommands) The CLI converts spaces to dashes when searching for the plugin executable, so both formats work. :::tip If you're using GitHub to host your plugin's repository, consider adding a `stellar-cli-plugin` [repository topic](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/classifying-your-repository-with-topics). This way, your plugin will be listed by `stellar plugin search`. ::: ## Troubleshooting If the Stellar CLI can't find your plugin, check the following: - **Naming**: Ensure the file starts with `stellar-` - **Permissions**: Verify it's executable using `chmod +x` - **PATH**: Check that the plugin's directory is in your `PATH` environment variable - **Shell**: Restart your terminal or reload your shell configuration (e.g., run `source ~/.zshrc` or `source ~/.bashrc`) You can verify your plugin is in your PATH by running: ```sh which stellar-hello ``` If this returns a path, your plugin is accessible. If it returns nothing, the plugin's directory is not in your PATH. --- ## Stellar CLI Manual This document contains the help content for the `stellar` command-line program. ## `stellar` Work seamlessly with Stellar accounts, contracts, and assets from the command line. - Generate and manage keys and accounts - Build, deploy, and interact with contracts - Deploy asset contracts - Stream events - Start local testnets - Decode, encode XDR - More! For additional information see: - Stellar Docs: https://developers.stellar.org - Smart Contract Docs: https://developers.stellar.org/docs/build/smart-contracts/overview - CLI Docs: https://developers.stellar.org/docs/tools/developer-tools/cli/stellar-cli To get started generate a new identity: stellar keys generate alice Use keys with the `--source` flag in other commands. Commands that work with contracts are organized under the `contract` subcommand. List them: stellar contract --help Use contracts like a CLI: stellar contract invoke --id CCR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OTE2 --source alice --network testnet -- --help Anything after the `--` double dash (the "slop") is parsed as arguments to the contract-specific CLI, generated on-the-fly from the contract schema. For the hello world example, with a function called `hello` that takes one string argument `to`, here's how you invoke it: stellar contract invoke --id CCR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OTE2 --source alice --network testnet -- hello --to world **Usage:** `stellar [OPTIONS] ` ###### **Subcommands:** - `contract` — Tools for smart contract developers - `doctor` — Diagnose and troubleshoot CLI and network issues - `events` — Watch the network for contract events - `env` — Prints the environment variables - `keys` — Create and manage identities including keys and addresses - `network` — Configure connection to networks - `container` — Start local networks in containers - `config` — Manage CLI configuration - `snapshot` — Download a snapshot of a ledger from an archive - `token` — Interact with SEP-41 tokens and Stellar Asset Contracts - `tx` — Sign, Simulate, and Send transactions - `xdr` — Decode and encode XDR - `strkey` — Decode and encode strkey - `completion` — Print shell completion code for the specified shell - `cache` — Cache for transactions and contract specs - `version` — Print version information - `plugin` — The subcommand for CLI plugins - `ledger` — Fetch ledger information - `message` — Sign and verify arbitrary messages using SEP-53 - `fee-stats` — ⚠️ Deprecated, use `fees stats` instead. Fetch network feestats - `fees` — Fetch network feestats and configure CLI fee settings ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings - `-f`, `--filter-logs ` — Filter logs output. To turn on `stellar_cli::log::footprint=debug` or off `=off`. Can also use env var `RUST_LOG` - `-q`, `--quiet` — Do not write logs to stderr including `INFO` - `-v`, `--verbose` — Log DEBUG events - `--very-verbose` [alias: `vv`] — Log DEBUG and TRACE events - `--no-cache` — Do not cache your simulations and transactions ## `stellar contract` Tools for smart contract developers **Usage:** `stellar contract ` ###### **Subcommands:** - `asset` — Utilities to deploy a Stellar Asset Contract or get its id - `alias` — Utilities to manage contract aliases - `bindings` — Generate code client bindings for a contract - `build` — Build a contract from source - `extend` — Extend the time to live ledger of a contract-data ledger entry - `deploy` — Deploy a wasm contract - `fetch` — Fetch a contract's Wasm binary - `id` — Generate the contract id for a given contract or asset - `info` — Access info about contracts - `init` — Initialize a Soroban contract project - `inspect` — ⚠️ Deprecated, use `contract info`. Inspect a WASM file listing contract functions, meta, etc - `upload` — Install a WASM file to the ledger without creating a contract instance - `install` — ⚠️ Deprecated, use `contract upload`. Install a WASM file to the ledger without creating a contract instance - `invoke` — Invoke a contract function - `optimize` — ⚠️ Deprecated, use `build --optimize`. Optimize a WASM file - `read` — Print the current value of a contract-data ledger entry - `restore` — Restore an evicted value for a contract-data legder entry ## `stellar contract asset` Utilities to deploy a Stellar Asset Contract or get its id **Usage:** `stellar contract asset ` ###### **Subcommands:** - `id` — Get Id of builtin Soroban Asset Contract. Deprecated, use `stellar contract id asset` instead - `deploy` — Deploy builtin Soroban Asset Contract ## `stellar contract asset id` Get Id of builtin Soroban Asset Contract. Deprecated, use `stellar contract id asset` instead **Usage:** `stellar contract asset id [OPTIONS] --asset ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--asset ` — ID of the Stellar classic asset to wrap, e.g. "native", "USDC:G...5", "USDC:alias" ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract asset deploy` Deploy builtin Soroban Asset Contract **Usage:** `stellar contract asset deploy [OPTIONS] --asset --source-account ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--asset ` — ID of the Stellar classic asset to wrap, e.g. "USDC:G...5" - `--alias ` — The alias that will be used to save the assets's id. Whenever used, `--alias` will always overwrite the existing contract id configuration without asking for confirmation ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config - `--resource-fee ` — Set the fee for smart contract resource consumption, in stroops. 1 stroop = 0.0000001 xlm. Overrides the simulated resource fee - `--instructions ` — ⚠️ Deprecated, use `--instruction-leeway` to increase instructions. Number of instructions to allocate for the transaction - `--instruction-leeway ` — Allow this many extra instructions when budgeting resources with transaction simulation - `--cost` — Output the cost execution to stderr ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar contract alias` Utilities to manage contract aliases **Usage:** `stellar contract alias ` ###### **Subcommands:** - `remove` — Remove contract alias - `add` — Add contract alias - `show` — Show the contract id associated with a given alias - `ls` — List all aliases ## `stellar contract alias remove` Remove contract alias **Usage:** `stellar contract alias remove [OPTIONS] ` ###### **Arguments:** - `` — The contract alias that will be removed ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract alias add` Add contract alias **Usage:** `stellar contract alias add [OPTIONS] --id ` ###### **Arguments:** - `` — The contract alias that will be used ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--overwrite` — Overwrite the contract alias if it already exists - `--id ` — The contract id that will be associated with the alias ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract alias show` Show the contract id associated with a given alias **Usage:** `stellar contract alias show [OPTIONS] ` ###### **Arguments:** - `` — The contract alias that will be displayed ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract alias ls` List all aliases **Usage:** `stellar contract alias ls [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ## `stellar contract bindings` Generate code client bindings for a contract **Usage:** `stellar contract bindings ` ###### **Subcommands:** - `rust` — Generate Rust bindings - `typescript` — Generate a TypeScript / JavaScript package - `python` — Generate Python bindings - `java` — Generate Java bindings - `flutter` — Generate Flutter bindings - `swift` — Generate Swift bindings - `php` — Generate PHP bindings ## `stellar contract bindings rust` Generate Rust bindings **Usage:** `stellar contract bindings rust --wasm ` ###### **Options:** - `--wasm ` — Path to wasm binary ## `stellar contract bindings typescript` Generate a TypeScript / JavaScript package **Usage:** `stellar contract bindings typescript [OPTIONS] --output-dir <--wasm |--wasm-hash |--contract-id >` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--wasm ` — Wasm file path on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id` - `--wasm-hash ` — Hash of Wasm blob on a network. Provide this OR `--wasm` OR `--contract-id` - `--contract-id ` [alias: `id`] — Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm` - `--output-dir ` — Where to place generated project - `--overwrite` — Whether to overwrite output directory if it already exists ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract bindings python` Generate Python bindings **Usage:** `stellar contract bindings python` ## `stellar contract bindings java` Generate Java bindings **Usage:** `stellar contract bindings java` ## `stellar contract bindings flutter` Generate Flutter bindings **Usage:** `stellar contract bindings flutter` ## `stellar contract bindings swift` Generate Swift bindings **Usage:** `stellar contract bindings swift` ## `stellar contract bindings php` Generate PHP bindings **Usage:** `stellar contract bindings php` ## `stellar contract build` Build a contract from source Builds all crates that are referenced by the cargo manifest (Cargo.toml) that have cdylib as their crate-type. Crates are built for the wasm32 target. Unless configured otherwise, crates are built with their default features and with their release profile. In workspaces builds all crates unless a package name is specified, or the command is executed from the sub-directory of a workspace crate. To view the commands that will be executed, without executing them, use the --print-commands-only option. **Usage:** `stellar contract build [OPTIONS]` ###### **Features:** - `--features ` — Build with the list of features activated, space or comma separated - `--all-features` — Build with the all features activated - `--no-default-features` — Build with the default feature not activated ###### **Metadata:** - `--meta ` — Add key-value to contract meta (adds the meta to the `contractmetav0` custom section) ###### **Options:** - `--manifest-path ` — Path to Cargo.toml - `--package ` — Package to build If omitted, all packages that build for crate-type cdylib are built. - `--profile ` — Build with the specified profile Default value: `release` - `--out-dir ` — Directory to copy wasm files to If provided, wasm files can be found in the cargo target directory, and the specified directory. If ommitted, wasm files are written only to the cargo target directory. - `--locked` — Assert that `Cargo.lock` will remain unchanged - `--optimize ` — Optimize the generated wasm. Enabled by default; pass `--optimize=false` to disable. Requires the `additional-libs` feature Default value: `true` Possible values: `true`, `false` ###### **Other:** - `--print-commands-only` — Print commands to build without executing them ## `stellar contract extend` Extend the time to live ledger of a contract-data ledger entry. If no keys are specified the contract itself is extended. **Usage:** `stellar contract extend [OPTIONS] --ledgers-to-extend --source-account ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--ledgers-to-extend ` — Number of ledgers to extend the entries - `--ttl-ledger-only` — Only print the new Time To Live ledger - `--id ` — Contract ID to which owns the data entries. If no keys provided the Contract's instance will be extended - `--key ` — Storage key (symbols only) - `--key-xdr ` — Storage key (base64-encoded XDR) - `--wasm ` — Path to Wasm file of contract code to extend - `--wasm-hash ` — Path to Wasm file of contract code to extend - `--durability ` — Storage entry durability Default value: `persistent` Possible values: - `persistent`: Persistent - `temporary`: Temporary ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config - `--resource-fee ` — Set the fee for smart contract resource consumption, in stroops. 1 stroop = 0.0000001 xlm. Overrides the simulated resource fee - `--instructions ` — ⚠️ Deprecated, use `--instruction-leeway` to increase instructions. Number of instructions to allocate for the transaction - `--instruction-leeway ` — Allow this many extra instructions when budgeting resources with transaction simulation - `--cost` — Output the cost execution to stderr ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar contract deploy` Deploy a wasm contract **Usage:** `stellar contract deploy [OPTIONS] --source-account [-- ...]` ###### **Arguments:** - `` — If provided, will be passed to the contract's `__constructor` function with provided arguments for that function as `--arg-name value` ###### **Build Options:** - `--package ` — Package to build when auto-building without --wasm ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Metadata:** - `--meta ` — Add key-value to contract meta (adds the meta to the `contractmetav0` custom section) ###### **Options:** - `--wasm ` — WASM file to deploy. When neither --wasm nor --wasm-hash is provided inside a Cargo workspace, builds the project automatically. One of --wasm or --wasm-hash is required when outside a Cargo workspace - `--wasm-hash ` — Hash of the already installed/deployed WASM file - `--salt ` — Custom salt 32-byte salt for the token id - `-i`, `--ignore-checks` — Whether to ignore safety checks when deploying contracts Default value: `false` - `--alias ` — The alias that will be used to save the contract's id. Whenever used, `--alias` will always overwrite the existing contract id configuration without asking for confirmation - `--optimize ` — Optimize the generated wasm. Enabled by default; pass `--optimize=false` to disable. Requires the `additional-libs` feature Default value: `true` Possible values: `true`, `false` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config - `--resource-fee ` — Set the fee for smart contract resource consumption, in stroops. 1 stroop = 0.0000001 xlm. Overrides the simulated resource fee - `--instructions ` — ⚠️ Deprecated, use `--instruction-leeway` to increase instructions. Number of instructions to allocate for the transaction - `--instruction-leeway ` — Allow this many extra instructions when budgeting resources with transaction simulation - `--cost` — Output the cost execution to stderr - `--auth-mode ` — Set the authorization mode for transaction simulation. When unset, the RPC default is used: record with the root mode if no authorization entries exist, otherwise enforce the provided entries. Should only be set for `InvokeHostFunction` transactions. The `enforce` mode is for simulating transactions that already contain authorization entries Possible values: - `enforce`: Validate the authorization entries already on the transaction - `root`: Record authorization entries, requiring each to be rooted at the transaction's top-level operation - `non-root`: Record all authorization entries, including non-root entries ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar contract fetch` Fetch a contract's Wasm binary **Usage:** `stellar contract fetch [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--id ` — Contract ID to fetch - `--wasm-hash ` — Wasm to fetch - `-o`, `--out-file ` — Where to write output otherwise stdout is used ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract id` Generate the contract id for a given contract or asset **Usage:** `stellar contract id ` ###### **Subcommands:** - `asset` — Derive the contract id for a builtin Stellar Asset Contract - `wasm` — Derive the contract id for a Wasm contract ## `stellar contract id asset` Derive the contract id for a builtin Stellar Asset Contract **Usage:** `stellar contract id asset [OPTIONS] --asset ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--asset ` — ID of the Stellar classic asset to wrap, e.g. "native", "USDC:G...5", "USDC:alias" ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract id wasm` Derive the contract id for a Wasm contract **Usage:** `stellar contract id wasm [OPTIONS] --salt --source-account ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--salt ` — ID of the Soroban contract ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided ## `stellar contract info` Access info about contracts **Usage:** `stellar contract info ` ###### **Subcommands:** - `interface` — Output the interface of a contract - `meta` — Output the metadata stored in a contract - `env-meta` — Output the env required metadata stored in a contract - `build` — Output the contract build information, if available - `hash` — Output the SHA-256 hash of a contract's Wasm ## `stellar contract info interface` Output the interface of a contract. A contract's interface describes the functions, parameters, and types that the contract makes accessible to be called. The data outputted by this command is a stream of `SCSpecEntry` XDR values. See the type definitions in [stellar-xdr](https://github.com/stellar/stellar-xdr). [See also XDR data format](https://developers.stellar.org/docs/learn/encyclopedia/data-format/xdr). Outputs no data when no data is present in the contract. **Usage:** `stellar contract info interface [OPTIONS] <--wasm |--wasm-hash |--contract-id >` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--wasm ` — Wasm file path on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id` - `--wasm-hash ` — Hash of Wasm blob on a network. Provide this OR `--wasm` OR `--contract-id` - `--contract-id ` [alias: `id`] — Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm` - `--output ` — Format of the output Default value: `rust` Possible values: - `rust`: Rust code output of the contract interface - `xdr-base64`: XDR output of the info entry - `json`: JSON output of the info entry (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the info entry ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract info meta` Output the metadata stored in a contract. A contract's meta is a series of key-value pairs that the contract developer can set with any values to provided metadata about the contract. The meta also contains some information like the version of Rust SDK, and Rust compiler version. The data outputted by this command is a stream of `SCMetaEntry` XDR values. See the type definitions in [stellar-xdr](https://github.com/stellar/stellar-xdr). [See also XDR data format](https://developers.stellar.org/docs/learn/encyclopedia/data-format/xdr). Outputs no data when no data is present in the contract. **Usage:** `stellar contract info meta [OPTIONS] <--wasm |--wasm-hash |--contract-id >` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--wasm ` — Wasm file path on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id` - `--wasm-hash ` — Hash of Wasm blob on a network. Provide this OR `--wasm` OR `--contract-id` - `--contract-id ` [alias: `id`] — Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm` - `--output ` — Format of the output Default value: `text` Possible values: - `text`: Text output of the meta info entry - `xdr-base64`: XDR output of the info entry - `json`: JSON output of the info entry (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the info entry ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract info env-meta` Output the env required metadata stored in a contract. Env-meta is information stored in all contracts, in the `contractenvmetav0` WASM custom section, about the environment that the contract was built for. Env-meta allows the Soroban Env to know whether the contract is compatibility with the network in its current configuration. The data outputted by this command is a stream of `SCEnvMetaEntry` XDR values. See the type definitions in [stellar-xdr](https://github.com/stellar/stellar-xdr). [See also XDR data format](https://developers.stellar.org/docs/learn/encyclopedia/data-format/xdr). Outputs no data when no data is present in the contract. **Usage:** `stellar contract info env-meta [OPTIONS] <--wasm |--wasm-hash |--contract-id >` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--wasm ` — Wasm file path on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id` - `--wasm-hash ` — Hash of Wasm blob on a network. Provide this OR `--wasm` OR `--contract-id` - `--contract-id ` [alias: `id`] — Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm` - `--output ` — Format of the output Default value: `text` Possible values: - `text`: Text output of the meta info entry - `xdr-base64`: XDR output of the info entry - `json`: JSON output of the info entry (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the info entry ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract info build` Output the contract build information, if available. If the contract has a meta entry like `source_repo=github:user/repo`, this command will try to fetch the attestation information for the WASM file. **Usage:** `stellar contract info build [OPTIONS] <--wasm |--wasm-hash |--contract-id >` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--wasm ` — Wasm file path on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id` - `--wasm-hash ` — Hash of Wasm blob on a network. Provide this OR `--wasm` OR `--contract-id` - `--contract-id ` [alias: `id`] — Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract info hash` Output the SHA-256 hash of a contract's Wasm. The hash can be computed from a local .wasm file (`--wasm`) or read from a deployed contract (`--id`). The two flags are mutually exclusive. Stellar Asset Contracts have no Wasm and therefore no hash; using `--id` against a SAC will return an error. **Usage:** `stellar contract info hash [OPTIONS] <--wasm |--contract-id >` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--wasm ` — Path to a local .wasm file - `--contract-id ` [alias: `id`] — Contract ID or alias of a deployed contract ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract init` Initialize a Soroban contract project. This command will create a Cargo workspace project and add a sample Stellar contract. The name of the contract can be specified by `--name`. It can be run multiple times with different names in order to generate multiple contracts, and files won't be overwritten unless `--overwrite` is passed. **Usage:** `stellar contract init [OPTIONS] ` ###### **Arguments:** - `` ###### **Options:** - `--name ` — An optional flag to specify a new contract's name. Default value: `hello-world` - `--overwrite` — Overwrite all existing files. ## `stellar contract inspect` ⚠️ Deprecated, use `contract info`. Inspect a WASM file listing contract functions, meta, etc **Usage:** `stellar contract inspect [OPTIONS] --wasm ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--wasm ` — Path to wasm binary - `--output ` — Output just XDR in base64 Default value: `docs` Possible values: - `xdr-base64`: XDR of array of contract spec entries - `xdr-base64-array`: Array of xdr of contract spec entries - `docs`: Pretty print of contract spec entries ## `stellar contract upload` Install a WASM file to the ledger without creating a contract instance **Usage:** `stellar contract upload [OPTIONS] --source-account ` ###### **Build Options:** - `--package ` — Package to build when --wasm is not provided ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Metadata:** - `--meta ` — Add key-value to contract meta (adds the meta to the `contractmetav0` custom section) ###### **Options:** - `--wasm ` — Path to wasm binary. When omitted inside a Cargo workspace, builds the project automatically. Required when outside a Cargo workspace - `-i`, `--ignore-checks` — Whether to ignore safety checks when deploying contracts Default value: `false` - `--optimize ` — Optimize the generated wasm. Enabled by default; pass `--optimize=false` to disable. Requires the `additional-libs` feature Default value: `true` Possible values: `true`, `false` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config - `--resource-fee ` — Set the fee for smart contract resource consumption, in stroops. 1 stroop = 0.0000001 xlm. Overrides the simulated resource fee - `--instructions ` — ⚠️ Deprecated, use `--instruction-leeway` to increase instructions. Number of instructions to allocate for the transaction - `--instruction-leeway ` — Allow this many extra instructions when budgeting resources with transaction simulation - `--cost` — Output the cost execution to stderr - `--auth-mode ` — Set the authorization mode for transaction simulation. When unset, the RPC default is used: record with the root mode if no authorization entries exist, otherwise enforce the provided entries. Should only be set for `InvokeHostFunction` transactions. The `enforce` mode is for simulating transactions that already contain authorization entries Possible values: - `enforce`: Validate the authorization entries already on the transaction - `root`: Record authorization entries, requiring each to be rooted at the transaction's top-level operation - `non-root`: Record all authorization entries, including non-root entries ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar contract install` ⚠️ Deprecated, use `contract upload`. Install a WASM file to the ledger without creating a contract instance **Usage:** `stellar contract install [OPTIONS] --source-account ` ###### **Build Options:** - `--package ` — Package to build when --wasm is not provided ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Metadata:** - `--meta ` — Add key-value to contract meta (adds the meta to the `contractmetav0` custom section) ###### **Options:** - `--wasm ` — Path to wasm binary. When omitted inside a Cargo workspace, builds the project automatically. Required when outside a Cargo workspace - `-i`, `--ignore-checks` — Whether to ignore safety checks when deploying contracts Default value: `false` - `--optimize ` — Optimize the generated wasm. Enabled by default; pass `--optimize=false` to disable. Requires the `additional-libs` feature Default value: `true` Possible values: `true`, `false` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config - `--resource-fee ` — Set the fee for smart contract resource consumption, in stroops. 1 stroop = 0.0000001 xlm. Overrides the simulated resource fee - `--instructions ` — ⚠️ Deprecated, use `--instruction-leeway` to increase instructions. Number of instructions to allocate for the transaction - `--instruction-leeway ` — Allow this many extra instructions when budgeting resources with transaction simulation - `--cost` — Output the cost execution to stderr - `--auth-mode ` — Set the authorization mode for transaction simulation. When unset, the RPC default is used: record with the root mode if no authorization entries exist, otherwise enforce the provided entries. Should only be set for `InvokeHostFunction` transactions. The `enforce` mode is for simulating transactions that already contain authorization entries Possible values: - `enforce`: Validate the authorization entries already on the transaction - `root`: Record authorization entries, requiring each to be rooted at the transaction's top-level operation - `non-root`: Record all authorization entries, including non-root entries ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar contract invoke` Invoke a contract function Generates an "implicit CLI" for the specified contract on-the-fly using the contract's schema, which gets embedded into every Soroban contract. The "slop" in this command, everything after the `--`, gets passed to this implicit CLI. Get in-depth help for a given contract: stellar contract invoke ... -- --help **Usage:** `stellar contract invoke [OPTIONS] --id --source-account [-- ...]` ###### **Arguments:** - `` — Function name as subcommand, then arguments for that function as `--arg-name value` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--id ` — Contract ID to invoke - `--is-view` — ⚠️ Deprecated, use `--send=no`. View the result simulating and do not sign and submit transaction - `--send ` — Whether or not to send a transaction Default value: `default` Possible values: - `default`: Send transaction if simulation indicates there are ledger writes, published events, or auth required, otherwise return simulation result - `no`: Do not send transaction, return simulation result - `yes`: Always send transaction ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config - `--resource-fee ` — Set the fee for smart contract resource consumption, in stroops. 1 stroop = 0.0000001 xlm. Overrides the simulated resource fee - `--instructions ` — ⚠️ Deprecated, use `--instruction-leeway` to increase instructions. Number of instructions to allocate for the transaction - `--instruction-leeway ` — Allow this many extra instructions when budgeting resources with transaction simulation - `--cost` — Output the cost execution to stderr - `--auth-mode ` — Set the authorization mode for transaction simulation. When unset, the RPC default is used: record with the root mode if no authorization entries exist, otherwise enforce the provided entries. Should only be set for `InvokeHostFunction` transactions. The `enforce` mode is for simulating transactions that already contain authorization entries Possible values: - `enforce`: Validate the authorization entries already on the transaction - `root`: Record authorization entries, requiring each to be rooted at the transaction's top-level operation - `non-root`: Record all authorization entries, including non-root entries ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar contract optimize` ⚠️ Deprecated, use `build --optimize`. Optimize a WASM file **Usage:** `stellar contract optimize [OPTIONS] --wasm ...` ###### **Options:** - `--wasm ` — Path to one or more wasm binaries - `--wasm-out ` — Path to write the optimized WASM file to (defaults to same location as --wasm with .optimized.wasm suffix) ## `stellar contract read` Print the current value of a contract-data ledger entry **Usage:** `stellar contract read [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--output ` — Type of output to generate Default value: `string` Possible values: - `string`: String - `json`: Json - `xdr`: XDR - `--id ` — Contract ID to which owns the data entries. If no keys provided the Contract's instance will be extended - `--key ` — Storage key (symbols only) - `--key-xdr ` — Storage key (base64-encoded XDR) - `--wasm ` — Path to Wasm file of contract code to extend - `--wasm-hash ` — Path to Wasm file of contract code to extend - `--durability ` — Storage entry durability Default value: `persistent` Possible values: - `persistent`: Persistent - `temporary`: Temporary ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar contract restore` Restore an evicted value for a contract-data legder entry. If no keys are specificed the contract itself is restored. **Usage:** `stellar contract restore [OPTIONS] --source-account ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--id ` — Contract ID to which owns the data entries. If no keys provided the Contract's instance will be extended - `--key ` — Storage key (symbols only) - `--key-xdr ` — Storage key (base64-encoded XDR) - `--wasm ` — Path to Wasm file of contract code to extend - `--wasm-hash ` — Path to Wasm file of contract code to extend - `--durability ` — Storage entry durability Default value: `persistent` Possible values: - `persistent`: Persistent - `temporary`: Temporary - `--ledgers-to-extend ` — Number of ledgers to extend the entry - `--ttl-ledger-only` — Only print the new Time To Live ledger ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config - `--resource-fee ` — Set the fee for smart contract resource consumption, in stroops. 1 stroop = 0.0000001 xlm. Overrides the simulated resource fee - `--instructions ` — ⚠️ Deprecated, use `--instruction-leeway` to increase instructions. Number of instructions to allocate for the transaction - `--instruction-leeway ` — Allow this many extra instructions when budgeting resources with transaction simulation - `--cost` — Output the cost execution to stderr ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar doctor` Diagnose and troubleshoot CLI and network issues **Usage:** `stellar doctor [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ## `stellar events` Watch the network for contract events **Usage:** `stellar events [OPTIONS]` ###### **FILTERS:** - `--id ` — A set of (up to 5) contract IDs to filter events on. This parameter can be passed multiple times, e.g. `--id C123.. --id C456..`, or passed with multiple parameters, e.g. `--id C123 C456`. Though the specification supports multiple filter objects (i.e. combinations of type, IDs, and topics), only one set can be specified on the command-line today, though that set can have multiple IDs/topics. - `--topic ` — A set of (up to 5) topic filters to filter event topics on. A single topic filter can contain 1-4 different segments, separated by commas. An asterisk (`*` character) indicates a wildcard segment. In addition to up to 4 possible topic filter segments, the "**" wildcard can also be added, and will allow for a flexible number of topics in the returned events. The "**" wildcard must be the last segment in a query. If the "\*\*" wildcard is not included, only events with the exact number of topics as the given filter will be returned. **Example:** topic filter with two segments: `--topic "AAAABQAAAAdDT1VOVEVSAA==,*"` **Example:** two topic filters with one and two segments each: `--topic "AAAABQAAAAdDT1VOVEVSAA==" --topic '*,*'` **Example:** topic filter with four segments and the "**" wildcard: --topic "AAAABQAAAAdDT1VOVEVSAA==,_,_,\*,**" Note that all of these topic filters are combined with the contract IDs into a single filter (i.e. combination of type, IDs, and topics). - `--type ` — Specifies which type of contract events to display Default value: `all` Possible values: `all`, `contract`, `system` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--start-ledger ` — The first ledger sequence number in the range to pull events https://developers.stellar.org/docs/learn/encyclopedia/network-configuration/ledger-headers#ledger-sequence - `--cursor ` — The cursor corresponding to the start of the event range - `--output ` — Output formatting options for event stream Default value: `pretty` Possible values: - `pretty`: Human-readable output with decoded event names and parameters - `plain`: Human-readable output without colors - `json`: JSON output with decoded event names and parameters - `raw`: Raw event output without self-describing decoding - `-c`, `--count ` — The maximum number of events to display (defer to the server-defined limit) Default value: `10` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar env` Prints the environment variables Prints to stdout in a format that can be used as .env file. Environment variables have precedence over defaults. By default, secret values are concealed. To display them, use `--reveal`. Pass a name to get the value of a single environment variable. Its value is printed without shell quoting (control characters are neutralized), suitable for command substitution. Concealed variables print nothing unless `--reveal` is passed. If there are no environment variables in use, prints the defaults. **Usage:** `stellar env [OPTIONS] [NAME]` ###### **Arguments:** - `` — Env variable name to get the value of. E.g.: $ stellar env STELLAR_ACCOUNT ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--reveal` — Whether to reveal the value of concealed env vars. By default, concealed env vars are hidden behind a placeholder value ## `stellar keys` Create and manage identities including keys and addresses **Usage:** `stellar keys ` ###### **Subcommands:** - `add` — Add a new identity (keypair, ledger, OS specific secure store) - `public-key` — Given an identity return its address (public key) - `fund` — Fund an identity on a test network - `generate` — Generate a new identity using a 24-word seed phrase The seed phrase can be stored in a config file (default) or in an OS-specific secure store - `ls` — List identities - `rm` — Remove an identity - `secret` — Output an identity's secret key - `use` — Set the default identity that will be used on all commands. This allows you to skip `--source-account` or setting a environment variable, while reusing this value in all commands that require it - `unset` — Unset the default key identity defined previously with `keys use ` ## `stellar keys add` Add a new identity (keypair, ledger, OS specific secure store) **Usage:** `stellar keys add [OPTIONS] ` ###### **Arguments:** - `` — Name of identity ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--secret-key` — ⚠️ Deprecated, use `--secure-store`. Enter secret (S) key when prompted - `--seed-phrase` — ⚠️ Deprecated, use `--secure-store`. Enter key using 12-24 word seed phrase - `--secure-store` — Save the new key in your OS's credential secure store. On Mac this uses Keychain, on Windows it is Secure Store Service, and on \*nix platforms it uses a combination of the kernel keyutils and DBus-based Secret Service. This only supports seed phrases for now. - `--public-key ` — Add a public key, ed25519, or muxed account, e.g. G1.., M2.. - `--ledger` — Derive the address from a connected Ledger hardware wallet at `m/44'/148'/N'`, where `N` defaults to 0 and can be set with `--hd-path`. Persists the derived public key (and `--hd-path`, when provided) so later commands work without the device - `--overwrite` — Overwrite existing identity if it already exists. When combined with --secure-store, also replaces the existing Secure Store entry - `--hd-path ` — When importing a seed phrase, which `hd_path` to derive the key at. Persisted on the identity so later commands derive the same account without re-passing the flag. Not valid with `--public-key` or a raw secret key ## `stellar keys public-key` Given an identity return its address (public key) **Usage:** `stellar keys public-key [OPTIONS] [NAME]` **Command Alias:** `address` ###### **Arguments:** - `` — Name of identity to lookup. Required unless `--ledger` is provided ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--hd-path ` — If identity is a seed phrase use this hd path, default is 0. With --ledger this is the Ledger account index (default 0) - `--ledger` — Derive the address from a connected Ledger hardware wallet at `m/44'/148'/N'`, where `N` defaults to 0 and can be set with `--hd-path` ## `stellar keys fund` Fund an identity on a test network **Usage:** `stellar keys fund [OPTIONS] [NAME]` ###### **Arguments:** - `` — Name of identity to lookup. Required unless `--ledger` is provided ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--hd-path ` — If identity is a seed phrase use this hd path, default is 0. With --ledger this is the Ledger account index (default 0) - `--ledger` — Derive the address from a connected Ledger hardware wallet at `m/44'/148'/N'`, where `N` defaults to 0 and can be set with `--hd-path` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar keys generate` Generate a new identity using a 24-word seed phrase The seed phrase can be stored in a config file (default) or in an OS-specific secure store **Usage:** `stellar keys generate [OPTIONS] ` ###### **Arguments:** - `` — Name of identity ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--seed ` — Optional seed to use when generating seed phrase. Random otherwise - `-s`, `--as-secret` — Output the generated identity as a secret key - `--secure-store` — Save the new key in your OS's credential secure store. On Mac this uses Keychain, on Windows it is Secure Store Service, and on \*nix platforms it uses a combination of the kernel keyutils and DBus-based Secret Service. - `--hd-path ` — Which `hd_path` to derive the key at from the seed phrase. Honored across all storage modes: with `--as-secret` it picks which derived key is stored, with `--secure-store` or plain seed-phrase storage it is persisted on the identity so later commands derive the same account without re-passing the flag - `--fund` — Fund generated key pair Default value: `false` - `--overwrite` — Overwrite existing identity if it already exists. When combined with --secure-store, also replaces the existing Secure Store entry ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar keys ls` List identities **Usage:** `stellar keys ls [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `-l`, `--long` ## `stellar keys rm` Remove an identity **Usage:** `stellar keys rm [OPTIONS] ` ###### **Arguments:** - `` — Identity to remove ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--force` — Skip confirmation prompt ## `stellar keys secret` Output an identity's secret key **Usage:** `stellar keys secret [OPTIONS] ` ###### **Arguments:** - `` — Name of identity to lookup, default is test identity ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--phrase` — Output seed phrase instead of private key - `--hd-path ` — If identity is a seed phrase use this hd path, default is 0 ## `stellar keys use` Set the default identity that will be used on all commands. This allows you to skip `--source-account` or setting a environment variable, while reusing this value in all commands that require it **Usage:** `stellar keys use [OPTIONS] ` ###### **Arguments:** - `` — Set the default network name ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ## `stellar keys unset` Unset the default key identity defined previously with `keys use ` **Usage:** `stellar keys unset [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ## `stellar network` Configure connection to networks **Usage:** `stellar network ` ###### **Subcommands:** - `add` — Add a new network - `rm` — Remove a network - `ls` — List networks - `use` — Set the default network that will be used on all commands. This allows you to skip `--network` or setting a environment variable, while reusing this value in all commands that require it - `health` — Fetch the health of the configured RPC - `info` — Checks the health of the configured RPC - `settings` — Fetch the network's config settings - `unset` — Unset the default network defined previously with `network use ` - `root-account` — Compute the root account keypair for a network ## `stellar network add` Add a new network **Usage:** `stellar network add [OPTIONS] --rpc-url --network-passphrase ` ###### **Arguments:** - `` — Name of network ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — Optional header to include in requests to the RPC, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server ## `stellar network rm` Remove a network **Usage:** `stellar network rm [OPTIONS] ` ###### **Arguments:** - `` — Network to remove ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ## `stellar network ls` List networks **Usage:** `stellar network ls [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `-l`, `--long` — Get more info about the networks ## `stellar network use` Set the default network that will be used on all commands. This allows you to skip `--network` or setting a environment variable, while reusing this value in all commands that require it **Usage:** `stellar network use [OPTIONS] ` ###### **Arguments:** - `` — Set the default network name ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ## `stellar network health` Fetch the health of the configured RPC **Usage:** `stellar network health [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--output ` — Format of the output Default value: `text` Possible values: - `text`: Text output of network health status - `json`: JSON result of the RPC request - `json-formatted`: Formatted (multiline) JSON output of the RPC request ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar network info` Checks the health of the configured RPC **Usage:** `stellar network info [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--output ` — Format of the output Default value: `text` Possible values: - `text`: Text output of network info - `json`: JSON result of the RPC request - `json-formatted`: Formatted (multiline) JSON output of the RPC request ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar network settings` Fetch the network's config settings **Usage:** `stellar network settings [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--internal` — Include internal config settings that are not upgradeable and are internally maintained by the network - `--output ` — Format of the output Default value: `json` Possible values: - `xdr`: XDR (`ConfigUpgradeSet` type) - `json`: JSON, XDR-JSON of the `ConfigUpgradeSet` XDR type - `json-formatted`: JSON formatted, XDR-JSON of the `ConfigUpgradeSet` XDR type ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar network unset` Unset the default network defined previously with `network use ` **Usage:** `stellar network unset [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ## `stellar network root-account` Compute the root account keypair for a network **Usage:** `stellar network root-account ` ###### **Subcommands:** - `public-key` — Output a network's root account address (public key) - `secret` — Output a network's root account secret key ## `stellar network root-account public-key` Output a network's root account address (public key) **Usage:** `stellar network root-account public-key [OPTIONS]` **Command Alias:** `address` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--network-passphrase ` — Network passphrase to derive the root account from - `-n`, `--network ` — Name of network to use from config ## `stellar network root-account secret` Output a network's root account secret key **Usage:** `stellar network root-account secret [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--network-passphrase ` — Network passphrase to derive the root account from - `-n`, `--network ` — Name of network to use from config ## `stellar container` Start local networks in containers **Usage:** `stellar container ` ###### **Subcommands:** - `logs` — Get logs from a running network container - `start` — Start a container running a Stellar node, RPC, API, and friendbot (faucet) - `stop` — Stop a network container started with `stellar container start` - `use` — Set the default container engine used by `stellar container` commands - `unset` — Unset the default container engine defined previously with `container use ` ## `stellar container logs` Get logs from a running network container **Usage:** `stellar container logs [OPTIONS] [NAME]` ###### **Arguments:** - `` — Container to get logs from Default value: `local` ###### **Options:** - `-d`, `--docker-host ` — Optional argument to override the default docker host. This is useful when you are using a non-standard docker host path for your Docker-compatible container runtime, e.g. Docker Desktop defaults to $HOME/.docker/run/docker.sock instead of /var/run/docker.sock - `--engine ` — Container engine to use [default: docker] Possible values: - `docker`: Docker, or any Docker-compatible CLI - `apple-container`: Apple's `container` CLI (macOS 26+, Apple silicon) ## `stellar container start` Start a container running a Stellar node, RPC, API, and friendbot (faucet). `stellar container start NETWORK [OPTIONS]` By default, when starting a testnet container, without any optional arguments, it will run the equivalent of the following docker command: `docker run --rm -p 8000:8000 --name stellar stellar/quickstart:latest --testnet --enable rpc,horizon` **Usage:** `stellar container start [OPTIONS] [NETWORK]` ###### **Arguments:** - `` — Network to start. Default is `local` Possible values: `local`, `testnet`, `futurenet`, `pubnet` ###### **Options:** - `-d`, `--docker-host ` — Optional argument to override the default docker host. This is useful when you are using a non-standard docker host path for your Docker-compatible container runtime, e.g. Docker Desktop defaults to $HOME/.docker/run/docker.sock instead of /var/run/docker.sock - `--engine ` — Container engine to use [default: docker] Possible values: - `docker`: Docker, or any Docker-compatible CLI - `apple-container`: Apple's `container` CLI (macOS 26+, Apple silicon) - `--cpus ` — Limit the number of CPUs available to the container, e.g. `2`. A whole number: Apple's `container` engine does not accept fractional CPUs - `--memory ` — Limit the memory available to the container, e.g. `2g` or `512m` - `--name ` — Optional argument to specify the container name - `-l`, `--limits ` — Optional argument to specify the limits for the local network only - `-p`, `--ports-mapping ` — Argument to specify the `HOST_PORT:CONTAINER_PORT` mapping Default value: `8000:8000` - `-t`, `--image-tag-override ` — Optional argument to override the default docker image tag for the given network - `--protocol-version ` — Optional argument to specify the protocol version for the local network only ## `stellar container stop` Stop a network container started with `stellar container start` **Usage:** `stellar container stop [OPTIONS] [NAME]` ###### **Arguments:** - `` — Container to stop Default value: `local` ###### **Options:** - `-d`, `--docker-host ` — Optional argument to override the default docker host. This is useful when you are using a non-standard docker host path for your Docker-compatible container runtime, e.g. Docker Desktop defaults to $HOME/.docker/run/docker.sock instead of /var/run/docker.sock - `--engine ` — Container engine to use [default: docker] Possible values: - `docker`: Docker, or any Docker-compatible CLI - `apple-container`: Apple's `container` CLI (macOS 26+, Apple silicon) ## `stellar container use` Set the default container engine used by `stellar container` commands **Usage:** `stellar container use [OPTIONS] ` ###### **Arguments:** - `` — Container engine to use by default Possible values: - `docker`: Docker, or any Docker-compatible CLI - `apple-container`: Apple's `container` CLI (macOS 26+, Apple silicon) ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ## `stellar container unset` Unset the default container engine defined previously with `container use ` **Usage:** `stellar container unset [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ## `stellar config` Manage CLI configuration **Usage:** `stellar config ` ###### **Subcommands:** - `migrate` — Migrate the local configuration to the global directory - `dir` — Show the global configuration directory ## `stellar config migrate` Migrate the local configuration to the global directory **Usage:** `stellar config migrate [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ## `stellar config dir` Show the global configuration directory. The location will depend on how your system is configured. - It looks up for `XDG_CONFIG_HOME` environment variable. If it's set, `$XDG_CONFIG_HOME/stellar` will be used. - If not set, it defaults to `$HOME/.config`. - Can be overridden by `--config-dir` flag. **Usage:** `stellar config dir [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ## `stellar snapshot` Download a snapshot of a ledger from an archive **Usage:** `stellar snapshot ` ###### **Subcommands:** - `create` — Create a ledger snapshot using a history archive - `merge` — Merge multiple ledger snapshots into a single snapshot file ## `stellar snapshot create` Create a ledger snapshot using a history archive. Filters (address, wasm-hash) specify what ledger entries to include. Account addresses include the account, and trustlines. Contract addresses include the related wasm, contract data. If a contract is a Stellar asset contract, it includes the asset issuer's account and trust lines, but does not include all the trust lines of other accounts holding the asset. To include them specify the addresses of relevant accounts. Any invalid contract id passed as `--address` will be ignored. **Usage:** `stellar snapshot create [OPTIONS]` ###### **Archive Options:** - `--archive-url ` — Archive URL ###### **Filter Options:** - `--address
` — Account or contract address/alias to include in the snapshot - `--wasm-hash ` — WASM hashes to include in the snapshot ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--ledger ` — The ledger sequence number to snapshot. Defaults to latest history archived ledger - `--output ` — Format of the out file Default value: `json` Possible values: `json` - `--out ` — Out path that the snapshot is written to Default value: `snapshot.json` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar snapshot merge` Merge multiple ledger snapshots into a single snapshot file. When the same ledger key appears in multiple snapshots, the entry from the last snapshot in the argument list takes precedence. Metadata (protocol_version, sequence_number, timestamp, etc.) is taken from the last snapshot. Example: stellar snapshot merge A.json B.json --out merged.json This allows combining snapshots from different contract deployments or manually edited snapshots without regenerating from scratch. **Usage:** `stellar snapshot merge [OPTIONS] ...` ###### **Arguments:** - `` — Snapshot files to merge (at least 2 required) ###### **Options:** - `-o`, `--out ` — Output path for the merged snapshot Default value: `snapshot.json` ## `stellar token` Interact with SEP-41 tokens and Stellar Asset Contracts **Usage:** `stellar token ` ###### **Subcommands:** - `transfer` — Transfer tokens from one account to another - `balance` — Read the token balance of an account or contract ## `stellar token transfer` Transfer tokens from one account to another **Usage:** `stellar token transfer [OPTIONS] --id --from --to --amount ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--id ` — The token to transfer from: a contract id or alias, `native`, or a classic asset as `CODE:ISSUER` - `--from ` — Account to transfer tokens from. Signs and authorizes the transfer, so it must be an identity or secret key you control - `--to ` — Account or contract to transfer the tokens to. Accepts a `G…`/`M…` account, a `C…` contract address, or an alias - `--amount ` — Amount to transfer, in the token's smallest unit (stroops for a Stellar Asset Contract) - `--output ` — Format of the output Default value: `text` Possible values: - `text`: Human-readable text - `json`: Compact, single-line JSON receipt - `json-formatted`: Formatted (multiline) JSON receipt ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ## `stellar token balance` Read the token balance of an account or contract **Usage:** `stellar token balance [OPTIONS] --id --account ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--id ` — The token to query: a contract id or alias, `native`, or a classic asset as `CODE:ISSUER` - `--account ` — Account or contract whose balance to read - `--decimal` — Format the balance as a decimal using the token's `decimals`, instead of the raw smallest unit (stroops for a Stellar Asset Contract) - `--output ` — Format of the output Default value: `text` Possible values: - `text`: Human-readable text - `json`: Compact, single-line JSON receipt - `json-formatted`: Formatted (multiline) JSON receipt ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar tx` Sign, Simulate, and Send transactions **Usage:** `stellar tx ` ###### **Subcommands:** - `update` — Update the transaction - `edit` — Edit a transaction envelope from stdin. This command respects the environment variables `STELLAR_EDITOR`, `EDITOR` and `VISUAL`, in that order - `hash` — Calculate the hash of a transaction envelope - `new` — Create a new transaction - `operation` — Manipulate the operations in a transaction, including adding new operations - `send` — Send a transaction envelope to the network - `sign` — Sign a transaction envelope appending the signature to the envelope - `simulate` — Simulate a transaction envelope from stdin - `fetch` — Fetch a transaction from the network by hash If no subcommand is passed in, the transaction envelope will be returned - `decode` — Decode a transaction envelope from XDR to JSON - `encode` — Encode a transaction envelope from JSON to XDR ## `stellar tx update` Update the transaction **Usage:** `stellar tx update ` ###### **Subcommands:** - `sequence-number` — Edit the sequence number on a transaction ## `stellar tx update sequence-number` Edit the sequence number on a transaction **Usage:** `stellar tx update sequence-number ` **Command Alias:** `seq-num` ###### **Subcommands:** - `next` — Fetch the source account's seq-num and increment for the given tx ## `stellar tx update sequence-number next` Fetch the source account's seq-num and increment for the given tx **Usage:** `stellar tx update sequence-number next [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar tx edit` Edit a transaction envelope from stdin. This command respects the environment variables `STELLAR_EDITOR`, `EDITOR` and `VISUAL`, in that order. Example: Start a new edit session $ stellar tx edit Example: Pipe an XDR transaction envelope $ stellar tx new manage-data --data-name hello --build-only | stellar tx edit **Usage:** `stellar tx edit` ## `stellar tx hash` Calculate the hash of a transaction envelope **Usage:** `stellar tx hash [OPTIONS] [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar tx new` Create a new transaction **Usage:** `stellar tx new ` ###### **Subcommands:** - `account-merge` — Transfer XLM balance to another account and remove source account - `begin-sponsoring-future-reserves` — Begin sponsoring future reserves for another account - `bump-sequence` — Bump sequence number to invalidate older transactions - `change-trust` — Create, update, or delete a trustline - `claim-claimable-balance` — Claim a claimable balance by its balance ID - `clawback` — Clawback an asset from an account - `clawback-claimable-balance` — Clawback a claimable balance by its balance ID - `create-account` — Create and fund a new account - `create-claimable-balance` — Create a claimable balance that can be claimed by specified accounts - `create-passive-sell-offer` — Create a passive sell offer on the Stellar DEX - `end-sponsoring-future-reserves` — End sponsoring future reserves - `liquidity-pool-deposit` — Deposit assets into a liquidity pool - `liquidity-pool-withdraw` — Withdraw assets from a liquidity pool - `manage-buy-offer` — Create, update, or delete a buy offer - `manage-data` — Set, modify, or delete account data entries - `manage-sell-offer` — Create, update, or delete a sell offer - `path-payment-strict-send` — Send a payment with a different asset using path finding, specifying the send amount - `path-payment-strict-receive` — Send a payment with a different asset using path finding, specifying the receive amount - `payment` — Send asset to destination account - `revoke-sponsorship` — Revoke sponsorship of a ledger entry or signer - `set-options` — Set account options like flags, signers, and home domain - `set-trustline-flags` — Configure authorization and trustline flags for an asset ## `stellar tx new account-merge` Transfer XLM balance to another account and remove source account **Usage:** `stellar tx new account-merge [OPTIONS] --source-account --account ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--account ` — Muxed Account to merge with, e.g. `GBX...`, 'MBX...' ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new begin-sponsoring-future-reserves` Begin sponsoring future reserves for another account **Usage:** `stellar tx new begin-sponsoring-future-reserves [OPTIONS] --source-account --sponsored-id ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--sponsored-id ` — Account that will be sponsored ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new bump-sequence` Bump sequence number to invalidate older transactions **Usage:** `stellar tx new bump-sequence [OPTIONS] --source-account --bump-to ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--bump-to ` — Sequence number to bump to ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new change-trust` Create, update, or delete a trustline **Usage:** `stellar tx new change-trust [OPTIONS] --source-account --line ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--line ` - `--limit ` — Limit for the trust line, 0 to remove the trust line Default value: `9223372036854775807` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new claim-claimable-balance` Claim a claimable balance by its balance ID **Usage:** `stellar tx new claim-claimable-balance [OPTIONS] --source-account --balance-id ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--balance-id ` — Balance ID of the claimable balance to claim (64-character hex string) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new clawback` Clawback an asset from an account **Usage:** `stellar tx new clawback [OPTIONS] --source-account --from --asset --amount ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--from ` — Account to clawback assets from, e.g. `GBX...` - `--asset ` — Asset to clawback - `--amount ` — Amount of the asset to clawback, in stroops. 1 stroop = 0.0000001 of the asset ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new clawback-claimable-balance` Clawback a claimable balance by its balance ID **Usage:** `stellar tx new clawback-claimable-balance [OPTIONS] --source-account --balance-id ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--balance-id ` — Balance ID of the claimable balance to clawback. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new create-account` Create and fund a new account **Usage:** `stellar tx new create-account [OPTIONS] --source-account --destination ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--destination ` — Account Id to create, e.g. `GBX...` - `--starting-balance ` — Initial balance in stroops of the account, default 1 XLM Default value: `10_000_000` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new create-claimable-balance` Create a claimable balance that can be claimed by specified accounts **Usage:** `stellar tx new create-claimable-balance [OPTIONS] --source-account --amount ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--asset ` — Asset to be held in the ClaimableBalanceEntry Default value: `native` - `--amount ` — Amount of asset to store in the entry, in stroops. 1 stroop = 0.0000001 of the asset - `--claimant ` — Claimants of the claimable balance. Format: account_id or account_id:predicate_json Can be specified multiple times for multiple claimants. Examples: - `--claimant alice (unconditional)` - `--claimant 'bob:{"before_absolute_time":"1735689599"}'` - `--claimant 'charlie:{"and":[{"before_absolute_time":"1735689599"},{"before_relative_time":"3600"}]}'` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new create-passive-sell-offer` Create a passive sell offer on the Stellar DEX **Usage:** `stellar tx new create-passive-sell-offer [OPTIONS] --source-account --selling --buying --amount --price ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--selling ` — Asset to sell - `--buying ` — Asset to buy - `--amount ` — Amount of selling asset to offer, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops) - `--price ` — Price of 1 unit of selling asset in terms of buying asset as "numerator:denominator" (e.g., "1:2" means 0.5) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new end-sponsoring-future-reserves` End sponsoring future reserves **Usage:** `stellar tx new end-sponsoring-future-reserves [OPTIONS] --source-account ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new liquidity-pool-deposit` Deposit assets into a liquidity pool **Usage:** `stellar tx new liquidity-pool-deposit [OPTIONS] --source-account --liquidity-pool-id --max-amount-a --max-amount-b ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--liquidity-pool-id ` — Liquidity pool ID to deposit to - `--max-amount-a ` — Maximum amount of the first asset to deposit, in stroops - `--max-amount-b ` — Maximum amount of the second asset to deposit, in stroops - `--min-price ` — Minimum price for the first asset in terms of the second asset as "numerator:denominator" (e.g., "1:2" means 0.5) Default value: `1:1` - `--max-price ` — Maximum price for the first asset in terms of the second asset as "numerator:denominator" (e.g., "1:2" means 0.5) Default value: `1:1` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new liquidity-pool-withdraw` Withdraw assets from a liquidity pool **Usage:** `stellar tx new liquidity-pool-withdraw [OPTIONS] --source-account --liquidity-pool-id --amount --min-amount-a --min-amount-b ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--liquidity-pool-id ` — Liquidity pool ID to withdraw from - `--amount ` — Amount of pool shares to withdraw, in stroops - `--min-amount-a ` — Minimum amount of the first asset to receive, in stroops - `--min-amount-b ` — Minimum amount of the second asset to receive, in stroops ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new manage-buy-offer` Create, update, or delete a buy offer **Usage:** `stellar tx new manage-buy-offer [OPTIONS] --source-account --selling --buying --amount --price ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--selling ` — Asset to sell - `--buying ` — Asset to buy - `--amount ` — Amount of buying asset to purchase, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops). Use `0` to remove the offer - `--price ` — Price of 1 unit of buying asset in terms of selling asset as "numerator:denominator" (e.g., "1:2" means 0.5) - `--offer-id ` — Offer ID. If 0, will create new offer. Otherwise, will update existing offer Default value: `0` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new manage-data` Set, modify, or delete account data entries **Usage:** `stellar tx new manage-data [OPTIONS] --source-account --data-name ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--data-name ` — String up to 64 bytes long. If this is a new Name it will add the given name/value pair to the account. If this Name is already present then the associated value will be modified - `--data-value ` — Up to 64 bytes long hex string If not present then the existing Name will be deleted. If present then this value will be set in the `DataEntry` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new manage-sell-offer` Create, update, or delete a sell offer **Usage:** `stellar tx new manage-sell-offer [OPTIONS] --source-account --selling --buying --amount --price ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--selling ` — Asset to sell - `--buying ` — Asset to buy - `--amount ` — Amount of selling asset to offer, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops). Use `0` to remove the offer - `--price ` — Price of 1 unit of selling asset in terms of buying asset as "numerator:denominator" (e.g., "1:2" means 0.5) - `--offer-id ` — Offer ID. If 0, will create new offer. Otherwise, will update existing offer Default value: `0` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new path-payment-strict-send` Send a payment with a different asset using path finding, specifying the send amount **Usage:** `stellar tx new path-payment-strict-send [OPTIONS] --source-account --send-asset --send-amount --destination --dest-asset --dest-min ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--send-asset ` — Asset to send (pay with) - `--send-amount ` — Amount of send asset to deduct from sender's account, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops) - `--destination ` — Account that receives the payment - `--dest-asset ` — Asset that the destination will receive - `--dest-min ` — Minimum amount of destination asset that the destination account can receive. The operation will fail if this amount cannot be met - `--path ` — List of intermediate assets for the payment path, comma-separated (up to 5 assets). Each asset should be in the format 'code:issuer' or 'native' for XLM ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new path-payment-strict-receive` Send a payment with a different asset using path finding, specifying the receive amount **Usage:** `stellar tx new path-payment-strict-receive [OPTIONS] --source-account --send-asset --send-max --destination --dest-asset --dest-amount ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--send-asset ` — Asset to send (pay with) - `--send-max ` — Maximum amount of send asset to deduct from sender's account, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops) - `--destination ` — Account that receives the payment - `--dest-asset ` — Asset that the destination will receive - `--dest-amount ` — Exact amount of destination asset that the destination account will receive, in stroops. 1 stroop = 0.0000001 of the asset - `--path ` — List of intermediate assets for the payment path, comma-separated (up to 5 assets). Each asset should be in the format 'code:issuer' or 'native' for XLM ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new payment` Send asset to destination account **Usage:** `stellar tx new payment [OPTIONS] --source-account --destination --amount ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--destination ` — Account to send to, e.g. `GBX...` - `--asset ` — Asset to send, default native, e.i. XLM Default value: `native` - `--amount ` — Amount of the aforementioned asset to send, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new revoke-sponsorship` Revoke sponsorship of a ledger entry or signer **Usage:** `stellar tx new revoke-sponsorship [OPTIONS] --source-account --account-id ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--account-id ` — Account ID (required for all sponsorship types) - `--asset ` — Asset for trustline sponsorship (format: CODE:ISSUER) - `--data-name ` — Data name for data entry sponsorship - `--offer-id ` — Offer ID for offer sponsorship - `--liquidity-pool-id ` — Pool ID for liquidity pool sponsorship. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - `--claimable-balance-id ` — Claimable balance ID for claimable balance sponsorship. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA - `--signer-key ` — Signer key for signer sponsorship ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new set-options` Set account options like flags, signers, and home domain **Usage:** `stellar tx new set-options [OPTIONS] --source-account ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--inflation-dest ` — Account of the inflation destination - `--master-weight ` — A number from 0-255 (inclusive) representing the weight of the master key. If the weight of the master key is updated to 0, it is effectively disabled - `--low-threshold ` — A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a low threshold. https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig - `--med-threshold ` — A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a medium threshold. https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig - `--high-threshold ` — A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a high threshold. https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig - `--home-domain ` — Sets the home domain of an account. See https://developers.stellar.org/docs/learn/encyclopedia/network-configuration/federation - `--signer ` — Add, update, or remove a signer from an account - `--signer-weight ` — Signer weight is a number from 0-255 (inclusive). The signer is deleted if the weight is 0 - `--set-required` — When enabled, an issuer must approve an account before that account can hold its asset. https://developers.stellar.org/docs/tokens/control-asset-access#authorization-required-0x1 - `--set-revocable` — When enabled, an issuer can revoke an existing trustline's authorization, thereby freezing the asset held by an account. https://developers.stellar.org/docs/tokens/control-asset-access#authorization-revocable-0x2 - `--set-clawback-enabled` — Enables the issuing account to take back (burning) all of the asset. https://developers.stellar.org/docs/tokens/control-asset-access#clawback-enabled-0x8 - `--set-immutable` — With this setting, none of the other authorization flags (`AUTH_REQUIRED_FLAG`, `AUTH_REVOCABLE_FLAG`) can be set, and the issuing account can't be merged. https://developers.stellar.org/docs/tokens/control-asset-access#authorization-immutable-0x4 - `--clear-required` - `--clear-revocable` - `--clear-immutable` - `--clear-clawback-enabled` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx new set-trustline-flags` Configure authorization and trustline flags for an asset **Usage:** `stellar tx new set-trustline-flags [OPTIONS] --source-account --trustor --asset ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--trustor ` — Account to set trustline flags for, e.g. `GBX...`, or alias, or muxed account, `M123...`` - `--asset ` — Asset to set trustline flags for - `--set-authorize` — Signifies complete authorization allowing an account to transact freely with the asset to make and receive payments and place orders - `--set-authorize-to-maintain-liabilities` — Denotes limited authorization that allows an account to maintain current orders but not to otherwise transact with the asset - `--set-trustline-clawback-enabled` — Enables the issuing account to take back (burning) all of the asset. See our section on Clawbacks: https://developers.stellar.org/docs/learn/encyclopedia/transactions-specialized/clawbacks - `--clear-authorize` - `--clear-authorize-to-maintain-liabilities` - `--clear-trustline-clawback-enabled` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation` Manipulate the operations in a transaction, including adding new operations **Usage:** `stellar tx operation ` **Command Alias:** `op` ###### **Subcommands:** - `add` — Add Operation to a transaction ## `stellar tx operation add` Add Operation to a transaction **Usage:** `stellar tx operation add ` ###### **Subcommands:** - `account-merge` — Transfer XLM balance to another account and remove source account - `begin-sponsoring-future-reserves` — Begin sponsoring future reserves for another account - `bump-sequence` — Bump sequence number to invalidate older transactions - `change-trust` — Create, update, or delete a trustline - `claim-claimable-balance` — Claim a claimable balance by its balance ID - `clawback` — Clawback an asset from an account - `clawback-claimable-balance` — Clawback a claimable balance by its balance ID - `create-account` — Create and fund a new account - `create-claimable-balance` — Create a claimable balance that can be claimed by specified accounts - `create-passive-sell-offer` — Create a passive sell offer on the Stellar DEX - `end-sponsoring-future-reserves` — End sponsoring future reserves - `liquidity-pool-deposit` — Deposit assets into a liquidity pool - `liquidity-pool-withdraw` — Withdraw assets from a liquidity pool - `manage-buy-offer` — Create, update, or delete a buy offer - `manage-data` — Set, modify, or delete account data entries - `manage-sell-offer` — Create, update, or delete a sell offer - `path-payment-strict-receive` — Send a payment with a different asset using path finding, specifying the receive amount - `path-payment-strict-send` — Send a payment with a different asset using path finding, specifying the send amount - `payment` — Send asset to destination account - `revoke-sponsorship` — Revoke sponsorship of a ledger entry or signer - `set-options` — Set account options like flags, signers, and home domain - `set-trustline-flags` — Configure authorization and trustline flags for an asset ## `stellar tx operation add account-merge` Transfer XLM balance to another account and remove source account **Usage:** `stellar tx operation add account-merge [OPTIONS] --source-account --account [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--account ` — Muxed Account to merge with, e.g. `GBX...`, 'MBX...' ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add begin-sponsoring-future-reserves` Begin sponsoring future reserves for another account **Usage:** `stellar tx operation add begin-sponsoring-future-reserves [OPTIONS] --source-account --sponsored-id [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--sponsored-id ` — Account that will be sponsored ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add bump-sequence` Bump sequence number to invalidate older transactions **Usage:** `stellar tx operation add bump-sequence [OPTIONS] --source-account --bump-to [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--bump-to ` — Sequence number to bump to ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add change-trust` Create, update, or delete a trustline **Usage:** `stellar tx operation add change-trust [OPTIONS] --source-account --line [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--line ` - `--limit ` — Limit for the trust line, 0 to remove the trust line Default value: `9223372036854775807` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add claim-claimable-balance` Claim a claimable balance by its balance ID **Usage:** `stellar tx operation add claim-claimable-balance [OPTIONS] --source-account --balance-id [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--balance-id ` — Balance ID of the claimable balance to claim (64-character hex string) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add clawback` Clawback an asset from an account **Usage:** `stellar tx operation add clawback [OPTIONS] --source-account --from --asset --amount [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--from ` — Account to clawback assets from, e.g. `GBX...` - `--asset ` — Asset to clawback - `--amount ` — Amount of the asset to clawback, in stroops. 1 stroop = 0.0000001 of the asset ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add clawback-claimable-balance` Clawback a claimable balance by its balance ID **Usage:** `stellar tx operation add clawback-claimable-balance [OPTIONS] --source-account --balance-id [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--balance-id ` — Balance ID of the claimable balance to clawback. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add create-account` Create and fund a new account **Usage:** `stellar tx operation add create-account [OPTIONS] --source-account --destination [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--destination ` — Account Id to create, e.g. `GBX...` - `--starting-balance ` — Initial balance in stroops of the account, default 1 XLM Default value: `10_000_000` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add create-claimable-balance` Create a claimable balance that can be claimed by specified accounts **Usage:** `stellar tx operation add create-claimable-balance [OPTIONS] --source-account --amount [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--asset ` — Asset to be held in the ClaimableBalanceEntry Default value: `native` - `--amount ` — Amount of asset to store in the entry, in stroops. 1 stroop = 0.0000001 of the asset - `--claimant ` — Claimants of the claimable balance. Format: account_id or account_id:predicate_json Can be specified multiple times for multiple claimants. Examples: - `--claimant alice (unconditional)` - `--claimant 'bob:{"before_absolute_time":"1735689599"}'` - `--claimant 'charlie:{"and":[{"before_absolute_time":"1735689599"},{"before_relative_time":"3600"}]}'` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add create-passive-sell-offer` Create a passive sell offer on the Stellar DEX **Usage:** `stellar tx operation add create-passive-sell-offer [OPTIONS] --source-account --selling --buying --amount --price [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--selling ` — Asset to sell - `--buying ` — Asset to buy - `--amount ` — Amount of selling asset to offer, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops) - `--price ` — Price of 1 unit of selling asset in terms of buying asset as "numerator:denominator" (e.g., "1:2" means 0.5) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add end-sponsoring-future-reserves` End sponsoring future reserves **Usage:** `stellar tx operation add end-sponsoring-future-reserves [OPTIONS] --source-account [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add liquidity-pool-deposit` Deposit assets into a liquidity pool **Usage:** `stellar tx operation add liquidity-pool-deposit [OPTIONS] --source-account --liquidity-pool-id --max-amount-a --max-amount-b [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--liquidity-pool-id ` — Liquidity pool ID to deposit to - `--max-amount-a ` — Maximum amount of the first asset to deposit, in stroops - `--max-amount-b ` — Maximum amount of the second asset to deposit, in stroops - `--min-price ` — Minimum price for the first asset in terms of the second asset as "numerator:denominator" (e.g., "1:2" means 0.5) Default value: `1:1` - `--max-price ` — Maximum price for the first asset in terms of the second asset as "numerator:denominator" (e.g., "1:2" means 0.5) Default value: `1:1` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add liquidity-pool-withdraw` Withdraw assets from a liquidity pool **Usage:** `stellar tx operation add liquidity-pool-withdraw [OPTIONS] --source-account --liquidity-pool-id --amount --min-amount-a --min-amount-b [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--liquidity-pool-id ` — Liquidity pool ID to withdraw from - `--amount ` — Amount of pool shares to withdraw, in stroops - `--min-amount-a ` — Minimum amount of the first asset to receive, in stroops - `--min-amount-b ` — Minimum amount of the second asset to receive, in stroops ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add manage-buy-offer` Create, update, or delete a buy offer **Usage:** `stellar tx operation add manage-buy-offer [OPTIONS] --source-account --selling --buying --amount --price [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--selling ` — Asset to sell - `--buying ` — Asset to buy - `--amount ` — Amount of buying asset to purchase, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops). Use `0` to remove the offer - `--price ` — Price of 1 unit of buying asset in terms of selling asset as "numerator:denominator" (e.g., "1:2" means 0.5) - `--offer-id ` — Offer ID. If 0, will create new offer. Otherwise, will update existing offer Default value: `0` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add manage-data` Set, modify, or delete account data entries **Usage:** `stellar tx operation add manage-data [OPTIONS] --source-account --data-name [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--data-name ` — String up to 64 bytes long. If this is a new Name it will add the given name/value pair to the account. If this Name is already present then the associated value will be modified - `--data-value ` — Up to 64 bytes long hex string If not present then the existing Name will be deleted. If present then this value will be set in the `DataEntry` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add manage-sell-offer` Create, update, or delete a sell offer **Usage:** `stellar tx operation add manage-sell-offer [OPTIONS] --source-account --selling --buying --amount --price [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--selling ` — Asset to sell - `--buying ` — Asset to buy - `--amount ` — Amount of selling asset to offer, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops). Use `0` to remove the offer - `--price ` — Price of 1 unit of selling asset in terms of buying asset as "numerator:denominator" (e.g., "1:2" means 0.5) - `--offer-id ` — Offer ID. If 0, will create new offer. Otherwise, will update existing offer Default value: `0` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add path-payment-strict-receive` Send a payment with a different asset using path finding, specifying the receive amount **Usage:** `stellar tx operation add path-payment-strict-receive [OPTIONS] --source-account --send-asset --send-max --destination --dest-asset --dest-amount [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--send-asset ` — Asset to send (pay with) - `--send-max ` — Maximum amount of send asset to deduct from sender's account, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops) - `--destination ` — Account that receives the payment - `--dest-asset ` — Asset that the destination will receive - `--dest-amount ` — Exact amount of destination asset that the destination account will receive, in stroops. 1 stroop = 0.0000001 of the asset - `--path ` — List of intermediate assets for the payment path, comma-separated (up to 5 assets). Each asset should be in the format 'code:issuer' or 'native' for XLM ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add path-payment-strict-send` Send a payment with a different asset using path finding, specifying the send amount **Usage:** `stellar tx operation add path-payment-strict-send [OPTIONS] --source-account --send-asset --send-amount --destination --dest-asset --dest-min [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--send-asset ` — Asset to send (pay with) - `--send-amount ` — Amount of send asset to deduct from sender's account, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops) - `--destination ` — Account that receives the payment - `--dest-asset ` — Asset that the destination will receive - `--dest-min ` — Minimum amount of destination asset that the destination account can receive. The operation will fail if this amount cannot be met - `--path ` — List of intermediate assets for the payment path, comma-separated (up to 5 assets). Each asset should be in the format 'code:issuer' or 'native' for XLM ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add payment` Send asset to destination account **Usage:** `stellar tx operation add payment [OPTIONS] --source-account --destination --amount [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--destination ` — Account to send to, e.g. `GBX...` - `--asset ` — Asset to send, default native, e.i. XLM Default value: `native` - `--amount ` — Amount of the aforementioned asset to send, in stroops. 1 stroop = 0.0000001 of the asset (e.g. 1 XLM = `10_000_000` stroops) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add revoke-sponsorship` Revoke sponsorship of a ledger entry or signer **Usage:** `stellar tx operation add revoke-sponsorship [OPTIONS] --source-account --account-id [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--account-id ` — Account ID (required for all sponsorship types) - `--asset ` — Asset for trustline sponsorship (format: CODE:ISSUER) - `--data-name ` — Data name for data entry sponsorship - `--offer-id ` — Offer ID for offer sponsorship - `--liquidity-pool-id ` — Pool ID for liquidity pool sponsorship. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - `--claimable-balance-id ` — Claimable balance ID for claimable balance sponsorship. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA - `--signer-key ` — Signer key for signer sponsorship ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add set-options` Set account options like flags, signers, and home domain **Usage:** `stellar tx operation add set-options [OPTIONS] --source-account [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--inflation-dest ` — Account of the inflation destination - `--master-weight ` — A number from 0-255 (inclusive) representing the weight of the master key. If the weight of the master key is updated to 0, it is effectively disabled - `--low-threshold ` — A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a low threshold. https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig - `--med-threshold ` — A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a medium threshold. https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig - `--high-threshold ` — A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a high threshold. https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig - `--home-domain ` — Sets the home domain of an account. See https://developers.stellar.org/docs/learn/encyclopedia/network-configuration/federation - `--signer ` — Add, update, or remove a signer from an account - `--signer-weight ` — Signer weight is a number from 0-255 (inclusive). The signer is deleted if the weight is 0 - `--set-required` — When enabled, an issuer must approve an account before that account can hold its asset. https://developers.stellar.org/docs/tokens/control-asset-access#authorization-required-0x1 - `--set-revocable` — When enabled, an issuer can revoke an existing trustline's authorization, thereby freezing the asset held by an account. https://developers.stellar.org/docs/tokens/control-asset-access#authorization-revocable-0x2 - `--set-clawback-enabled` — Enables the issuing account to take back (burning) all of the asset. https://developers.stellar.org/docs/tokens/control-asset-access#clawback-enabled-0x8 - `--set-immutable` — With this setting, none of the other authorization flags (`AUTH_REQUIRED_FLAG`, `AUTH_REVOCABLE_FLAG`) can be set, and the issuing account can't be merged. https://developers.stellar.org/docs/tokens/control-asset-access#authorization-immutable-0x4 - `--clear-required` - `--clear-revocable` - `--clear-immutable` - `--clear-clawback-enabled` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx operation add set-trustline-flags` Configure authorization and trustline flags for an asset **Usage:** `stellar tx operation add set-trustline-flags [OPTIONS] --source-account --trustor --asset [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--operation-source-account ` [alias: `op-source`] — Source account used for the operation - `--trustor ` — Account to set trustline flags for, e.g. `GBX...`, or alias, or muxed account, `M123...`` - `--asset ` — Asset to set trustline flags for - `--set-authorize` — Signifies complete authorization allowing an account to transact freely with the asset to make and receive payments and place orders - `--set-authorize-to-maintain-liabilities` — Denotes limited authorization that allows an account to maintain current orders but not to otherwise transact with the asset - `--set-trustline-clawback-enabled` — Enables the issuing account to take back (burning) all of the asset. See our section on Clawbacks: https://developers.stellar.org/docs/learn/encyclopedia/transactions-specialized/clawbacks - `--clear-authorize` - `--clear-authorize-to-maintain-liabilities` - `--clear-trustline-clawback-enabled` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout ## `stellar tx send` Send a transaction envelope to the network **Usage:** `stellar tx send [OPTIONS] [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar tx sign` Sign a transaction envelope appending the signature to the envelope **Usage:** `stellar tx sign [OPTIONS] [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR, or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ## `stellar tx simulate` Simulate a transaction envelope from stdin **Usage:** `stellar tx simulate [OPTIONS] --source-account [TX_XDR]` ###### **Arguments:** - `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--instruction-leeway ` — Allow this many extra instructions when budgeting resources during transaction simulation ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config - `--auth-mode ` — Set the authorization mode for transaction simulation. When unset, the RPC default is used: record with the root mode if no authorization entries exist, otherwise enforce the provided entries. Should only be set for `InvokeHostFunction` transactions. The `enforce` mode is for simulating transactions that already contain authorization entries Possible values: - `enforce`: Validate the authorization entries already on the transaction - `root`: Record authorization entries, requiring each to be rooted at the transaction's top-level operation - `non-root`: Record all authorization entries, including non-root entries ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - `--sign-with-lab` — Sign with https://lab.stellar.org - `--sign-with-ledger` — Sign with a ledger wallet - `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries ###### **Transaction Options:** - `-s`, `--source-account ` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail - `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided ## `stellar tx fetch` Fetch a transaction from the network by hash If no subcommand is passed in, the transaction envelope will be returned **Usage:** `stellar tx fetch [OPTIONS] fetch ` ###### **Subcommands:** - `result` — Fetch the transaction result - `meta` — Fetch the transaction meta - `fee` — Fetch the transaction fee information - `events` — Fetch the transaction events ###### **Options:** - `--hash ` — Hash of transaction to fetch - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output with parsed XDRs - `xdr`: Original RPC output (containing XDRs) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar tx fetch result` Fetch the transaction result **Usage:** `stellar tx fetch result [OPTIONS] --hash ` ###### **Options:** - `--hash ` — Transaction hash to fetch - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output with parsed XDRs - `xdr`: Original RPC output (containing XDRs) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar tx fetch meta` Fetch the transaction meta **Usage:** `stellar tx fetch meta [OPTIONS] --hash ` ###### **Options:** - `--hash ` — Transaction hash to fetch - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output with parsed XDRs - `xdr`: Original RPC output (containing XDRs) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar tx fetch fee` Fetch the transaction fee information **Usage:** `stellar tx fetch fee [OPTIONS] --hash ` ###### **Options:** - `--hash ` — Transaction hash to fetch - `--output ` — Output format for fee command Default value: `table` Possible values: - `json`: JSON output of the ledger entry with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the ledger entry with parsed XDRs - `table`: Formatted in a table comparing fee types ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar tx fetch events` Fetch the transaction events **Usage:** `stellar tx fetch events [OPTIONS] --hash ` ###### **Options:** - `--hash ` — Transaction hash to fetch - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output of the events with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of events with parsed XDRs - `text`: Human readable event output with parsed XDRs ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar tx decode` Decode a transaction envelope from XDR to JSON **Usage:** `stellar tx decode [OPTIONS] [INPUT]...` ###### **Arguments:** - `` — XDR or files containing XDR to decode, or stdin if empty ###### **Options:** - `--input ` Default value: `single-base64` Possible values: `single-base64`, `single` - `--output ` Default value: `json` Possible values: `json`, `json-formatted` ## `stellar tx encode` Encode a transaction envelope from JSON to XDR **Usage:** `stellar tx encode [OPTIONS] [INPUT]...` ###### **Arguments:** - `` — XDR or files containing XDR to decode, or stdin if empty ###### **Options:** - `--input ` Default value: `json` Possible values: `json` - `--output ` Default value: `single-base64` Possible values: `single-base64`, `single` ## `stellar xdr` Decode and encode XDR **Usage:** `stellar xdr ` ###### **Subcommands:** - `types` — View information about types - `guess` — Guess the XDR type - `decode` — Decode XDR - `encode` — Encode XDR - `compare` — Compare two XDR values with each other - `generate` — Generate XDR values - `xfile` — Preprocess XDR .x files - `version` — Print version information ## `stellar xdr types` View information about types **Usage:** `stellar xdr types ` ###### **Subcommands:** - `list` — - `schema` — - `schema-files` — Generate JSON schema files for the XDR types, writing a file for each type to the out directory ## `stellar xdr types list` **Usage:** `stellar xdr types list [OPTIONS]` ###### **Options:** - `--output ` Default value: `plain` Possible values: `plain`, `json`, `json-formatted` ## `stellar xdr types schema` **Usage:** `stellar xdr types schema [OPTIONS] --type ` ###### **Options:** - `--type ` — XDR type to generate schema for - `--output ` Default value: `json-schema-draft201909` Possible values: `json-schema-draft201909` ## `stellar xdr types schema-files` Generate JSON schema files for the XDR types, writing a file for each type to the out directory **Usage:** `stellar xdr types schema-files [OPTIONS] --out-dir ` ###### **Options:** - `--out-dir ` - `--output ` Default value: `json-schema-draft201909` Possible values: `json-schema-draft201909` ## `stellar xdr guess` Guess the XDR type. Prints a list of types that the XDR values can be decoded into. **Usage:** `stellar xdr guess [OPTIONS] [INPUT]` ###### **Arguments:** - `` — XDR or file containing XDR to decode, or stdin if empty ###### **Options:** - `--input ` Default value: `single-base64` Possible values: `single`, `single-base64`, `stream`, `stream-base64`, `stream-framed` - `--output ` Default value: `list` Possible values: `list` - `--certainty ` — Certainty as an arbitrary value Default value: `2` ## `stellar xdr decode` Decode XDR **Usage:** `stellar xdr decode [OPTIONS] --type [INPUT]...` ###### **Arguments:** - `` — XDR or files containing XDR to decode, or stdin if empty ###### **Options:** - `--type ` — XDR type to decode - `--input ` Default value: `stream-base64` Possible values: `single`, `single-base64`, `stream`, `stream-base64`, `stream-framed` - `--output ` Default value: `json` Possible values: `json`, `json-formatted`, `text`, `rust-debug`, `rust-debug-formatted` ## `stellar xdr encode` Encode XDR **Usage:** `stellar xdr encode [OPTIONS] --type [INPUT]...` ###### **Arguments:** - `` — XDR or files containing XDR to decode, or stdin if empty ###### **Options:** - `--type ` — XDR type to encode - `--input ` Default value: `json` Possible values: `json` - `--output ` Default value: `single-base64` Possible values: `single`, `single-base64`, `stream` ## `stellar xdr compare` Compare two XDR values with each other Outputs: `-1` when the left XDR value is less than the right XDR value, `0` when the left XDR value is equal to the right XDR value, `1` when the left XDR value is greater than the right XDR value **Usage:** `stellar xdr compare [OPTIONS] --type ` ###### **Arguments:** - `` — XDR file to decode and compare with the right value - `` — XDR file to decode and compare with the left value ###### **Options:** - `--type ` — XDR type of both inputs - `--input ` Default value: `single-base64` Possible values: `single`, `single-base64` ## `stellar xdr generate` Generate XDR values **Usage:** `stellar xdr generate ` ###### **Subcommands:** - `default` — Generate default XDR values - `arbitrary` — Generate arbitrary XDR values ## `stellar xdr generate default` Generate default XDR values **Usage:** `stellar xdr generate default [OPTIONS] --type ` ###### **Options:** - `--type ` — XDR type to generate - `--output ` Default value: `single-base64` Possible values: `single`, `single-base64`, `json`, `json-formatted`, `text` ## `stellar xdr generate arbitrary` Generate arbitrary XDR values **Usage:** `stellar xdr generate arbitrary [OPTIONS] --type ` ###### **Options:** - `--type ` — XDR type to generate - `--output ` Default value: `single-base64` Possible values: `single`, `single-base64`, `json`, `json-formatted`, `text` ## `stellar xdr xfile` Preprocess XDR .x files **Usage:** `stellar xdr xfile ` ###### **Subcommands:** - `preprocess` — Preprocess XDR .x files by evaluating #ifdef/#ifndef/#elif/#else/#endif directives ## `stellar xdr xfile preprocess` Preprocess XDR .x files by evaluating #ifdef/#ifndef/#elif/#else/#endif directives **Usage:** `stellar xdr xfile preprocess [OPTIONS] [INPUT]` ###### **Arguments:** - `` — XDR .x file to preprocess, or stdin if omitted ###### **Options:** - `--features ` — Features/symbols to define - `--all-features` — Enable all features/symbols found in the input ## `stellar xdr version` Print version information **Usage:** `stellar xdr version` ## `stellar strkey` Decode and encode strkey **Usage:** `stellar strkey ` ###### **Subcommands:** - `decode` — Decode strkey - `encode` — Encode strkey - `zero` — Generate the zero strkey - `version` — Print version information ## `stellar strkey decode` Decode strkey **Usage:** `stellar strkey decode ` ###### **Arguments:** - `` — Strkey to decode ## `stellar strkey encode` Encode strkey **Usage:** `stellar strkey encode ` ###### **Arguments:** - `` — JSON for Strkey to encode ## `stellar strkey zero` Generate the zero strkey **Usage:** `stellar strkey zero [OPTIONS] ` ###### **Arguments:** - `` — Strkey type to generate the zero value for Possible values: `public_key_ed25519`, `pre_auth_tx`, `hash_x`, `muxed_account_ed25519`, `signed_payload_ed25519`, `contract`, `liquidity_pool`, `claimable_balance_v0` ###### **Options:** - `--output ` — Output format Default value: `strkey` Possible values: `strkey`, `json` ## `stellar strkey version` Print version information **Usage:** `stellar strkey version` ## `stellar completion` Print shell completion code for the specified shell Ensure the completion package for your shell is installed, e.g. bash-completion for bash. To enable autocomplete in the current bash shell, run: `source <(stellar completion --shell bash)` To enable autocomplete permanently, run: `echo "source <(stellar completion --shell bash)" >> ~/.bashrc` **Usage:** `stellar completion --shell ` ###### **Options:** - `--shell ` — The shell type Possible values: `bash`, `elvish`, `fish`, `powershell`, `zsh` ## `stellar cache` Cache for transactions and contract specs **Usage:** `stellar cache ` ###### **Subcommands:** - `clean` — Delete the cache - `path` — Show the location of the cache - `actionlog` — Access details about cached actions like transactions, and simulations. (Experimental. May see breaking changes at any time.) ## `stellar cache clean` Delete the cache **Usage:** `stellar cache clean` ## `stellar cache path` Show the location of the cache **Usage:** `stellar cache path` ## `stellar cache actionlog` Access details about cached actions like transactions, and simulations. (Experimental. May see breaking changes at any time.) **Usage:** `stellar cache actionlog ` ###### **Subcommands:** - `ls` — List cached actions (transactions, simulations) - `read` — Read cached action ## `stellar cache actionlog ls` List cached actions (transactions, simulations) **Usage:** `stellar cache actionlog ls [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `-l`, `--long` ## `stellar cache actionlog read` Read cached action **Usage:** `stellar cache actionlog read --id ` ###### **Options:** - `--id ` — ID of the cache entry ## `stellar version` Print version information **Usage:** `stellar version [OPTIONS]` ###### **Options:** - `--only-version` — Print only the version - `--only-version-major` — Print only the major version - `--only-commit` — Print only the commit sha ## `stellar plugin` The subcommand for CLI plugins **Usage:** `stellar plugin ` ###### **Subcommands:** - `search` — Search for CLI plugins using GitHub - `ls` — List installed plugins ## `stellar plugin search` Search for CLI plugins using GitHub **Usage:** `stellar plugin search` ## `stellar plugin ls` List installed plugins **Usage:** `stellar plugin ls` ## `stellar ledger` Fetch ledger information **Usage:** `stellar ledger ` ###### **Subcommands:** - `entry` — Work with ledger entries - `latest` — Get the latest ledger sequence and information from the network - `fetch` — ## `stellar ledger entry` Work with ledger entries **Usage:** `stellar ledger entry ` ###### **Subcommands:** - `fetch` — Fetch ledger entries. This command supports all types of ledger entries supported by the RPC. Read more about the RPC command here: [https://developers.stellar.org/docs/data/apis/rpc/api-reference/methods/getLedgerEntries#types-of-ledgerkeys](https://developers.stellar.org/docs/data/apis/rpc/api-reference/methods/getLedgerEntries#types-of-ledgerkeys) ## `stellar ledger entry fetch` Fetch ledger entries. This command supports all types of ledger entries supported by the RPC. Read more about the RPC command here: [https://developers.stellar.org/docs/data/apis/rpc/api-reference/methods/getLedgerEntries#types-of-ledgerkeys](https://developers.stellar.org/docs/data/apis/rpc/api-reference/methods/getLedgerEntries#types-of-ledgerkeys) **Usage:** `stellar ledger entry fetch ` ###### **Subcommands:** - `account` — Fetch account entry by public key or alias - `contract-data` — Fetch contract ledger entry by address or alias and storage key - `claimable-balance` — Fetch a claimable balance ledger entry by id - `liquidity-pool` — Fetch a liquidity pool ledger entry by id - `contract-code` — Fetch a Contract's WASM bytecode by WASM hash - `trustline` — Fetch a trustline by account and asset - `data` — Fetch key-value data entries attached to an account (see manageDataOp) - `offer` — Fetch an offer by account and offer id ## `stellar ledger entry fetch account` Fetch account entry by public key or alias **Usage:** `stellar ledger entry fetch account [OPTIONS] --account ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--account ` — Account alias or address to lookup - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output of the ledger entry with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the ledger entry with parsed XDRs - `xdr`: Original RPC output (containing XDRs) - `--hd-path ` — If identity is a seed phrase use this hd path, default is 0 ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar ledger entry fetch contract-data` Fetch contract ledger entry by address or alias and storage key **Usage:** `stellar ledger entry fetch contract-data [OPTIONS] --contract ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--contract ` — Contract alias or address to fetch - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output of the ledger entry with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the ledger entry with parsed XDRs - `xdr`: Original RPC output (containing XDRs) - `--durability ` — Storage entry durability Default value: `persistent` Possible values: - `persistent`: Persistent - `temporary`: Temporary - `--key ` — Storage key (symbols only) - `--key-xdr ` — Storage key (base64-encoded XDR) - `--instance` — If the contract instance ledger entry should be included in the output ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar ledger entry fetch claimable-balance` Fetch a claimable balance ledger entry by id **Usage:** `stellar ledger entry fetch claimable-balance [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--id ` — Claimable Balance Ids to fetch an entry for - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output of the ledger entry with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the ledger entry with parsed XDRs - `xdr`: Original RPC output (containing XDRs) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar ledger entry fetch liquidity-pool` Fetch a liquidity pool ledger entry by id **Usage:** `stellar ledger entry fetch liquidity-pool [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--id ` — Liquidity pool ids - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output of the ledger entry with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the ledger entry with parsed XDRs - `xdr`: Original RPC output (containing XDRs) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar ledger entry fetch contract-code` Fetch a Contract's WASM bytecode by WASM hash **Usage:** `stellar ledger entry fetch contract-code [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--wasm-hash ` — Get WASM bytecode by hash - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output of the ledger entry with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the ledger entry with parsed XDRs - `xdr`: Original RPC output (containing XDRs) ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar ledger entry fetch trustline` Fetch a trustline by account and asset **Usage:** `stellar ledger entry fetch trustline [OPTIONS] --account --asset ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output of the ledger entry with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the ledger entry with parsed XDRs - `xdr`: Original RPC output (containing XDRs) - `--account ` — Account alias or address to lookup - `--asset ` — Assets to get trustline info for - `--hd-path ` — If account is a seed phrase use this hd path, default is 0 ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar ledger entry fetch data` Fetch key-value data entries attached to an account (see manageDataOp) **Usage:** `stellar ledger entry fetch data [OPTIONS] --account --data-name ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output of the ledger entry with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the ledger entry with parsed XDRs - `xdr`: Original RPC output (containing XDRs) - `--account ` — Account alias or address to lookup - `--data-name ` — Fetch key-value data entries attached to an account (see manageDataOp) - `--hd-path ` — If identity is a seed phrase use this hd path, default is 0 ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar ledger entry fetch offer` Fetch an offer by account and offer id **Usage:** `stellar ledger entry fetch offer [OPTIONS] --account --offer ` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--output ` — Format of the output Default value: `json` Possible values: - `json`: JSON output of the ledger entry with parsed XDRs (one line, not formatted) - `json-formatted`: Formatted (multiline) JSON output of the ledger entry with parsed XDRs - `xdr`: Original RPC output (containing XDRs) - `--account ` — Account alias or address to lookup - `--offer ` — ID of an offer made on the Stellar DEX - `--hd-path ` — If identity is a seed phrase use this hd path, default is 0 ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar ledger latest` Get the latest ledger sequence and information from the network **Usage:** `stellar ledger latest [OPTIONS]` ###### **Options:** - `--output ` — Format of the output Default value: `text` Possible values: - `text`: Text output of network info - `json`: JSON result of the RPC request - `json-formatted`: Formatted (multiline) JSON output of the RPC request ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar ledger fetch` **Usage:** `stellar ledger fetch [OPTIONS] ` ###### **Arguments:** - `` — Ledger Sequence to start fetch (inclusive) ###### **Options:** - `--limit ` — Number of ledgers to fetch Default value: `1` - `--output ` — Format of the output Default value: `text` Possible values: - `text`: Text output of network info - `json`: JSON result of the RPC request - `json-formatted`: Formatted (multiline) JSON output of the RPC request - `--xdr-format ` — Format of the xdr in the output Default value: `json` Possible values: - `json`: XDR fields will be fetched as json and accessible via the headerJson and metadataJson fields - `xdr`: XDR fields will be fetched as xdr and accessible via the headerXdr and metadataXdr fields ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar message` Sign and verify arbitrary messages using SEP-53 **Usage:** `stellar message ` ###### **Subcommands:** - `sign` — Sign an arbitrary message using SEP-53 - `verify` — Verify a SEP-53 signed message ## `stellar message sign` Sign an arbitrary message using SEP-53 Signs a message following the SEP-53 specification for arbitrary message signing. The provided message will get prefixed with "Stellar Signed Message:\n", hashed with SHA-256, and signed with the ed25519 private key. Example: stellar message sign "Hello, World!" --sign-with-key alice **Usage:** `stellar message sign [OPTIONS] --sign-with-key [MESSAGE]` ###### **Arguments:** - `` — The message to sign. If not provided, reads from stdin. This should **not** include the SEP-53 prefix "Stellar Signed Message:\n", as it will be added automatically ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--base64` — Treat the message as base64-encoded binary data ###### **Signing Options:** - `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path - `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` ## `stellar message verify` Verify a SEP-53 signed message Verifies that a signature was produced by the holder of the private key corresponding to the given account public key, following the SEP-53 specification. The provided message will get prefixed with "Stellar Signed Message:\n" before verification. Example: stellar message verify "Hello, World!" --signature BASE64_SIG --public-key GABC... **Usage:** `stellar message verify [OPTIONS] --signature --public-key [MESSAGE]` ###### **Arguments:** - `` — The message to verify. If not provided, reads from stdin. This should **not** include the SEP-53 prefix "Stellar Signed Message:\n", as it will be added automatically ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--base64` — Treat the message as base64-encoded binary data - `-s`, `--signature ` — The base64-encoded signature to verify - `-p`, `--public-key ` — The public key to verify the signature against. Can be an identity (--public-key alice), a public key (--public-key GDKW...) - `--hd-path ` — If public key identity is a seed phrase use this hd path, default is 0 ## `stellar fee-stats` ⚠️ Deprecated, use `fees stats` instead. Fetch network feestats **Usage:** `stellar fee-stats [OPTIONS]` ###### **Options:** - `--output ` — Format of the output Default value: `text` Possible values: - `text`: Text output of network info - `json`: JSON result of the RPC request - `json-formatted`: Formatted (multiline) JSON output of the RPC request ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar fees` Fetch network feestats and configure CLI fee settings **Usage:** `stellar fees ` ###### **Subcommands:** - `stats` — Fetch the feestats from the network - `use` — Set the default inclusion fee settings for the CLI - `unset` — Remove the default inclusion fee settings for the CLI ## `stellar fees stats` Fetch the feestats from the network **Usage:** `stellar fees stats [OPTIONS]` ###### **Options:** - `--output ` — Format of the output Default value: `text` Possible values: - `text`: Text output of network info - `json`: JSON result of the RPC request - `json-formatted`: Formatted (multiline) JSON output of the RPC request ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar fees use` Set the default inclusion fee settings for the CLI **Usage:** `stellar fees use [OPTIONS] <--amount |--fee-metric >` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings ###### **Options:** - `--amount ` — Set the default inclusion fee amount, in stroops. 1 stroop = 0.0000001 xlm - `--fee-metric ` — Set the default inclusion fee based on a metric from the network's fee stats Possible values: `max`, `min`, `mode`, `p10`, `p20`, `p30`, `p40`, `p50`, `p60`, `p70`, `p80`, `p90`, `p95`, `p99` ###### **RPC Options:** - `--rpc-url ` — RPC server endpoint - `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config ## `stellar fees unset` Remove the default inclusion fee settings for the CLI **Usage:** `stellar fees unset [OPTIONS]` ###### **Global Options:** - `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings --- ## Developer Tools; Build, Test, Deploy Apps & Set Up Ramps with SDKs & APIs # More Developer Tools Check out even more helpful tools created by SDF and the community in this section. --- ## Analytics Platforms There's a wide range of analytics platforms that make exploring Stellar network data easy and fun! The following platforms allow you to easily create aggregates, visualizations, and dashboards to figure out exactly the story you want to tell, with Stellar data.. ### [Artemis](https://app.artemisanalytics.com/project/stellar?from=projects) Artemis allows you to evaluate, compare and analyze trending chains and dApps across various metrics in one terminal. They offer pre-built dashboards and the ability to create your own ### [Dune](https://dune.com) Dune is a web-based platform that allows you to query Stellar network data and aggregate it into beautiful dashboards. ### [DappRadar](https://dappradar.com/chain/stellar) Discover and track dapps on the Stellar network. ### [Untangled OctoPos](https://octopos.untangled.finance) Untangled OctoPos is a real-time portfolio and risk explorer for Stellar (and EVM), tracking major Soroban protocols — Blend, Aquarius, Soroswap, and Phoenix — with priced pool reserves, lending-position health factors, and swap simulation. It also generates AI-driven risk reports and publishes a [public OpenAPI reference](https://octopos.untangled.finance/#/docs/stellar) for programmatic access. --- ## Anchor Tools ### [Anchor Directory](https://anchors.stellar.org/?) View all anchors on Stellar, their currencies, and where they operate. ### [Demo Wallet](https://demo-wallet.stellar.org) An application for interactively testing anchor services. Lets financial application developers test their integrations and learn how Stellar ecosystem protocols (SEPs) work; use the demo wallet to test Regulated Assets (SEP-8), Hosted Deposit and Withdrawal (SEP-24), and Cross-Border Payments (SEP-31) with any home domain that has a Stellar Info File (also known as SEP-1, or a stellar.toml file). ### [Anchor Test Suite](https://github.com/stellar/stellar-anchor-tests/) A test suite for validating SEP6, SEP24, SEP31 transfer servers. --- ## Asset Sandbox ### [Asset Sandbox](https://stellar.cheesecakelabs.com) A sandbox supported by SDF and Cheesecake Labs for businesses to experiment with asset issuance on Stellar's test network. --- ## Block Explorers Block explorers exist to publicly display blockchain data in an easily digestible way. They can be browsed with an ordinary web browser, and do not require any special developer skills to use. The block explorers available for Soroban index data related to payments, accounts, deployed contracts, transaction history, and more. ### [StellarExpert](https://stellar.expert/explorer/public) Explore transactions and network activity with StellarExpert. Check stats specific to an asset code, transaction hash, account address, or ledger sequence number. Not available for Futurenet. ### [StellarChain](https://stellarchain.io) Explore transactions and network activity for Stellar’s networks, including Futurenet. ### [Stellar Explorer](https://steexp.com) Check data related to payments, accounts, deployed contracts, and more for Stellar’s Futurenet, Testnet, and Mainnet. --- ## Jupyter Notebooks Jupyter Notebooks are a document format that can include code, text, and other rich output. And with the appropriate setup they support Soroban Rust contracts. To use Soroban contracts in a Jupyter Notebook, the following setup is required. The following setup uses Visual Studio Code, but any Jupyter client and server can be used. :::caution Rust support in Jupyter Notebooks is experimental. You might run into bugs, or unexpected behavior. ::: ## Getting Started 1. Install [Visual Studio Code] (VSCode) 2. Install the [Jupyter Notebook extension] in VSCode 3. Install the `evcxr` Rust Jupyter kernel with: ``` cargo install --locked evcxr_jupyter evcxr_jupyter --install ``` 4. Run the `Create: New Jupyter Notebook` command in VSCode 5. Click the `Select Kernel` button in the top right 6. Select `Jupyter Kernel...` 7. Select `Rust` by searching for Rust 8. Enter on the first line an import of the `soroban-sdk` dependency with the `testutils` feature enabled. ```rust :dep soroban-sdk = { version = "22.0.7", features = ["testutils"] } ``` 9. Enter a contract. For example: ```rust use soroban_sdk::{contract, contractimpl}; #[contract] pub struct Contract; #[contractimpl] impl Contract { pub fn add(x: u32, y: u32) -> u32 { x+y } } ``` 10. Enter some code to create a Soroban environment, register the contract, and invoke it. ```rust use soroban_sdk::{Env}; let env = Env::default(); let id = env.register(Contract, ()); let client: ContractClient = ContractClient::new(&env, &id); client.add(&1, &2) ``` 11. Click the play button to run the code. Congratulations you have a Jupyter Notebook with contract code that should look something like the screenshot below, ready for hacking and experimenting. ## Screenshot ![A running Jupyter notebook](/assets/dev-tools/jupyter-notebooks.png) ## Community Have ideas for how to improve Soroban contracts in Jupyter Notebooks? Join the community on [Discord]. [Discord]: https://discord.com/channels/897514728459468821/1263811925813366794 [Visual Studio Code]: https://code.visualstudio.com [Jupyter Notebook extension]: https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter [Jupyter Desktop]: https://github.com/jupyterlab/jupyterlab-desktop --- ## Network Insights ### [StellarFee](https://stellarfee.expert) A GUI tool which is a useful fee estimator, transaction simulator to find the resources consumed and the expected fees for a transaction. --- ## Network Status ### [Dashboard](https://dashboard.stellar.org) Displays the current status of the Testnet and Mainnet. Monitor fee stats, recent operations, lumen supply, and more. ### [Obsrvr Radar](https://radar.withobsrvr.com) View Stellar network nodes and visualize consensus. ### [Status Page](https://status.stellar.org) Tracks network incidents and scheduled maintenance for the Testnet and Mainnet. Subscribe to updates to be notified about important events, including protocol upgrades and Testnet resets. --- ## Node Operator Tools ### [GitHub Repository](https://github.com/stellar/go-stellar-sdk/tree/master/tools) A GitHub repository with tools like Stellar Archivist (for Stellar Core archive maintenance) and Horizon cmp (compares responses of two Horizon servers). --- ## Online IDE ### [Soropg](https://soropg.com) Soropg is an online IDE specifically designed for Stellar smart contract development. It provides a browser-based environment where developers can write, compile, and test Stellar smart contracts without needing to set up a local development environment. ### [StellarIDE](https://stellaride.dev) StellarIDE is a professional, browser-native IDE for Soroban smart contract development on Stellar. It provides a full development environment where developers can write, compile, test, and deploy Soroban contracts directly from the browser with zero local setup required. Features include: - Monaco Editor with Rust syntax highlighting - WASM compilation - `cargo test` support - One-click Testnet deployment with Friendbot funding - A built-in AI assistant - Real-time collaboration (coming soon) StellarIDE is free and [open source](https://github.com/Alouzious/StellarIDE). --- ## Security Tools ### [Scout: Bug Fighter](https://www.coinfabrik.com/products/scout) A static code analysis tool built to assist Soroban developers and auditors in identifying potential security threats and applying best practices. ### [Almanax](https://www.almanax.ai) Almanax is an AI security engineer designed to help teams prevent hacks. Almanax uses LLMs to identify complex security vulnerabilities in both smart contracts and conventional code. It also integrates with CI/CD pipelines to automatically flag issues before code reaches production. ### [Certora Sunbeam](https://docs.certora.com/en/latest/docs/sunbeam/index.html) Sunbeam is a formal verification tool developed by Certora for Soroban smart contracts on the Stellar blockchain. Designed specifically for WebAssembly (Wasm) bytecode, Sunbeam verifies the deployed contract code—not just the Rust source—eliminating the need to trust the Rust compiler. Developers write correctness properties using a lightweight spec language embedded in Rust, and Sunbeam rigorously checks that the compiled Wasm upholds those properties. ### [The Soroban Security Portal](https://sorobansecurity.com) The Soroban Security Portal is a security platform developed by Inferara. It is a community-driven solution, based on the constantly maintained database of security audits and vulnerability reports related to Soroban smart contract development. The user experience is enriched with semantic search and other features, enabling smooth search and work with security-related data. The Portal also warps to Stellar ecosystem projects, auditors, tools, and many more. --- ## Wallet Integration ### [Stellar Wallet Kit](https://github.com/Creit-Tech/Stellar-Wallets-Kit) A simple-to-use wallet kit to manage integration to multiple Stellar ecosystem wallet. Learn more about how to integrate this library from [the Stellar Wallets Kit Docs](https://stellarwalletskit.dev). ![Stellar Wallets Kit](/assets/dev-tools/stellar-wallet-kit.gif) The Stellar Wallets Kit supports many wallets including: - Albedo - Freighter - Hana - Ledger Hardware Wallet - Trezor Hardware Wallet - Lobstr - Rabet - WalletConnect - xBull - HOT Wallet ### [Account Viewer](https://accountviewer.stellar.org) A stripped-down wallet where you can check an account’s XLM balance and send simple payments on Testnet and Mainnet. Account Viewer's [source code](https://github.com/stellar/account-viewer-v2) is also useful and instructive for developers building wallet-related features into their applications. ### [Freighter Wallet](https://www.freighter.app) SDF’s flagship non-custodial wallet extension that allows users to sign Stellar transactions via their browser. Freighter wallet's codebase is open sourced here: https://github.com/stellar/freighter. ### [NEAR Intents](https://docs.near.org/chain-abstraction/intents/overview) NEAR Intents is a multichain transaction protocol for wallets that allows users or AI agents to simply state the outcome they want (like swapping Token A for Token B), and then lets a network of off‑chain market makers (called solvers) compete to fulfill that request; once the best solution is selected, it is sent to the user for approval, then execution is verified and executed through a Verifier smart contract on NEAR. Read the [docs](https://docs.near.org/chain-abstraction/intents/overview). ### [Stellar Wallet Sponsorship Calculator](https://docs.google.com/spreadsheets/d/1LgVMFRggdjBAxSpI4nxucUmbZIsnUbMPT8RJQ_rJ5k4/edit?gid=1771515065#gid=1771515065) Estimates the XLM requirements for wallets looking to use sponsored reserves and fee-bump transactions to cover account creation, transaction fees, trustlines, and more. - [Simplified spreadsheet](https://docs.google.com/spreadsheets/d/1Vhk__s-ZrJLEgEs3N2oGEsI35RKPixD5w-qQ6uGYnO0/edit?gid=270060868#gid=270060868) - [Tutorial article and video](https://cheesecakelabs.com/blog/stellar-lumens-xlm-cost-estimator) ### [Blux](https://blux.cc/) Blux is wallet infrastructure built specifically for Stellar dApps, designed to make user onboarding seamless, even for those who have never used a Stellar wallet before. Newcomers can sign in instantly with credentials they already use, while existing Stellar users can connect their preferred wallet, all through a single integration, so no one has to set up a wallet before getting started. ![Blux](/assets/dev-tools/blux_demo.gif) Beyond traditional wallets, Blux lets users sign in with: - **Email** - **Social accounts**, including Google, Discord, GitHub, X, LinkedIn, and more - **Passkeys** Blux supports a broad range of Stellar ecosystem wallets: - Freighter - Rabet - WalletConnect - HOT Wallet - Hana - xBull - Lobstr - Albedo - Bitget - OneKey - Ledger Hardware Wallet - Trezor Hardware Wallet - and more... For developers, Blux ships with a comprehensive set of pre-built hooks and modals covering transaction signing, balance checking, swaps, transaction history, and on/off-ramp flows. Its SDKs offer type-safe, TypeScript-first APIs for both React and vanilla JavaScript, and the fully configurable modal system can be tailored to match each dApp's branding. You can also track and review user analytics directly from the Blux dashboard. Learn more in the [Blux documentation](https://docs.blux.cc/). ### [Dfns](https://www.dfns.co/) A wallet-as-a-service platform that streamlines digital asset operations, offering full wallet feature support, including automatic detection of asset and NFT balances (when applicable), on-chain transfer history, asset transfers, transaction broadcasting, and signature generation. View the [docs](https://docs.dfns.co/d/api-docs/wallets/broadcast-transaction/stellar). ### [Privy](https://docs.privy.io/wallets/overview/chains) Wallet infrastructure that enables users to programmatically create a new wallet within their applications. Follow this [guide](https://docs.privy.io/wallets/wallets/create/create-a-wallet) to create a Stellar wallet. --- ## Infrastructure Tools A list of network and infrastructure tools for Stellar. --- ## Cross-Chain Bridges and messaging layers that connect different blockchains. ## [Axelar](https://www.axelar.network) Axelar Network is the universal interoperability network that securely connects all blockchain ecosystems, applications, assets, and users. Get set up to write cross-chain smart contracts on Stellar using Axelar in the following guides. ### Learn about the Stellar GMP contracts General Message Passing (GMP) is a cross-chain communication protocol that allows smart contracts on different blockchains to communicate with each other. With GMP, Stellar contracts can: - Send messages to contracts on other chains like Ethereum, Avalanche, Base, Polygon, etc. - Receive and process messages from contracts on other chains. - Execute cross-chain operations securely and efficiently. [Dive into Stellar GMP documentation →](https://docs.axelar.dev/dev/general-message-passing/stellar-gmp/intro) ### Learn about the Stellar Interchain Token Service (ITS) The Interchain Token Service (ITS) enables tokens to scale across multiple chains by supporting both existing and newly minted tokens, preserving native-like fungibility and functionality on connected EVM chains, and automating deployment and maintenance to help teams easily manage supply on an open, scalable, and secure network. For the Stellar ecosystem, ITS offers: - Creation of new tokens that can exist on multiple blockchains. - Connecting existing Stellar tokens to other blockchain ecosystems. - Secure transfers of tokens between Stellar and other chains. [Learn how to use Axelar's ITS with Stellar →](https://docs.axelar.dev/dev/send-tokens/stellar/intro) --- ## Lab ### [Stellar Lab](https://lab.stellar.org) Stellar Lab is our go-to tool for development, experimenting, and testing, as well as [exploring APIs](https://lab.stellar.org/endpoints) developers use to interact with the Stellar network. Whether you're a developer seeking to [test transactions](https://lab.stellar.org/transaction/build), [deploy](https://lab.stellar.org/smart-contracts/deploy-contract) and [invoke smart contracts](https://lab.stellar.org/smart-contracts/contract-explorer), or explore RPC methods or Horizon endpoints, Stellar Lab provides a modern and user-friendly interface that makes the process smooth and intuitive. ![Lab: Homepage](/assets/lab/lab-01232026.png) ### Features of Stellar Lab - **[Easily Create Accounts](https://lab.stellar.org/account/create)**: Create Accounts on Mainnet, Testnet, and Futurenet using a web UI. You can use [Friendbot](https://lab.stellar.org/account/fund?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;;) to fund those accounts directly on Lab for Testnet and Futurenet. You can also add a trustline to USDC and EURC on Testnet. - **[Smart Contract Deployment](https://lab.stellar.org/smart-contracts/deploy-contract)**: Deploy smart contracts directly from the Lab interface without needing command-line tools. Upload your contract WASM files and deploy them to the network with a user-friendly workflow. - **[Invoke Smart Contracts](https://lab.stellar.org/smart-contracts/contract-explorer)**: Interact with deployed smart contracts through an intuitive interface. Select contract functions, fill in parameters with type hints, simulate transactions, and invoke contract methods — all from your browser. - **[Transaction Dashboard](https://lab.stellar.org/transaction-dashboard)**: View comprehensive transaction details including XDR, operations, results, and metadata. Explore transaction history, examine individual operations, and debug transaction failures with detailed error information. - **[Access RPC Methods and Horizon Endpoints](https://lab.stellar.org/endpoints)**: Leverage powerful Stellar RPC methods and Stellar Horizon endpoints in a web UI to interact with the Stellar network and obtain crucial data. Try RPC methods to get states from the ledger like accounts, trustlines, contract WASM, and more. - **[Simulate](https://lab.stellar.org/transaction/simulate) and [Submit](https://lab.stellar.org/transaction/submit)**: Lab supports the ability to use custom RPC providers so that you can simulate transactions, save transactions, and submit transactions directly using Lab. You can also submit transactions using Horizon. - **[Save and Share API Requests](https://lab.stellar.org/endpoints/saved)**: Easily save your requests and transactions for the future or share them with teammates to build faster together. - **[XDR ⇔ JSON Support](https://lab.stellar.org/xdr/view)**: We have introduced a canonical [XDR to JSON mapping](https://www.npmjs.com/package/@stellar/stellar-xdr-json-web) which is used on Stellar Lab (also used in the [Stellar CLI](https://github.com/stellar/stellar-cli)). You can see this at work in the Lab where [XDR can be converted to JSON](https://lab.stellar.org/xdr/view). - **Mobile Friendly**: We heard that you use Lab on mobile, so we optimized the Lab to have a mobile responsive layout. ### Video Tutorial for Stellar Lab - Video Tutorials for Lab are available on [Lumen Loop's YouTube Channel](https://www.youtube.com/watch?v=sv2gV13q7FI&list=PLj3KReXFn3x73pPlgSun-2FYTrP4tZVPL) ### Help Us Improve! We’re committed to making Stellar Lab even better. If you have any feature requests, please submit them on [Github](https://github.com/stellar/laboratory/issues). Your feedback is important to us with product iterations and in shaping the future of Stellar Lab. --- ## Account ## [Create Account Keypair](https://lab.stellar.org/account/create) ![Lab: Create Account](/assets/lab/lab-keypair-01232026.png) From the Lab’s main navigation, click on the "Account" link to expand sub-navigation. Click the "Create Account Keypair" page. Here you can generate a [keypair](../../learn/glossary.mdx#keypair) by following these steps: 1. Make sure you are on the Stellar network for which you want to generate the keypair. You can see the current network in the upper right corner of the page. Click on dropdown to change the network (don’t forget to provide any necessary information). We will use Testnet network for this example. 2. Click the "Generate keypair" button to create public and secret keys for an account. Save them someplace safe, even if they are used only for testing. :::warning Anyone with the account’s secret key has full access and control over the account. Keep the secret key safe. ::: 3. An account is active only once it is funded. You can use Friendbot to fund it on Testnet and Futurenet networks only. Click on the "Fund account with Friendbot" button to add 10,000 XLM to the account. When the operation completes, a message will be displayed at the bottom. :::info The Friendbot can be used for new account or accounts with balance under a starting balance of 10,000 XLM. Click the "Fund account with Friendbot" button again to see what happens. It’s the Lab, so feel free to experiment (as long as you are on Testnet or Futurenet network)! ::: 4. Optionally, to save the generated keypair, click the "Save Keypair" button. Enter the name in the pop-up and click the "Save" button to save the keypair in the browser's local storage. Click the "Saved" link on the main menu to expand the submenu, then click on the "[Keypairs](./saved/keypairs)" link to view saved keypairs. The save feature is available only on Testnet and Futurenet networks. ## [Fund Account](https://lab.stellar.org/account/fund) ![Lab: Fund Account](/assets/lab/lab-fund-01232026.png) If you already have a keypair you want to fund, go to the "Fund Account" page (under the "Account" item in the main menu). 1. If you have generated a keypair on the "Create Account Keypair" page, you can use its public key by clicking the "Fill in the generated key" button. The button will be disabled if there is no keypair. 2. You can always manually input a public key. 3. Once you enter the public key, click the "Get lumens" button to fund the account with 10,000 XLM on the Testnet or Futurenet network. 4. You will see a response message once the operation is completed. 5. Once an account is funded with 10,000 XLM, you can create a trustline with USDC or EURC to interact with these assets. :::info You can also create an account using a Stellar SDK. Follow a guide [here](../../build/guides/transactions/create-account.mdx). ::: --- ## API Explorer # Developer Tools This section demonstrates how to use RPC methods and Horizon endpoints on the Stellar Lab across different networks, including features like sharing URLs and saving requests for future use. You can explore the various endpoints from the RPC and Horizon, make requests to these endpoints, and save them for future use. --- ## Horizon Endpoints :::warning Horizon is nearing end-of-life and will eventually be deprecated in favor of Stellar RPC and [Portfolio APIs](../../../data/indexers/README.mdx#portfolio-apis). While it will continue to receive updates to maintain compatibility with upcoming protocol releases, it won't receive new feature development. Consider switching to [Stellar RPC or another data product](../../../data/apis/README.mdx). ::: Horizon provides an HTTP API to access data stored on the Stellar network. This API acts as a bridge between applications and Stellar Core. Stellar Lab's [API Explorer](https://lab.stellar.org/endpoints) offers a Horizon Endpoints UI, enabling developers to interact with Horizon on Futurenet, Testnet, Mainnet, and custom networks. Developers can use Stellar Lab to build and submit transactions, query account balances, and stream events such as transactions for an account. Data on the Stellar ledger is organized into [resources](../../../data/apis/horizon/api-reference/resources): **Accounts, Assets, Claimable Balances, Effects, Ledgers, Liquidity Pools, Offers, Operations, Trades**, and **Transactions**. Each resource has multiple endpoints. Endpoints that aggregate ledger data (also known as [aggregations](../../../data/apis/horizon/api-reference/aggregations)) include resources like **Order Books, Paths, Trade Aggregations**, and **Fee Stats**. Stellar Lab's Horizon Endpoints lists each resource type in the menu, allowing developers to interact with its respective endpoints. ![Lab: Horizon Page](/assets/lab/horizon-endpoints.png) :::info For detailed information about each endpoint on a page, click the **View Docs** link in the header to access its API Reference documentation. ::: ![Lab: Horizon - View Docs](/assets/lab/lab-horizon-view-docs.png) ## Examples of Horizon Endpoints Let's explore a few examples of Horizon endpoints on Testnet and Mainnet. We also have a [documentation on how to set up a local Stellar network environment](../quickstart-with-lab) if you are interested in using a local environment. This will demonstrate how to use the Lab to access these endpoints. - [Single Account](#single-account): `/accounts/:account-id` - [Payments for Account](#payments-for-account): `/accounts/:account-id/payments` - [All Assets](#all-assets): `/assets` ### [Single Account](https://lab.stellar.org/endpoints/accounts/single) The single account endpoint `/accounts/:account-id` provides information on a specific account. This example uses an account `GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F` on Testnet. ![Lab: Horizon - Single Account](/assets/lab/lab-single-account.png) When I click the **Submit** button, the account endpoint returns the account details in `JSON` format. Developers can use the **Copy Json** functionality to copy and paste the response. Along with the account details such as `id`, `balances` of the account including the assets that the account has set its trustline to, `thresholds`, `flags`, and `signers` (3 signers in the example account). The endpoint also provides links to related resource pages on the Stellar Lab for the queried account. For example, the response includes `transactions`, `operations`, `payments`, and more under `_links`. Clicking a transaction's `href` takes developers to the [Transactions for Account page](https://lab.stellar.org/endpoints/transactions/account?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$account_id=GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F;;) page on the Stellar Lab where developers can query transactions for that account right away. ![Lab: Horizon - Single Account Response](/assets/lab/lab-single-account-response.png) ```{ "_links": { "self": { "href": "https://horizon-testnet.stellar.org/accounts/GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F" }, "transactions": { "href": "https://horizon-testnet.stellar.org/accounts/GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F/transactions{?cursor,limit,order}", "templated": true }, "operations": { "href": "https://horizon-testnet.stellar.org/accounts/GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F/operations{?cursor,limit,order}", "templated": true }, "payments": { "href": "https://horizon-testnet.stellar.org/accounts/GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F/payments{?cursor,limit,order}", "templated": true }, "effects": { "href": "https://horizon-testnet.stellar.org/accounts/GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F/effects{?cursor,limit,order}", "templated": true }, "offers": { "href": "https://horizon-testnet.stellar.org/accounts/GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F/offers{?cursor,limit,order}", "templated": true }, "trades": { "href": "https://horizon-testnet.stellar.org/accounts/GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F/trades{?cursor,limit,order}", "templated": true }, "data": { "href": "https://horizon-testnet.stellar.org/accounts/GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F/data/{key}", "templated": true } }, "id": "GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F", "account_id": "GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F", "sequence": "4113457683103748", "sequence_ledger": 958608, "sequence_time": "1731620848", "subentry_count": 3, "last_modified_ledger": 958608, "last_modified_time": "2024-11-14T21:47:28Z", "thresholds": { "low_threshold": 5, "med_threshold": 5, "high_threshold": 5 }, "flags": { "auth_required": false, "auth_revocable": false, "auth_immutable": false, "auth_clawback_enabled": false }, "balances": [ { "balance": "0.0000000", "limit": "922337203685.4775807", "buying_liabilities": "0.0000000", "selling_liabilities": "0.0000000", "last_modified_ledger": 958608, "is_authorized": true, "is_authorized_to_maintain_liabilities": true, "asset_type": "credit_alphanum4", "asset_code": "USDC", "asset_issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, { "balance": "9990.9999400", "buying_liabilities": "0.0000000", "selling_liabilities": "0.0000000", "asset_type": "native" } ], "signers": [ { "weight": 2, "key": "GAGOOY3NKKNEXDOVKTKIP2AUCT6ZOQD4SMWAB54TSHTAXATLJSVVDJCZ", "type": "ed25519_public_key" }, { "weight": 1, "key": "GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F", "type": "ed25519_public_key" }, { "weight": 2, "key": "GDFHN4ILDFEBM5YHW6MYOQIRNY3EQ7VV72IW3YIAZNXDW363SUAK7BBL", "type": "ed25519_public_key" } ], "data": {}, "num_sponsoring": 0, "num_sponsored": 0, "paging_token": "GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F" } ``` ### [Payments for Account](https://lab.stellar.org/endpoints/payments/account) The Payments for Account endpoint `/accounts/:account-id/payments` provides successful payments for a given account and can be used in streaming mode. We're going to use the same account that we used from the previous example: `GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F`. ![Lab: Horizon - Payments](/assets/lab/horizon-payments.png) When I click the **Submit** button, the endpoint returns the records of payments (see the [api reference](../../../data/apis/horizon/api-reference/get-payments-by-account-id.api.mdx) for more information on the response format) including the [payment object](../../../data/apis/horizon/api-reference/resources/payments/object.mdx) that were submitted successfully in the account in `JSON` format. ![Lab: Horizon - Payments Response](/assets/lab/horizon-payments-response.png) ``` { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/accounts/GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F/payments?cursor=&limit=10&order=asc" }, "next": { "href": "https://horizon-testnet.stellar.org/accounts/GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F/payments?cursor=4115390418391041&limit=10&order=asc" }, "prev": { "href": "https://horizon-testnet.stellar.org/accounts/GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F/payments?cursor=4113457683111937&limit=10&order=desc" } }, "_embedded": { "records": [ { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/4113457683111937" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/b86ad0433bd024d37739b803529e77601bceb83976d6ae328a61f8f0f8300cb6" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/4113457683111937/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=4113457683111937" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=4113457683111937" } }, "id": "4113457683111937", "paging_token": "4113457683111937", "transaction_successful": true, "source_account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", "type": "create_account", "type_i": 0, "created_at": "2024-11-14T20:34:58Z", "transaction_hash": "b86ad0433bd024d37739b803529e77601bceb83976d6ae328a61f8f0f8300cb6", "starting_balance": "10000.0000000", "funder": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", "account": "GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F" }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/4113586532130817" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/bbc6b423efb9c5a8ebb1ac72437a326ffd4b8a202782157f9a9bca38f742f7d7" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/4113586532130817/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=4113586532130817" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=4113586532130817" } }, "id": "4113586532130817", "paging_token": "4113586532130817", "transaction_successful": true, "source_account": "GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F", "type": "payment", "type_i": 1, "created_at": "2024-11-14T20:37:28Z", "transaction_hash": "bbc6b423efb9c5a8ebb1ac72437a326ffd4b8a202782157f9a9bca38f742f7d7", "asset_type": "native", "from": "GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F", "to": "GDE25LQ34AFCSDMYTOI6AVVEHRXFRJI4MOAVIUGUDUQEC5ZWN5OZDLAZ", "amount": "4.0000000" }, { "_links": { "self": { "href": "https://horizon-testnet.stellar.org/operations/4115390418391041" }, "transaction": { "href": "https://horizon-testnet.stellar.org/transactions/6de546bd7702f1590f21928e6aa1329582b436f11d9fc9bcce304ea51f1893b0" }, "effects": { "href": "https://horizon-testnet.stellar.org/operations/4115390418391041/effects" }, "succeeds": { "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=4115390418391041" }, "precedes": { "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=4115390418391041" } }, "id": "4115390418391041", "paging_token": "4115390418391041", "transaction_successful": true, "source_account": "GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F", "type": "payment", "type_i": 1, "created_at": "2024-11-14T21:12:31Z", "transaction_hash": "6de546bd7702f1590f21928e6aa1329582b436f11d9fc9bcce304ea51f1893b0", "asset_type": "native", "from": "GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F", "to": "GCNJ2TA24PJNDPE2JF3BYVLUTJX2GLRDGGBG4SYMQHKHYDHSMAPRZXWC", "amount": "5.0000000" } ] } } ``` ### [All Assets](https://lab.stellar.org/endpoints/assets) Horizon's `/assets` endpoint provides information about an asset. Since we are not interested in historical data, we will use SDF's Mainnet Horizon to retrieve the latest information about the asset. ![Lab: Horizon - Assets Page](/assets/lab/horizon-assets.png) Let's inquire about one of the most popular assets on the Stellar network: [USDC by Circle](https://www.circle.com/usdc). To do this, enter `USDC` as the Asset Code and `GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN` as the Asset Issuer. ![Lab: Horizon - Assets Page](/assets/lab/horizon-assets-usdc.png) When I click the **Submit** button, the `/assets` endpoint returns the `USDC` asset details in `JSON` format. ![Lab: Horizon - Assets Page](/assets/lab/horizon-assets-usdc-response.png) The JSON response for the `USDC` asset includes [the asset object](../../../data/apis/horizon/api-reference/resources/assets/object.mdx), which contains details about USDC, such as the asset's TOML file (also known as [the Stellar info file](../../../tokens/publishing-asset-info.mdx)), the number of claimable balances, liquidity pools, contracts, accounts, balances, and flags. It shows that `auth_revocable` is set to `true` under `flags`, indicating that this account (in this case, the `USDC` issuer) can freeze the balance of a holder of an asset it has issued. ``` { "_links": { "self": { "href": "https://horizon.stellar.org/assets?asset_code=USDC&asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN&cursor=&limit=10&order=asc" }, "next": { "href": "https://horizon.stellar.org/assets?asset_code=USDC&asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN&cursor=USDC_GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN_credit_alphanum4&limit=10&order=asc" }, "prev": { "href": "https://horizon.stellar.org/assets?asset_code=USDC&asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN&cursor=USDC_GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN_credit_alphanum4&limit=10&order=desc" } }, "_embedded": { "records": [ { "_links": { "toml": { "href": "https://centre.io/.well-known/stellar.toml" } }, "asset_type": "credit_alphanum4", "asset_code": "USDC", "asset_issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", "paging_token": "USDC_GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN_credit_alphanum4", "contract_id": "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75", "num_claimable_balances": 449, "num_liquidity_pools": 645, "num_contracts": 388, "num_archived_contracts": 18, "accounts": { "authorized": 1058053, "authorized_to_maintain_liabilities": 0, "unauthorized": 0 }, "claimable_balances_amount": "8567.4304807", "liquidity_pools_amount": "6154162.8631633", "contracts_amount": "2734426.6007745", "archived_contracts_amount": "26.3776599", "balances": { "authorized": "121258232.6884846", "authorized_to_maintain_liabilities": "0.0000000", "unauthorized": "0.0000000" }, "flags": { "auth_required": false, "auth_revocable": true, "auth_immutable": false, "auth_clawback_enabled": false } } ] } } ``` ## Share Feature The Stellar Lab offers a share feature that allows you to copy a URL to its page, which redirects users to a form with the endpoint URL and parameter values pre-filled. This feature makes it easy to collaborate with others. For example, clicking the share icon on the Payments for Account page with the account ID `GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F`, copies a URL that includes the account ID parameter and network setting. An example of a copied URL is [this link](https://lab.stellar.org/endpoints/payments/account?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$account_id=GBPIMUEJFYS7RT23QO2ACH2JMKGXLXZI4E5ACBSQMF32RKZ5H3SVNL5F;;). ![Lab: Horizon - Share Feature](/assets/lab/horizon-share.png) ## Saved Requests The icon next to the Share Feature icon allows you to save an endpoint with its parameters in your browser's local storage. It displays the endpoint for the selected network, which can be changed using the menu in the upper-right corner. ![Lab: Horizon - Save](/assets/lab/horizon-save.png) Clicking the icon opens a modal. We recommend providing an identifiable name to make it easier to search for later. ![Lab: Horizon - Modal](/assets/lab/horizon-save-modal.png) Saved endpoints are located in the main menu under the "Saved" menu, "[Requests](../saved/requests.mdx)" submenu. --- ## RPC Methods RPC methods are a set of functions that allow developers to interact directly with the Stellar network. You can learn more about all the RPC methods [here](../../../data/apis/rpc/api-reference/methods). The Lab provides an easy way to use these methods from the UI. To access the "RPC Methods" items, click on the "API Explorer" link in the main navigation, then click the "RPC Methods" submenu link, which will open another submenu listing all RPC methods by name. Click on the method name to open that page. :::warning RPC URL is required to submit these methods. You can update or set the RPC URL in the network selector in the top right corner. ::: ## Example We’ll select the `getLedgerEntries` method as an example. At the top of the page is a "View Docs" link to learn more about this method. The read-only input field shows the RPC URL (you’ll see a warning to provide the RPC URL if the input is empty). Next to the input is a Submit button, which will be disabled until all the required data is filled and valid. The Share and Save buttons are next to the Submit button. More about them below. There are two ways to input the Ledger Key: XDR Base64 string or manual inputs. You can change the input mode by clicking the "Switch input" button. Here’s an example when inputting XDR string `AAAABgAAAAHMA/50/Q+w3Ni8UXWm/trxFBfAfl6De5kFttaMT0/ACwAAABAAAAABAAAAAgAAAA8AAAAHQ291bnRlcgAAAAASAAAAAAAAAAAg4dbAxsGAGICfBG3iT2cKGYQ6hK4sJWzZ6or1C5v6GAAAAAE=`. Manual inputs are generated as read-only (XDR string would be generated if the manual input mode was selected and correctly filled). ![Lab: getLedgerEntries example](/assets/lab/rpc-methods-item.png) You can add multiple ledger keys by clicking the "Add another ledger key" button. You can change the response format in the "XDR Format" dropdown: `base64` (XDR) or `json` (more human-readable). The "Payload" section generates a preview of the payload that will be submitted. To submit the request, click the Submit button at the top (it should be enabled if all the fields are correct). Once the request is processed, the response will be displayed at the bottom of the page. ## Share Feature The Stellar Lab allows you to share RPC methods with filled data. Simply click on the Share button (next to the Submit button) to copy the link to share. [Here](https://lab.stellar.org/endpoints/rpc/get-ledger-entries?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$xdrFormat=base64&ledgerKeyEntries=%5B%22AAAABgAAAAHMA//50//Q+w3Ni8UXWm//trxFBfAfl6De5kFttaMT0//ACwAAABAAAAABAAAAAgAAAA8AAAAHQ291bnRlcgAAAAASAAAAAAAAAAAg4dbAxsGAGICfBG3iT2cKGYQ6hK4sJWzZ6or1C5v6GAAAAAE=%22%5D;;) is an example of the shareable link from the above. ## Saved Requests You can save methods in the browser’s local storage by clicking the Save button (next to the Share button). Once you click that button, a modal will pop up, where you will need to enter the name and click save. To view your saved RPC methods in the Lab, click the "Saved" link in the main menu, then click the "[Requests](../saved/requests.mdx)" submenu link. Then click the "RPC Methods" tab in the top right. --- ## Using Lab with Quickstart ## Overview [Quickstart](https://github.com/stellar/quickstart) provides an easy way to set up a local Stellar network environment. Specifically, Quickstart docker image bundles Stellar Core with Horizon, RPC, Friendbot, and the necessary PostgreSQL databases. Now it is possible to use Stellar Lab as an interface on Quickstart. ## Prerequisites - [Stellar CLI](../cli/install-cli.mdx) - [Docker](https://www.docker.com) ## Start Quickstart Quickstart can be started for different networks. In this example, we will start Quickstart for `testnet`. Start Quickstart with the Stellar CLI using the following command: ```sh stellar container start testnet ``` Quickstart will usually start on `http://localhost:8000`. With this information, we can now configure Stellar Lab to use the local network. ## Configure Stellar Lab to use Local Horizon and Local RPC ![Lab: Network selector](/assets/lab/lab-custom-network.png) 1. Open Stellar Lab network selector on the upper right hand corner. 2. Select `Custom` network in the dropdown. 3. For the RPC URL, input `http://localhost:8000/rpc`. 4. For the Horizon URL, input `http://localhost:8000.`. 5. For the [network passphrase](../../networks/README.mdx), input `Test SDF Network ; September 2015` 6. Click the button `Switch to Custom Network` and switched to using your Quickstart. You should be able to use Horizon endpoints, RPC endpoints, Friendbot on Lab, with requests now send to your local environment (Quickstart). Lab gives you a user friendly interface to interact with you local environment. --- ## Saved View keypairs, requests, and transactions saved in local storage. --- ## Keypairs # [Keypairs](https://lab.stellar.org/account/saved) :::important You can only save keypairs on test networks—never on Mainnet—and you should never reuse Mainnet keypairs on test networks or share your secret keys with anyone. ::: ## Overview ![Lab: Saved Keypairs](/assets/lab/lab-account-saved.png) This page shows your saved keypairs for the selected network (only Testnet and Futurenet) in the browser's local storage. Saved keypairs are obfuscated, but _not_ encrypted — treat any secret key saved here as recoverable by anyone (or any script) with access to your browser (and note that keypairs saved before September 2025 may remain in plain text until they're re-saved). Every keypair has the following: 1. Name - makes it easy to find the keypair you're looking for. You can update the name anytime by clicking the edit button and saving the new name. 2. Public key - the public account address. You can quickly copy it with a click of a button. 3. Secret key - the secret key of the account. Click the Eye button to toggle between masked and clear text format. Like the Public key, you can copy it by clicking the Copy button. 4. Recovery phrase - the 12 or 24-word passphrase. Click the Eye button to toggle between masked and clear text format. Like the Public key, you can copy it by clicking the Copy button. 5. Delete button - click this button to delete the keypair. 6. Delete the saved keypair if it's no longer needed. 7. The last saved date and time. 8. XLM balance if the account is funded. 9. If the account is unfunded, you can get 10,000 XLM by clicking the "Fund with Friendbot" button. This might be useful after the Testnet or Futurenet reset, as all your saved accounts will also be reset. ## Manually save keypair To add a keypair manually, click the "Add keypair manually" button located in the top right corner of the screen. This will open a modal where you can enter a name for your keypair along with the [Stellar secret key](../../../learn/glossary.mdx#secret-private-key) (which begins with the letter "S") or recovery passphrase (a 12 or 24-word phrase). ![Lab: Add keypair manually](/assets/lab/lab-account-saved-manual.png) ## Using saved keypairs Saved keypairs make it easy to work with Stellar accounts and sign transactions. When building a transaction, click the "Get address" button in the Source Account input and choose your desired account from the dropdown. ![Lab: Use saved public key](/assets/lab/lab-account-saved-usage-public.png) When it's time to sign, head to the Signatures section on the Sign Transaction page. If you don't see it, import your transaction XDR first. Then click "Use secret key" in the "Sign with secret key" section and select the account key you'd like to use. ![Lab: Use saved secret key](/assets/lab/lab-account-saved-usage-secret.png) --- ## Requests # [Requests](https://lab.stellar.org/endpoints/saved) On this page, you can view saved requests for both the **RPC Methods** and **Horizon Endpoints**. Select the tab you want to view in the top right corner. :::info Only items for the selected network are shown. If you don’t see the item you’re looking for, try changing the network. ::: ## RPC Methods ![Lab: Saved RPC Methods](/assets/lab/lab-rpc-methods-saved.png) Every saved RPC request item has the following: 1. RPC method name - quickly identify the method you need. 2. Name - makes finding the RPC method you’re looking for easy. You can update the name anytime by clicking the edit button and saving the new name. 3. RPC URL - where to submit the request. 4. Share the URL to view the method with all the data in the Lab. 5. View the item in the API Explorer, where you can submit it. 6. View payload. 7. The last saved date and time. 8. Delete the item if it’s no longer needed. ## Horizon Endpoints ![Lab: Horizon - Saved Requests Page](/assets/lab/lab-horizon-saved-requests.png) Saved Horizon Endpoints have the following: 1. Name - You can update the name anytime by clicking the edit button and saving the new name. 2. Horizon URL - endpoint to fetch the data from. 3. Share the URL to view the endpoint with all the data in the Lab. 4. View the item in the API Explorer, where you can submit it. 5. The last saved date and time. 6. Delete the item if it’s no longer needed. --- ## Transactions(Saved) # [Transactions](https://lab.stellar.org/transaction/saved) ![Lab: Saved Transactions](/assets/lab/lab-transactions-saved.png) On this page, you'll see transactions saved in your browser's local storage. It only shows the transactions on the selected network, which you can change in the upper right corner. Saved transactions have the following: 1. Name - makes finding the transaction you're looking for easy. You can update the name anytime by clicking the edit button and saving the new name. 2. Operations - list of operations in this transaction. 3. Delete the saved transaction. 4. The last saved date and time. 5. The share button to get a shareable link to this transaction. 6. Transactions saved on the "Transaction Builder" page have the "View in builder" button to take you to the "Build Transaction" page. 7. Transactions saved on the "Submit Transaction" page have the "View in submitter" button to take you to that page. --- ## Smart Contracts(Smart-contracts) Stellar Lab provides a comprehensive suite of tools for exploring, testing, and interacting with smart contracts on the Stellar network. These tools are designed to help developers understand contract behavior, debug issues, and test contract interactions without writing additional code. :::tip If you don't know any Stellar smart contract ID, view [Smart Contract List](https://lab.stellar.org/smart-contracts/contract-list) and click on any of the items. This will direct you to the "Contract Explorer" page, where the selected ID is pre-filled. Load the contract and check it out! ::: ### Tips for New Developers - **Start with Testnet**: Always begin your development and testing on Testnet where XLM is free and mistakes have no real-world consequences - **Examine Example Contracts**: Study well-known contracts to understand common patterns and best practices - **Save Your Configurations**: Type in the Contract ID you want to explore on [Contract Explorer](https://lab.stellar.org/smart-contracts/contract-explorer), it will give you an option to save for quick access via 'Save Contract ID' - **Check Network Status**: Ensure you're connected to the correct network ## Additional Resources - [Smart Contracts Documentation](../../../build/smart-contracts/README.mdx) - [Soroban CLI Tools](../../cli/README.mdx) - [Example Contracts](../../../build/smart-contracts/example-contracts/README.mdx) --- ## Contract Explorer The [Contract Explorer](https://lab.stellar.org/smart-contracts/contract-explorer) is your primary interface for examining deployed smart contracts on the Stellar network. Through an intuitive browser-based interface, you can access comprehensive insights into [contract specifications](#contract-spec), [source code](#source-code), [storage state](#contract-storage), [build information](#build-info), [version history](#version-history), and [client bindings](#bindings) — plus [invoke contract](#invoke-contract) methods directly without any command-line tools. :::warning RPC URL is required to view the contract information. You can update or set the RPC URL in the network selector in the top right corner. See [RPC Providers](../../../data/apis/rpc/providers.mdx). ::: ![Lab: Contract Explorer](/assets/lab/lab-contract-explorer.png) ## Getting Started To explore a contract: 1. Navigate to the [Contract Explorer](https://lab.stellar.org/smart-contracts/contract-explorer) 2. Select your network `Testnet` or `Mainnet` 3. Enter a contract ID 4. Browse through the tabs to familiarize yourself with the contract's structure and capabilities Let's load `CAJJZSGMMM3PD7N33TAPHGBUGTB43OC73HVIK2L2G6BNGGGYOSSYBXBD` contract ID on the `Mainnet` network, and see what we can learn about it. ## Contract Info In the "Contract Info" section, we can view the date when the contract was created and the creator's Stellar address. It also displays the Wasm hash and GitHub repository, along with a link to the source code of this contract. Also, you can see how many contract storage entries this contract has. ![Lab: Contract Explorer: Contract Info](/assets/lab/lab-contract-explorer-info.png) ## Contract Spec In this section, you can view [Contract Meta](../../../learn/fundamentals/contract-development/overview#contract-meta), [Contract Env Meta](../../../learn/fundamentals/contract-development/overview#environment-meta), and [Contract Spec](../../../learn/fundamentals/contract-development/overview#contract-spec) from the Wasm file. You can view each section in `Interface`, `JSON` or `XDR` formats, by selecting the type from the dropdown in the top-right corner of the editor view. You can also download each section individually in the chosen format (the download button is located next to the type dropdown), or download the entire Wasm file by clicking the "Download Wasm" button at the bottom of the section. ![Lab: Contract Explorer: Contract Spec](/assets/lab/lab-contract-explorer-spec.png) ## Source Code In the code editor, you can view the `README.md` file of the smart contract repository. You can further explore the code by opening it in a Dev Container, Codeanywhere, or by using the GitHub link (see the dropdown in the top right corner of the editor view). ![Lab: Contract Explorer: Source Code](/assets/lab/lab-contract-explorer-source.png) ## Contract Storage This section displays all stored data entries for the smart contract in a human-readable format. The data can be filtered by `Key` and `Value`, sorted by `Durability`, `TTL`, or `Updated` columns, and exported in either `XDR` or `JSON` formats. ![Lab: Contract Explorer: Contract Storage](/assets/lab/lab-contract-explorer-storage.png) ### Restore Footprint Archived storage entries can be restored by clicking the `Restore` button next to the entry. ![Lab: Contract Explorer: Contract Storage Restore](/assets/lab/stellar-lab-restore.png) This action redirects you to the [Build Transactions page](https://lab.stellar.org/transaction/build) with a `RestoreFootprintOp` operation automatically configured with the necessary parameters to restore the archived footprint. ![Lab: Contract Explorer: Contract Storage Restore in action](/assets/lab/restore-footprint.gif) :::tip Verify that the prepared Soroban Transaction XDR includes the contract data you want to restore in its `footprint.read_write` resources, and confirm that both read and write bytes are non-zero. ::: ## Build Info If the smart contract has build verification configured (following the [Contract Source Validation SEP](https://github.com/orgs/stellar/discussions/1573)), this section displays information from the GitHub attestation. :::info "Build Verified" only means that the GitHub Action run has attested to have built the Wasm, but does not verify the source code. Always make sure you fully understand what the smart contract does before using it. ::: ![Lab: Contract Explorer: Build Info](/assets/lab/lab-contract-explorer-build.png) ## Version History In this section, you can find the Wasm history of changes. ![Lab: Contract Explorer: Version History](/assets/lab/lab-contract-explorer-version.png) ## Bindings Bindings are a feature of the [Stellar CLI](../../cli) that generate fully typed client libraries for your smart contracts, tailored to your chosen programming language, including TypeScript, JSON, Rust, Python, and Java. This makes it easy to integrate Stellar contracts into your application as if they were native modules. Each binding provides type-safe functions corresponding to your contract’s methods. To learn more about generating bindings, please see the [Stellar CLI’s bindings command](../../cli/stellar-cli#stellar-contract-bindings). ## Invoke Contract The Invoke Contract page lets you interact with smart contracts directly through the web interface—no command-line tools or custom scripts required. :::warning A connected wallet is required to invoke the contract. ::: ### How to Use 1. **Connect Your Wallet**: Choose to connect a browser wallet by clicking "Connect Wallet" in the top right corner. 2. **Select the network**: `Testnet` or `Mainnet` in the top right corner. 3. **Enter Contract Details**: Provide the contract ID 4. **Select a Function**: Choose which contract function you want to invoke from the available options. 5. **Fill in Parameters**: Enter the required parameters for your selected function. Lab provides type hints based on the [Contract Spec](#contract-spec). 6. **Simulate**: Simulate the transaction to get the correct fee amount and see the result before submitting to the network. By default, you'll see the result of the invoked function. If you want the full response of the invoke contract, simply toggle "Show full response". 7. **Submit**: Only use this when it's a `Write` operation and you want to change the data on the network. ![Lab: Contract Explorer: Invoke Contract](/assets/lab/lab-contract-explorer-invoke.gif) :::info Once you simulate the function, you will see a tooltip that says either `Read` or `Write` next to the title of the function you invoked. When a transaction doesn't change the state of the contract, it is considered a `Read` operation. In this scenario, it is not necessary to submit the transaction to the network, as it does not modify any data. You can simply simulate the transaction to see the results without incurring any costs. ::: ### Use Cases - **Testing**: Quickly test contract functions during development without deploying test scripts - **Debugging**: Investigate issues by invoking functions with different parameters and examining outputs - **Education**: Learn how contracts work by exploring and invoking public contracts - **Prototyping**: Experiment with contract interactions before building full applications --- ## Smart Contract List This page shows a list of recent Stellar smart contracts on the selected network. On Mainnet, there's an option to view popular contracts list. The table displays the smart contract ID or address, along with its creation date and time. Clicking on the contract address will direct you to the "Contract Explorer" page, where the selected address is pre-filled. ![Lab: Smart Contract List](/assets/lab/smart-contract-list-01232026.png) --- ## Upload and Deploy Contract There are several ways to upload and deploy contracts to the Stellar network. If you are familiar with the CLI workflow, you can easily upload and deploy contracts using two different commands on the [Stellar CLI](../../cli/README.mdx). If you prefer to do it all in a web interface, the ["Upload and Deploy Contract"](https://lab.stellar.org/smart-contracts/deploy-contract) page on Stellar Lab provides a convenient way to upload and deploy contracts to the network. ![Lab: Contract Explorer](/assets/lab/lab-upload-deploy-page.png) The deployment process consists of two distinct phases: 1. **Upload phase**: Uploading `WASM` bytecode to the network 2. **Deploy phase**: Deploying a contract instance from the successfully uploaded bytecode :::tip Each upload and deploy phase requires creating a transaction and signing the transaction. ::: ## Upload Phase The "Upload phase" accepts `.wasm` files through drag-and-drop or file browser selection. During this phase, the Lab checks whether the uploaded `.wasm` file already exists on the network. If it's not on the network, it creates an upload transaction that you need to sign using any of the following methods: secret key, hardware wallet, extension wallet, or signature. ![Lab: Upload and Deploy Contract - uploading contract](/assets/lab/lab-upload-contract.gif) If the requested `WASM` bytecode is already uploaded on the network, the Lab skips the "Upload contract" section and opens the "Deploy contract" section by default. You will see a message: "This contract `WASM` has already been uploaded. Wasm hash: \*\*\*\*" ![Lab: Upload and Deploy Contract - skipping uploading contract](/assets/lab/lab-already-uploaded.gif) ## Deploy Phase During the "Deploy phase", the Lab checks whether the `WASM` contract metadata has a constructor and what arguments it requires. Once all the required fields are filled, it creates a deployment transaction. After the transaction is signed and successfully submitted, you can check your contract on blockchain explorers like [Stellar.Expert](https://stellar.expert/explorer) or [Stellar Lab's contract explorer](https://lab.stellar.org/smart-contracts/contract-explorer) right away. ![Lab: Upload and Deploy Contract - deploying contract](/assets/lab/lab-deploy-contract.gif) --- ## Transaction Dashboard ![Lab: Transaction Dashboard](/assets/lab/ab-tx-dashboard-01232026.png) The [Transaction Dashboard](https://lab.stellar.org/transaction-dashboard) provides a comprehensive view of transaction details. For classic transactions, it offers deep insights into operations. For smart contract transactions, it offers details on its interactions (token summary, contracts, events, state change), resource consumption, signatures, and fee breakdowns. ![Transaction Details](/assets/lab/lab-tx-details.png) Transaction details for both smart contract and classic transactions include the status of the transaction, transaction hash, source account for the transaction, sequence number, the date it was processed, fee, and fee source account if applicable. In classic transactions, it includes `memo` and the number of `operations`. Whether you're debugging a failed transaction, analyzing contract behavior, or optimizing performance, the Transaction Dashboard gives you all the information you need in an organized, easy-to-understand interface. The dashboard automatically detects whether you're viewing a classic transaction or a smart contract transaction and displays the relevant tabs accordingly. :::note This feature uses an RPC and RPC retains at maximum 7 days of historical data. Any transaction older than 7 days will be displayed as invalid. See [Indexers](../../data/indexers/README.mdx) and [Block Explorers](../developer-tools/block-explorers.mdx#stellarexpert) for reference ::: ## Dashboard Tabs for Smart Contracts The Transaction Dashboard organizes transaction information into multiple tabs, each focusing on a specific aspect of the transaction: ### Token Summary The Token Summary tab displays information about token transfers and balance changes that occurred during the transaction. This tab is particularly useful for tracking asset movements and understanding the financial impact of a transaction. ![Token Summary tab](/assets/lab/lab-token-summary.png) **What you'll see:** Tokens transferred with asset codes and amounts, sender and receiver addresses for each transfer ### Contracts The Contracts tab shows detailed information about smart contracts involved in the transaction, including which contracts were invoked. ![Contracts tab](/assets/lab/lab-token-contracts.png) **What you'll see:** Contract IDs for all invoked contracts and their verification status ### Events The Events tab displays all events emitted during transaction execution. Events are logged outputs from smart contracts that track state changes and important occurrences, providing visibility into what happened inside contract execution. ![Events tab](/assets/lab/lab-events.png) **What you'll see:** Event topics, event data payloads with decoded values, Contract ID that emitted each event, chronological order of all events, and token events ### State Change The State Change tab shows how ledger entries changed before and after the transaction, giving you a complete picture of the transaction's impact on blockchain state. ![State change tab](/assets/lab/lab-state-change.png) **What you'll see:** Contract storage modifications (data read, written, or deleted), account balance changes, trustline updates, contract instance changes, state archival and restoration information, ledger footprint details (which entries were accessed) ### Resource Profiler The Resource Profiler provides detailed metrics on resource consumption during transaction execution, helping you understand performance characteristics and optimize costs. ![Resource Profiler tab](/assets/lab/lab-resource-profiler.png) **What you'll see:** CPU instructions, consumed Memory (RAM) bytes used, ledger read and write bytes, transaction size metrics, resource fee calculations, and etc. ### Signatures The Signatures tab displays all signing and authorization information for the transaction, essential for understanding multi-signature setups and contract authorization. ![Signatures tab](/assets/lab/lab-signatures.png) **What you'll see:** Required signers for the transaction, actual signatures provided, public keys of all signers. ### Fee Breakdown The Fee Breakdown tab provides a detailed analysis of all costs associated with the transaction, showing exactly where XLM was spent. ![Fee Breakdown tab](/assets/lab/lab-fee.png) **What you'll see:** Base network fee, resource fee, fee charged versus fee refunded, and final fee ## Dashboard Tabs for Classic The Transaction Dashboard for classic transactions is straightforward and focused on operations. It displays all operations that occurred in the transaction, with support for up to 100 operations per page. ![Classic Dashboard](/assets/lab/tx-dashboard-classic.png) ### Operations For more information on operations, check [List of Operations](https://developers.stellar.org/docs/learn/fundamentals/transactions/list-of-operations). ![Classic Dashboard Operations](/assets/lab/tx-dashboard-ops.png) --- ## Transactions(Lab) ## [Build Transaction](https://lab.stellar.org/transaction/build) ![Lab: Build Transaction](/assets/lab/lab-transactions-build.png) You can access the "Build Transaction" page by clicking the "Transactions" link in the Lab's main navigation. The transaction builder UI has helpful messages, links, and input validation to make learning how to build transactions on the Stellar network easier. There are three main sections: params, operations, and transaction validation (error or success). ### Params A Stellar transaction requires a source account, transaction sequence number, and a base fee. Memo and time bounds are optional. Once you enter a valid public address in the "Source Account" input, you can automatically fetch the sequence number by clicking the "Fetch next sequence" button. The base fee is 100 by default, but you may need to increase the fee if the network is congested. ### Operations ![Lab: Transaction operations](/assets/lab/lab-transactions-ops.png) A transaction requires at least one operation, but Stellar supports up to 100 operations in a single transaction. Select the operation type from the dropdown to get the fields for that operation type, and fill them out as needed. If you wish to add more operations, click the "Add Operation" button at the bottom of the operations section. You can also duplicate, delete, and move the operation by clicking the appropriate button on the upper right of the individual operation section. You can also save a valid transaction by clicking the save button at the bottom of the operations section. You will need to enter a name for this transaction in a pop-up and click the "Save" button. The transactions are saved in the browser's local storage and can be found on the "[Saved Transactions](./saved/transactions)" page (accessed by clicking the Saved link in the left navigation and clicking on the "Transactions" submenu item). Clicking the share button (right next to the save button) allows you to share a link with all the transaction information provided. You can share any transaction, even if it's invalid or incomplete. ### Transaction Validation ![Lab: Transaction validation success](/assets/lab/lab-transactions-response-success.png) The validation section at the bottom of the page shows the built transaction information if everything is correct. Clicking the "Sign in Transaction Signer" button will take you to the "Sign Transaction" page, where you can sign the transaction. You can also click the "View in XDR viewer" button to view the JSON of the built transaction. If there are errors in this transaction, you will see them grouped by section (params and every operation) to make it easier to tell what needs fixing. ![Lab: Transaction validation error](/assets/lab/lab-transactions-response-error.png) --- ## View XDR This section provides tools to work with [XDR](../../../learn/fundamentals/data-format/xdr.mdx) (External Data Representation) data. You can decode XDR into easy-to-read JSON format, convert JSON back into XDR format, or compare two XDR data sets to view their differences side-by-side. Learn more about XDR-JSON conversion [here](../../../learn/fundamentals/data-format/xdr-json.mdx). --- ## Diff XDRs :::info Learn more about XDR to JSON conversion tools [here](../../../learn/fundamentals/data-format/xdr-json.mdx). ::: ## Accessing the Diff XDRs Tool To access the [Diff XDRs](https://lab.stellar.org/xdr/diff) page, select "View XDR" from the left-hand navigation menu, then choose the "Diff XDRs" option from the submenu. ![Lab: Diff XDRs page](/assets/lab/lab-xdr-diff.png) ## Comparing XDR Data On this page, you can compare two base-64 encoded [XDRs](../../../learn/fundamentals/data-format/xdr.mdx) by viewing their JSON difference. Enter the Original XDR in the first input field, followed by the Changed XDR in the second field. Both XDRs must share the same XDR type, which is automatically populated when a valid Original XDR value is entered. If the suggested type appears incorrect, you can select an alternative from the dropdown menu. A search function is available to help locate the appropriate type. ![Lab: XDR type dropdown](/assets/lab/lab-xdr-diff-dropdown.png) ## Understanding the Output Once both XDR values are entered, a comparison view will display below, highlighting the differences between the two objects. Below each difference, you will find a button to copy the corresponding JSON data. ![Lab: Diff XDRs output](/assets/lab/lab-xdr-diff-filled.png) --- ## JSON to XDR :::info Learn more about JSON to XDR conversion tools [here](../../../learn/fundamentals/data-format/xdr-json.mdx). ::: ## Accessing the JSON to XDR Converter To access the [JSON to XDR](https://lab.stellar.org/xdr/to) page, select "View XDR" from the left-hand navigation menu, then choose the "JSON to XDR" option from the submenu. ![Lab: JSON to XDR page](/assets/lab/lab-xdr-from-json.png) ## Inputting JSON Data On this page, you can enter a valid [XDR](../../../learn/fundamentals/data-format/xdr.mdx) JSON object into the input field. To get sample data, navigate to the "[XDR to JSON](./xdr-to-json.mdx)" page, then use the "Copy JSON" button located below the JSON output to copy a formatted object for testing purposes. ## Understanding the Output After entering a valid JSON object, the interface displays two key components: 1. **XDR Type** – The identified type of XDR data, with an automatically pre-selected value. If the suggested type appears incorrect, you can select an alternative from the dropdown menu. A search function is available to help locate the appropriate type. ![Lab: XDR type dropdown](/assets/lab/lab-xdr-from-json-dropdown.png) 2. **XDR Output** – A base-64 encoded XDR generated based on the selected XDR type. You can copy this output by clicking the copy button located below the field. ![Lab: JSON to XDR output](/assets/lab/lab-xdr-from-json-filled.png) --- ## XDR to JSON :::info Learn more about XDR to JSON conversion tools [here](../../../learn/fundamentals/data-format/xdr-json.mdx). ::: To access the [XDR to JSON](https://lab.stellar.org/xdr/view) page, select "View XDR" from the left-hand navigation menu, then choose the "XDR to JSON" option from the submenu. ![Lab: XDR to JSON page](/assets/lab/lab-xdr-to-json.png) ## Inputting XDR Data On this page, you can enter a base-64 encoded [XDR](../../../learn/fundamentals/data-format/xdr.mdx) into the input field located at the top. Alternatively, you can get a sample XDR by selecting the "fetch the latest transaction" link positioned below the input field. ## Understanding the Output After loading a valid XDR (for example, by fetching the latest transaction), the interface displays three key components: 1. **Transaction Hash** – A read-only field containing the transaction hash, which can be copied for use in other tools, such as retrieving transaction details via [API Explorer > RPC Methods > getTransaction](https://lab.stellar.org/endpoints/rpc/get-transaction). 2. **XDR Type** – The identified type of XDR data, with an automatically pre-selected value. If the suggested type appears incorrect, you can select an alternative from the dropdown menu. A search function is available to help locate the appropriate type. ![Lab: XDR type dropdown](/assets/lab/lab-xdr-to-json-dropdown.png) 3. **JSON Output** – A JSON object generated based on the selected XDR type. You can copy this object by clicking the "Copy JSON" button located below the output. ![Lab: XDR to JSON output](/assets/lab/lab-xdr-to-json-filled.png) --- ## OpenZeppelin Contracts and Toolings ::::note OpenZeppelin is bringing its trusted [smart contracts libraries](https://github.com/OpenZeppelin/stellar-contracts) and developer workflows to Stellar, a Rust-based blockchain. Developers can build payments, stablecoins, and DeFi apps on Stellar using familiar, audited building blocks. To see more, go to: https://www.openzeppelin.com/networks/stellar. For latest OpenZeppelin developer docs, please visit: [https://docs.openzeppelin.com/stellar-contracts](https://docs.openzeppelin.com/stellar-contracts). :::: ## Getting started with Contract Wizard [OpenZeppelin's Contract Wizard](https://wizard.openzeppelin.com/stellar) includes support for Stellar’s Rust-based smart contracts, making it easier for developers to generate and deploy secure, audited contracts. After you have selected your desired template and options you can download it as a single file, a Rust development package, or a [Scaffold Stellar Package](./scaffold-stellar). Try it out below or visit the wizard [here](https://wizard.openzeppelin.com/stellar). For a walkthrough on using these contracts check out the video linked below! ## OpenZeppelin Stellar Contracts and Utilities OpenZeppelin Stellar Contracts is a collection of audited contracts and utilities for Stellar. The contracts are developed by OpenZeppelin in collaboration with the Stellar community and the Stellar Development Foundation (SDF), in an effort to bring a library of high-quality and audited contracts that can be used to build applications on the Stellar network. ### Audited Modules Available **Fungible Token** - Extensions: - **Burnable**: Allow token holders to destroy their tokens - **Capped**: Set maximum supply limits - **Allowlist**: Restrict transfers to approved addresses - **Blocklist**: Prevent transfers from/to blocked addresses **Non-Fungible Token** - Extensions: - **Burnable**: Allow token holders to destroy their NFTs - **Enumerable**: Enable iteration over all tokens and owner tokens - **Consecutive**: Efficiently mint multiple tokens in batches - **Royalties**: Support for creator royalties on secondary sales **Stablecoin Token** - Extensions: - **Burnable**: Allow token holders to destroy their tokens - **Capped**: Set maximum supply limits - **Allowlist**: Restrict transfers to approved addresses - **Blocklist**: Prevent transfers from/to blocked addresses **RWA (ERC-3643) Token** The RWA token extends the standard fungible token functionality with regulatory features required for security tokens, including: - Features: - **Identity Management**: Integration with identity registries for KYC/AML compliance - **Compliance Framework**: Modular compliance rules and validation for transfers and minting - **Transfer Controls**: Sophisticated transfer restrictions and validations - **Freezing Mechanisms**: Address-level and partial token freezing capabilities - **Recovery System**: Lost/old account recovery for verified investors - **Pausable Operations**: Emergency pause functionality for the entire token - **Role-Based Access Control (RBAC)**: Flexible privilege management for administrative functions **Token Vault** The Fungible Token Vault extends the Fungible Token and implements [SEP-56 Tokenized Vault Standard](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0056.md), enabling fungible tokens to represent shares in an underlying asset pool. The tokenized vault standard is the formalized interface for yield-bearing vaults that hold underlying assets. Vault shares enable hyperfungible collaterals in DeFi and remain fully compatible with standard fungible token operations. **Smart Accounts** Smart Accounts are contract based wallets made for flexible and programmable authorization. This framework takes a context-centric approach, separating three distinct concerns: who is allowed to sign (signers), what they are allowed to do (scope/context rules), and how those permissions are enforced (policies). The initial release includes policies for multisig and spending limits. - Components: - **Context rules**: Routing table for authorization - **Signers**: List of authorized signers (delegated address, or external signers) - **Policies**: Enforcement module attached to context rules - **Verifiers**: Trust contracts that validate signatures on behalf of smart accounts **Utilities** - **Pausable and Upgradeable Utilities** - **Role-based and Ownable Access Control** - **Merkle Distributor** - **Fixed point math (WAD)** - **Time lock** **Coming Soon** - Governor - Confidential Token Standard All contracts and extensions are audited by OpenZeppelin's security team, enhancing security and reliability of the contracts and extensions. Additional formal verification is being completed by Certora. To use the library, please visit: https://github.com/OpenZeppelin/stellar-contracts. ``` Repository Structure │── audits/ # Audit reports │── docs/ # Documentation │── examples/ # Example contracts │── packages/ │ ├── access/ # Access control, ownable, and role transfer utilities │ ├── accounts/ # Smart account framework │ ├── contract-utils/ # Utilities for token types (pausable, upgradable, etc.) │ ├── fee-abstraction/ # Utilities for implementing fee abstraction │ ├── governance/ # Utilities like timelock │ ├── macros/ # Macros for Stellar contractk │ ├── test-utils/ # Utilities for tests │ ├── tokens/ # Various token types (fungible, non-fungible, RWA, vault, etc.) ``` To provide feedback on these contracts and utilities, please open issues at: [https://github.com/OpenZeppelin/stellar-contracts/issues](https://github.com/OpenZeppelin/stellar-contracts/issues) ## OpenZeppelin Open Source Tools - [Relayer](https://github.com/OpenZeppelin/openzeppelin-relayer): Infrastructure for relaying transactions on Stellar. - [Monitor](https://github.com/OpenZeppelin/openzeppelin-monitor): Infrastructure tool for monitoring blockchain events and transactions. - [UI Builder](https://builder.openzeppelin.com): Open source tool for creating UI forms for contracts. - [MCP Server](https://mcp.openzeppelin.com): Generate secure Stellar smart contracts based on OpenZeppelin templates. - [Role Manager](https://github.com/OpenZeppelin/role-manager): Access control management interface for smart contracts. Visualize roles, permissions, and execute administrative actions. - [Soroban Security Detector SDK](https://github.com/OpenZeppelin/soroban-security-detectors-sdk): Spot issues in contracts before they reach mainnet. This SDK enables developers to build custom static-analysis scanners and comes with prebuilt checks to catch common pitfalls early. --- ## OpenZeppelin Relayer Launchtube, which served as an experimental service for fee sponsorship and contract invocations, has been instrumental in early-stage deployments and developer experimentation. However, while functional for testing and early use cases, Launchtube does not have the maturity, scalability and auditing as OpenZeppelin’s Relayer service, which is why the Stellar Development Foundation is discontinuing the Launchtube service and provides the Relayer service as a replacement. OpenZeppelin Relayer, also known as [Stellar Channels Service](https://docs.openzeppelin.com/relayer/1.3.x/guides/stellar-channels-guide), is a managed infrastructure for submitting Stellar Soroban transactions with automatic parallel processing and fee management. The service handles all the complexity of transaction submission, allowing you to focus on building your application. ## OpenZeppelin Relayer Status To see live status of the OpenZeppelin relayer, please visit this [Status] page: https://status.channels.openzeppelin.com/. ## Smart contract invocation Let’s create a simple application that submits a transaction, invoking a smart contract function, using Relayer. The application calls the Increment smart contract and returns the current counter value. The application is based on the Next.js framework and has server side code and client side code. The OpenZeppelin Relayer SDK makes HTTPS-requests to OpenZeppelin endpoints, which will trigger CORS errors when run client side, but by using the SDK server side, CORS will not be an issue. ### Prerequisites This guide assumes you have deployed the Increment smart contract example code found [here](https://github.com/stellar/soroban-examples/tree/v22.0.1/increment). See the [Getting Started](../build/smart-contracts/getting-started) tutorial section 3 and 4 for more information about building and deploying the Increment contract. ### Server Side In this simple application we only have one function server side, and that’s a function calling a smart contract function, by submitting a transaction through Relayer. #### 1. Initialize Relayer client First the two necessary SDKs are imported, and the Relayer client is initialized. In this tutorial we use testnet, so the Relayer service endpoint for testnet is used as the base URL. The API key can be generated [here](https://channels.openzeppelin.com/testnet/gen). ```js "use server"; // Initialize Channels Client const client = new RPChannels.ChannelsClient({ baseUrl: "https://channels.openzeppelin.com/testnet", apiKey: "c344ee79-6294-4dc3-8f7d-000000000000", }); ``` #### 2. Initialize contract, RPC and account Next we initialize the resources we need to build the transaction. Both the `contractId` and `sourceId` are arguments of the function and will be provided client side. ```js // Initialize Contract and RPC Server const contract = new StellarSDK.Contract(contractId); const rpc = new StellarSDK.rpc.Server("https://soroban-testnet.stellar.org"); // Get Source Account const account = await rpc.getAccount(sourceId); ``` #### 3. Build transaction `TransactionBuilder` takes the user account and network information as parameters, and operations can be added. In this case we want to add the contract invocation by using `contract.call()`, with the contract function name and arguments as parameters. ```js const tx = new StellarSDK.TransactionBuilder(account, { fee: "100", networkPassphrase: StellarSDK.Networks.TESTNET, }) .addOperation(contract.call(func, ...args)) .setTimeout(30) .build(); ``` #### 4. Simulate transaction and get XDRs The final steps before we can submit the transaction to Relayer is to simulate the transaction, bundle the transaction and simulation, and extract the transaction’s function and auth XDRs. We are extracting the function and auth from the assembled transaction, in XDR format, because that’s what we need to submit to Relayer. ```js // Simulate to get auth entries const simulation = await rpc.simulateTransaction(tx); const assembled = StellarSDK.rpc.assembleTransaction(tx, simulation).build(); // Extract function and auth XDRs const op = assembled.operations[0]; const contractFunc = op.func.toXDR("base64"); const contractAuth = (op.auth ?? []).map((a) => a.toXDR("base64")); ``` #### 5. Build Relayer request and submit it Now we have completed all necessary steps to submit a transaction to Relayer. First the request is built, and then the request is submitted to Relayer. ```typescript // Build request for Relayer const request: RPChannels.ChannelsFuncAuthRequest = { func: contractFunc, auth: contractAuth, }; // Submit to Channels Relayer const response: RPChannels.ChannelsTransactionResponse = await client.submitSorobanTransaction(request); ``` A successful submission will return a response of the following format: ```js { transactionId: string; // Internal tracking ID hash: string; // Stellar transaction hash status: string; // Transaction status (e.g., "confirmed") } ``` #### 6. Get returned value from contract The invoked Increment contract function will return the updated, incremented value so let’s get this value so we can show it in the client (frontend). To get the return value we simply poll for the transaction result until we get a response with `rpc.pollTransaction()`, which will return an object with more details than we need, so only the value is returned. ```js // Poll for transaction result let txResponse = await rpc.pollTransaction(response.hash!); // Return the result using the public SDK helper return StellarSDK.scValToNative(txResponse.returnValue); ``` #### 7. The complete code The previous six steps contain all the functionality needed for invoking a smart contract function through Relayer. This is the complete code for the function that we will call from the client side (frontend): ```js title="backend/index.tsx" "use server"; // Initialize Channels Client const client = new RPChannels.ChannelsClient({ baseUrl: 'https://channels.openzeppelin.com/testnet', apiKey: 'c344ee79-6294-4dc3-8f7d-000000000000', }); export const SendContractTransaction = async (sourceId: string, contractId: string, func: string, args: StellarSDK.xdr.ScVal[]) => { // Initialize Contract and RPC Server const contract = new StellarSDK.Contract(contractId); const rpc = new StellarSDK.rpc.Server('https://soroban-testnet.stellar.org'); // Get Source Account const account = await rpc.getAccount(sourceId); // Build the transaction const tx = new StellarSDK.TransactionBuilder(account, { fee: '100', networkPassphrase: StellarSDK.Networks.TESTNET, }) .addOperation(contract.call(func, ...args)) .setTimeout(30) .build(); // Simulate to get auth entries const simulation = await rpc.simulateTransaction(tx); const assembled = StellarSDK.rpc.assembleTransaction(tx, simulation).build(); // Extract function and auth XDRs const op = assembled.operations[0]; const contractFunc = op.func.toXDR('base64'); const contractAuth = (op.auth ?? []).map((a) => a.toXDR('base64')); // Build request for Relayer const request: RPChannels.ChannelsFuncAuthRequest = { func: contractFunc, auth: contractAuth, }; // Submit to Channels Relayer const response: RPChannels.ChannelsTransactionResponse = await client.submitSorobanTransaction(request); // Poll for transaction result let txResponse = await rpc.pollTransaction(response.hash!); // Return the decoded result from the ScVal return StellarSDK.scValToNative(txResponse.returnValue); } ``` ### Client Side The client side code shows a button on the page in the browser, and when clicked, the server side function will be called with the relevant parameters. When the function returns a value, the value is shown on the page instead of the button. The functionality is very simple, but serves well as an end-to-end example of submitting a transaction with Relayer. #### 1. Call server side function The client code has a function `callContract()` that can be invoked from the button click. The function calls the server side function `SendContractTransaction()` with the `sourceId`, `contractId`, contract function name and auth. ```js const [result, setResult] = React.useState(""); const callContract = async () => { const response = await SendContractTransaction( "GAZQUIHE242WV4CK7LLM5ZV4SM6SLV4CEY4LLAKEYD2O000000000000", "CDAZOG4V2KAPVBBKCAMHUT367AUGYWYEHD652UOJVER5ERNYGSOROBAN", "increment", [], ); setResult(response); }; ``` The response is stored as the result state. #### 2. Markup code The page markup code checks if result contains a value. If not, the button for invoking the callContract function is shown, and if result does contain a value, it’s shown on the page instead. That’s all there is to the client side markup code. ```html
{result ? ( Result from Relayer: {result} ) : ( )}
``` 3. The complete code This is the complete code for the client side (frontend): ```jsx title="page.tsx" "use client" export default function Home() { const [result, setResult] = React.useState(''); const callContract = async () => { const args: [] = []; const response = await SendContractTransaction( 'GAZQUIHE242WV4CK7LLM5ZV4SM6SLV4CEY4LLAKEYD2O000000000000', 'CDAZOG4V2KAPVBBKCAMHUT367AUGYWYEHD652UOJVER5ERNYGSOROBAN', "increment", [], ); setResult(response); } return (
{result ? ( Result from Relayer: {result} ) : ( )}
); } ``` ## Account transfer Let’s create another simple application, this application submits a transfer transaction, sending XLM tokens from one account to another, using Relayer. The application is based on the Next.js framework and has server side code and client side code. The OpenZeppelin Relayer SDK makes HTTPS-requests to OpenZeppelin endpoints, which will trigger CORS errors when run client side, but by using the SDK server side, CORS will not be an issue. ### Server Side In this simple application we only have one function server side, and that’s a function making a transfer of XLM tokens from one account to another, by submitting a transaction through Relayer. #### 1. Initialize Relayer client First the two necessary SDKs are imported, and the Relayer client is initialized. In this tutorial we use testnet, so the Relayer service endpoint for testnet is used as the base URL. The API key can be generated [here](https://channels.openzeppelin.com/testnet/gen). ```js "use server"; // Initialize Channels Client const client = new RPChannels.ChannelsClient({ baseUrl: "https://channels.openzeppelin.com/testnet", apiKey: "c344ee79-6294-4dc3-8f7d-000000000000", }); ``` #### 2. Initialize RPC and source account Next we initialize the resources we need to build the transaction. In this example code the source secret key is hardcoded, but this must be handled in a more secure manner in production applications. ```js // Initialize RPC Server const rpc = new StellarSDK.rpc.Server("https://soroban-testnet.stellar.org"); // Define source account details - this should be securely managed and not hardcoded in production const sourceSecret = "SADMGRVM3MDTZ54FN3SBEKSTRCVEY4WE5JAVCERJJBFR000000000000"; // Load the source account from the secret key const sourceKeypair = StellarSDK.Keypair.fromSecret(sourceSecret); const sourcePublicKey = sourceKeypair.publicKey(); // Load account details from the network const sourceAccount = await rpc.getAccount(sourcePublicKey); ``` #### 3. Build and sign transaction `TransactionBuilder` takes the source account and network information as parameters, and operations can be added. In this case we want to add the `payment()` operation to transfer an amount of XLM tokens (native asset) from the source account to the destination. The transaction is signed with the source keypair. ```js // Build the transaction const transaction = new StellarSDK.TransactionBuilder(sourceAccount, { fee: StellarSDK.BASE_FEE, networkPassphrase: StellarSDK.Networks.TESTNET, }) .addOperation( StellarSDK.Operation.payment({ destination: destinationPublicKey, asset: StellarSDK.Asset.native(), amount: amount.toString(), }), ) .setTimeout(30) .build(); transaction.sign(sourceKeypair); ``` #### 4. Submit the transaction Now we have completed all necessary steps to submit the transaction to Relayer. The Relayer SDK function `submitTransaction()` takes the transaction in XDR-format as an argument. ```js // Submit to Channels Relayer const response = await client.submitTransaction({ xdr: transaction.toXDR(), }); ``` A successful submission will return a response of the following format: ```js { transactionId: string; // Internal tracking ID hash: string; // Stellar transaction hash status: string; // Transaction status (e.g., "confirmed") } ``` #### 5. Return the transaction hash As a final step the transaction hash is returned. ```js return response.hash; ``` #### 6. The complete code The previous five steps contain all the functionality needed for transferring XLM tokens from the source account to another account through Relayer. This is the complete code for the function that we will call from the client side (frontend): ```js title="backend/index.tsx" "use server"; // Initialize Channels Client const client = new RPChannels.ChannelsClient({ baseUrl: 'https://channels.openzeppelin.com/testnet', apiKey: 'c344ee79-6294-4dc3-8f7d-000000000000', }); export const SendTransaction = async (destinationPublicKey: string, amount: string) => { // Initialize RPC Server const rpc = new StellarSDK.rpc.Server('https://soroban-testnet.stellar.org'); // Define source account details - this should be securely managed and not hardcoded in production const sourceSecret = 'SADMGRVM3MDTZ54FN3SBEKSTRCVEY4WE5JAVCERJJBFR000000000000'; // Load the source account from the secret key const sourceKeypair = StellarSDK.Keypair.fromSecret(sourceSecret); const sourcePublicKey = sourceKeypair.publicKey(); // Load account details from the network const sourceAccount = await rpc.getAccount(sourcePublicKey); try { // Build the transaction const transaction = new StellarSDK.TransactionBuilder(sourceAccount, { fee: StellarSDK.BASE_FEE, // Or use server.fetchBaseFee() for dynamic fees networkPassphrase: StellarSDK.Networks.TESTNET // Use Networks.PUBLIC for mainnet }) .addOperation( StellarSDK.Operation.payment({ destination: destinationPublicKey, asset: StellarSDK.Asset.native(), // XLM is the native asset amount: amount.toString() // Amount in XLM (e.g., "10" for 10 XLM) }) ) .setTimeout(30) // Transaction expires after 30 seconds .build(); // Sign the transaction transaction.sign(sourceKeypair); const response = await client.submitTransaction({ xdr: transaction.toXDR(), // base64 envelope XDR }); return response.hash; } catch (error) { console.error('Failed to submit Stellar transaction via OpenZeppelin Relayer:', error); throw error; } } ``` ### Client Side The client side code shows a form with input fields for a transfer-to-address, and amount to transfer, on the page in the browser. When the form is filled out and submitted, the server side function will be called with the form values as parameters. When the function returns with the transaction hash, the hash is shown on the page instead of the form. The functionality is very simple, but serves well as an end-to-end example of submitting a token transfer transaction with Relayer. #### 1. Call server side function The client code has a function `sendXLM()` that works as a form action function. The function calls the server side function `SendTransaction()` with the transfer destination public key and amount as parameters. ```js const [result, setResult] = React.useState(''); const sendXLM = async (formData: FormData) => { const to = formData.get('toAddress') as string; const amount = formData.get('amount') as string; const response = await SendTransaction(to, amount); setResult(response); }; ``` The response is stored as the result state. #### 2. Markup code The page markup code checks if `result` contains a value. If not, the form for submitting a transfer is shown, and if `result` does contain a value, it’s shown on the page instead. That’s all there is to the client side markup code. ```html
{result ? ( Transaction Hash: {result} ) : (
)}
``` #### 3. The complete code This is the complete code for the client side (frontend): ```js title="page.tsx" "use client" export default function Home() { const [result, setResult] = React.useState(''); const sendXLM = async (formData: FormData) => { const to = formData.get('toAddress') as string; const amount = formData.get('amount') as string; const response = await SendTransaction(to, amount); setResult(response); }; return (
{result ? ( Transaction Hash: {result} ) : (
)}
); } ``` ### OpenZeppelin Relayer documentation For more information see the OpenZeppelin Relayer documentation [here](https://github.com/OpenZeppelin/openzeppelin-relayer). --- ## Quickstart(Quickstart) Quickstart is a local Stellar network environment (node) that allows developers to run a local version of the Stellar network for development and testing. Quickstart runs a local version of Stellar Core, Horizon, RPC, and everything else needed to replicate the public network. For more information about running Stellar services in production, see the documentation for the individual services here: - [How to run Stellar Core in production](../../validators/README.mdx) - [How to run Horizon in production](../../data/apis/horizon/admin-guide/overview.mdx) - [How to run RPC in production](../../data/apis/rpc/admin-guide/README.mdx) Quickstart is intended for use in development, not in production, although running it in public mode will cause it to join the public network. **Run Quickstart** Ready to get started? The Getting Started section of the Quickstart documentation will show how to run Quickstart, but there are some configurations to decide on first. Most importantly, the network mode Quickstart will run in (pubnet, testnet, futurenet, local). Read about the modes in the Network Modes section. :::info Quickstart can be deployed using the Getting Started guide, but the configurations used in the guide may not be ideal for your use case, so please familiarize yourself with the options to configure Quickstart to fit your use case. ::: :::caution Please note: the quickstart image is not intended for production purposes. Information about setting up a production environment is available [here](../../validators/README.mdx). ::: --- ## Advanced Usage Quickstart works out of the box, and when launched using the Stellar CLI, no setup or configuration is required. While the standard configuration may work for many use cases, it may not fit everyone’s needs. If a custom setup and configuration is needed, the Quickstart container can be run using the docker CLI. Read more about Quickstart configuration and setup: --- ## Container Quickstart is packaged as a docker container image and provides a default, non-validating, ephemeral configuration that should work for most developers. By configuring a container using this image with a host-based volume (described below in the "Configuration" section) an operator gains access to full configuration customization and persistence of data. The Quickstart [docker image](https://hub.docker.com/r/stellar/quickstart) uses the following software: - [PostgreSQL](https://www.postgresql.org) - [stellar-core](https://github.com/stellar/stellar-core) - [horizon](https://github.com/stellar/stellar-horizon) - [friendbot](https://github.com/stellar/friendbot) - [stellar-rpc](https://github.com/stellar/stellar-rpc/tree/main/cmd/stellar-rpc) - [supervisord](http://supervisord.org) The Stellar CLI has commands for starting and stopping a Quickstart container, and also a command to get logs from a running container. See Stellar CLI [documentation](../../cli/stellar-cli.mdx#stellar-container) for more information. --- ## Operation Modes ## Background vs. Interactive containers Docker containers can be run interactively (using the `-it` flags) or in a detached, background state (using the `-d` flag). Many of the example commands below use the `-it` flags to aid in debugging but in many cases, you will simply want to run a node in the background. It's recommended that you use the [guides](https://docs.docker.com/get-started) at docker to familiarize yourself with using docker. ## Ephemeral mode Ephemeral mode is provided to support development and testing environments. Every time you start a container in ephemeral mode, the database starts empty and a default configuration file will be used for the appropriate network. Starting an ephemeral node is simple, just craft a `docker run` command to launch the appropriate image but do not mount a volume. To craft your docker command, you need the network name you intend to run against and the flags to expose the ports you want available (See the section named "Ports" below to learn about exposing ports). Thus, launching a testnet node while exposing Horizon would be: ```sh docker run --rm -it -p "8000:8000" --name stellar stellar/quickstart --testnet ``` As part of launching, an ephemeral mode container will generate a random password for securing the postgresql service and will output it to standard out. You may use this password (provided you have exposed the postgresql port) to access the running postgresql database. ## Persistent mode In comparison to ephemeral mode, persistent mode is more complicated to operate, but also more powerful. Persistent mode uses a mounted host volume, a directory on the host machine that is exposed to the running docker container, to store all database data as well as the configuration files used for running services. This allows you to manage and modify these files from the host system. Note that there is no guarantee that the organization of the files of the volume will remain consistent between releases of the image that occur on every commit to the stellar/quickstart repository. At any time new files may be added, old files removed, or dependencies and references between them changed. For this reason, persistent mode is primarily intended for running short-lived test instances for development. If consistency is required over any period of time use image digest references to pin to a specific build. Starting a persistent mode container is the same as the ephemeral mode with one exception: ```sh docker run --rm -it -p "8000:8000" -v "/home/scott/stellar:/opt/stellar" --name stellar stellar/quickstart --testnet ``` The `-v` option in the example above tells docker to mount the host directory `/home/scott/stellar` into the container at the `/opt/stellar` path. You may customize the host directory to any location you like, simply make sure to use the same value every time you launch the container. Also note: an absolute directory path is required. The second portion of the volume mount (`/opt/stellar`) should never be changed. This special directory is checked by the container to see if it is mounted from the host system which is used to see if we should launch in ephemeral or persistent mode. Upon launching a persistent mode container for the first time, the launch script will notice that the mounted volume is empty. This will trigger an interactive initialization process to populate the initial configuration for the container. This interactive initialization adds some complications to the setup process because in most cases you won't want to run the container interactively during normal operation, but rather in the background. We recommend the following steps to set up a persistent mode node: 1. Run an interactive session of the container at first, ensuring that all services start and run correctly. 2. Shut down the interactive container (using Ctrl-C). 3. Start a new container using the same host directory in the background. --- ## Ports Quickstart exposes one main port through which services provide their APIs: | Port | Service | Description | | ---- | ------------------------------- | -------------- | | 8000 | horizon, stellar-rpc, friendbot | main http port | Quickstart also exposes a few other ports that most developers do not need, but are available: | Port | Service | Description | | ----- | -------------------------- | -------------------- | | 5432 | postgresql | database access port | | 6060 | horizon | admin port | | 6061 | stellar-rpc | admin port | | 11625 | stellar-core | peer node port | | 11626 | stellar-core | main http port | | 11725 | stellar-core (horizon) | peer node port | | 11726 | stellar-core (horizon) | main http port | | 11825 | stellar-core (stellar-rpc) | peer node port | | 11826 | stellar-core (stellar-rpc) | main http port | --- ## Run Commands Below is a list of various ways you might want to run the Quickstart container annotated to illustrate what options are enabled. It's also recommended that you should learn and get familiar with the docker command. Start an ephemeral local-only dev/test network: ```sh docker run -d \ -p "8000:8000" \ --name stellar \ stellar/quickstart \ --local ``` ```powershell docker run -d ` -p "8000:8000" ` --name stellar ` stellar/quickstart ` --local ``` Start an ephemeral testnet node in the foreground: ```sh docker run --rm -it \ -p "8000:8000" \ --name stellar \ stellar/quickstart \ --testnet ``` ```powershell docker run --rm -it ` -p "8000:8000" ` --name stellar ` stellar/quickstart ` --testnet ``` Start a new persistent node using the host directory `/str`: ```sh docker run -it --rm \ -p "8000:8000" \ -v "/str:/opt/stellar" \ --name stellar \ stellar/quickstart \ --testnet ``` ```powershell docker run -it --rm ` -p "8000:8000" ` -v "/str:/opt/stellar" ` --name stellar ` stellar/quickstart ` --testnet ``` --- ## Service Options All network modes run all the services (Core, Horizon, and RPC) by default, but the --enable option can be used to enable just some of the services. The enable option behavior is slightly different in local network mode. The option takes a comma-separated list of service names to enable. To enable all services which is the default behavior, use: ```sh --enable core,horizon,rpc ``` To run only select services, simply specify only those services. For example, to enable the RPC, use: ```sh --enable rpc ``` :::info In local network mode, the core service always runs no matter what options are passed, the Friendbot faucet service runs whenever Horizon is running, and Horizon is run when RPC is requested so that Friendbot is available. ::: --- ## Cloud Deployment Quickstart can be deployed to a cloud platform to create a shareable test network for coordinated and multi-person testing. Any platform supporting a container runtime can deploy the Quickstart image: ``` docker.io/stellar/quickstart:latest ``` Use the following environment variables to configure the image: - `NETWORK` - The network that the container will setup or connect to. One of: - `local` - A new local network will be created for development / testing. - `testnet` - The container will connect to the public testnet. - `NETWORK_PASSPHRASE` - For `local` networks the network passphrase that will be used. - `RANDOMIZE_NETWORK_PASSPHRASE` - For `local` networks the network passphrase will have a random suffix. - `LIMITS` - The network limits that will be configured for smart contracts. One of: - `testnet` - Limits that match the public testnet. - `unlimited` - Max limits supported by the network under any configuration. Useful for experimental testing prior to optimization. :::warning Quickstart is designed for development and testing, not production. ::: Examples for how to deploy on different providers: --- ## DigitalOcean Deploy to DigitalOcean App Platform using the following link: [![Deploy to DigitalOcean](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/stellar/quickstart/tree/master) --- ## Fly.io Deploy to Fly.io using the following steps. Install the Fly CLI. See [fly.io/docs/flyctl/install](https://fly.io/docs/flyctl/install) for instructions. Launch the image: ```shell fly launch \ --image docker.io/stellar/quickstart:latest \ --ha=false \ --env NETWORK=local \ --env LIMITS=testnet \ --env NETWORK_PASSPHRASE="My Network" \ --internal-port 8000 ``` ```powershell fly launch ` --image docker.io/stellar/quickstart:latest ` --ha=false ` --env NETWORK=local ` --env LIMITS=testnet ` --env NETWORK_PASSPHRASE="My Network" ` --env RANDOMIZE_NETWORK_PASSPHRASE=false ` --internal-port 8000 ``` --- ## Debugging There will come a time when you want to inspect the running container, either to debug one of the services, to review logs, or perhaps some other administrative tasks. There are different ways to debug Quickstart: --- ## Diagnostic Events Soroban diagnostic events contain logs about internal events that have occurred while a contract is executing. They're particularly useful for debugging why a contract is trapped (panicked). To enable Soroban diagnostic events, provide the following command line flag when starting the container: `--enable-soroban-diagnostic-events` In local network mode, diagnostics are enabled by default and can be disabled with: `--disable-soroban-diagnostic-events` :::info Diagnostic events are unmetered and their execution is not metered or constrained by network limits or transaction resource limits. This means the resources consumed by an instance with diagnostic events enabled may exceed resources typically required by a deployment with diagnostic events disabled. ::: --- ## Viewing Logs You can view logs by starting a new interactive shell inside the running container: ```sh docker exec -it stellar /bin/bash ``` The command above assumes that you started your container with the name `stellar`. Replace that name with whatever you chose, if different. When run, it will open an interactive shell running as root within the container. Logs can be found within the container at the path `/var/log/supervisor/`. A file is kept for both the **_stdout_** and **_stderr_** of the processes managed by [supervisord](https://supervisord.org/index.html). Additionally, you can use the `tail` command provided by supervisord. Alternatively, to tail all logs into the container's output for all services, append the `--logs` option. --- ## Explorer When you run **Stellar Lab** via **Quickstart**, a lightweight block/transaction explorer is automatically available alongside Lab. This makes it easy to inspect network activity on your local Quickstart network while you build and test. ### URLs **Lab (home):** `http://localhost:8000/lab` **Explorer:** `http://localhost:8000/lab/transactions-explorer` If you’re using a custom port or host when starting Quickstart, replace `localhost:8000` with your chosen address. ### What You Can Do with the Explorer - Browse recent transactions on your Quickstart network. - Drill into a transaction to view its details (hash, result, involved operations, etc.). - Use it alongside Lab tools (e.g., building/signing transactions) to quickly verify results from the local network. ### TL;DR 1. Start Quickstart (per the repo instructions). 2. Open Lab: `http://localhost:8000/lab`. 3. Open the Explorer: `http://localhost:8000/lab/transactions-explorer`. 4. Build and submit transactions in Lab, then switch to the Explorer to confirm they landed. --- ## Faucet Quickstart uses Friendbot as a native asset faucet for testnet, futurenet, and local networks, just like Friendbot is used on the public testnet and futurenet. Friendbot is available on `:8000/friendbot` and can be used to fund accounts and contracts. For example: ```sh curl http://localhost:8000/friendbot?addr=G... ``` In local network mode, a local Friendbot is running. In testnet and futurenet network modes, requests to the local `:8000/friendbot` endpoint will be proxied to the Friendbot deployments for the respective network. --- ## Getting Started(Getting-started) In this guide, you will set up Quickstart using the Stellar CLI. Quickstart will use the standard configuration, which will work for many use cases. For a more customized setup of Quickstart, see the Advanced Usage section. ## Prerequisites Before following the steps in this guide, the following must be installed and setup on your local system: - [Stellar CLI](../../../build/smart-contracts/getting-started/setup.mdx#install-the-stellar-cli) - [Docker](https://docs.docker.com/get-started/get-docker) ## Start Quickstart When you use the Stellar CLI to start Quickstart, the docker image will automatically be downloaded, and the standard configuration applied. All you need to do to get started is to run this command: ```sh stellar container start local ``` In this example, the network mode local is chosen, see the Network Modes section for more information about available network modes. After running the command, Quickstart is running on your local system. In the next step, you will use Stellar Lab to verify Quickstart is working correctly. --- ## Connect Stellar Lab One way to confirm that Quickstart was successfully started and is available is to connect to Quickstart from [Stellar Lab](https://lab.stellar.org). Navigate to [Stellar Lab](https://lab.stellar.org) and in the top right corner, use the dropdown to select the Custom network. This will allow you to provide connection details - use these testnet details: - RPC URL: `http://localhost:8000/rpc` - Horizon URL: `http://localhost:8000` - Network passphrase: `Standalone Network ; February 2017` ![Stellar Lab](/assets/quickstart/stellar_lab_screenshot.png) Switch to the Custom network and you can start to interact with Quickstart. A good first task could be to navigate to the Account tab, create an account, and fund it with Friendbot (faucet). --- ## Deploy Smart Contract Now you can deploy a smart contract on the Quickstart local network. If you already have a smart contract, you can deploy your existing smart contract, otherwise, you can follow this guide to create a smart contract and build it. ## Create Identity Before you can deploy and invoke a smart contract, you need to create an identity on the local network. The identity is a required parameter, referred to as `source-account` or `source`. This is how you can create an identity on the local network called `bob`: ```sh stellar keys generate --global bob --network local --fund ``` The identity will have a private and a public key, and can be called by the name `bob` for convenience. ## Deploy contract Deploying a smart contract to Quickstart testnet is not very different from deploying a smart contract to the public testnet. Specify the RPC URL to be Quickstart’s and you can deploy it: ```sh stellar contract deploy \ --wasm target/wasm32v1-none/release/hello_world.wasm \ --source-account bob \ --network local \ --alias hello_world ``` ```powershell stellar contract deploy ` --wasm target/wasm32v1-none/release/hello_world.wasm ` --source-account bob ` --network local ` --alias hello_world ``` ## Invoke contract Now you have deployed the smart contract to the Quickstart testnet, you can also invoke it: ```sh stellar contract invoke \ --id hello_world \ --source-account bob \ --network local \ -- \ hello \ --to RPC ``` ```powershell stellar contract invoke ` --id hello_world ` --source-account bob ` --network local ` -- ` hello ` --to RPC ``` That’s it! Now you can dive into some of the benefits of running your own local node with Quickstart. Some benefits are that it’s faster to invoke a contract, you get better debugging options, etc. --- ## Network Modes Quickstart can operate in different network modes, and you need to decide which mode is suitable for your purpose before starting the container. The four available network modes are `local`, `testnet`, `futurenet` and `pubnet` (mainnet). The [Stellar Lab](https://lab.stellar.org) can connect to Quickstart, which makes it easy to create and fund accounts. ## Local The local network mode allows you to customize Quickstart by adding parameters to the container start command. The available parameters are: - `--protocol-version {version}` to run a specific protocol version (defaults to latest version) - `--limits {limits}` to configure specific Soroban resource limits to one of: - `default` leaves limits set extremely low which is stellar-core's default configuration - `testnet` sets limits to match those used on testnet (the default quickstart configuration) - `unlimited` sets limits to the maximum resources that can be configured Some settings and values are specific to the local network mode, which you must be aware of when running Quickstart in local network mode. ### Network Passphrase The default network passphrase of the local network is: ```sh Standalone Network ; February 2017 ``` Set the network passphrase in the SDK or tool you're using. If clients use an incorrect network passphrase when signing transactions, the transactions will fail with an authentication error. ### Root Account The root account of the network is fixed to: ```sh Public Key: GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI Secret Key: S... ``` The root account is derived from the network passphrase and if the network passphrase is changed, the root account will change. To retrieve the root account details when changing the network passphrase, view the logs for stellar-core on its first start. See [Viewing](https://github.com/stellar/quickstart?tab=readme-ov-file#viewing-logs) logs for more details. In local network mode, a ledger occurs every one second and so transactions are finalized faster than on deployed networks. ## Testnet The testnet network mode is recommended for software development. Accounts created on the testnet can be funded with Friendbot (faucet) so you don’t have to worry about losing real tokens when testing your software. ## Futurenet The futurenet network mode works like the testnet, and the biggest difference is that it contains the latest bleeding-edge features that haven’t made it to testnet yet. This can be useful for developers building smart contracts that rely on new features that are not available on testnet, or test existing smart contracts ahead of changes to test- and mainnet. ## Pubnet In pubnet network mode, Quickstart will be a part of the public production Steller network, also known as the mainnet. In pubnet mode, Quickstart will consume more disk, memory, and CPU resources because of the size of the ledger and frequency of transactions. If disk space warnings occur and the image is being used on a Docker runtime that uses a VM, like that of macOS and Windows, the VM may need to have its disk space allocation increased. :::caution Quickstart is not suitable for any production use as it has a fixed root account. Any private network intended for production use would also require a unique network passphrase. ::: --- ## Ramps On and off-ramps (called [anchors](../../learn/fundamentals/anchors.mdx) in the Stellar universe) that connect the Stellar network to traditional financial rails. Stellar has anchor services operating worldwide. View the [Anchor Directory](https://anchors.stellar.org) for information on Stellar anchors. Anchors can issue their own assets on the Stellar network, or they can honor assets that already exist. You can set up an anchor by using the SDF-maintained [Anchor Platform](../../platforms/anchor-platform/README.mdx), which is the easiest way to deploy an anchor service compatible with Stellar Ecosystem Proposals (SEPs). --- ## MoneyGram Ramps MoneyGram Ramps is a MoneyGram product that enables users of third-party applications, such as crypto wallets and exchanges, to cash-in (deposit) and cash-out (withdrawal) USDC on Stellar. Dive into the [MoneyGram Ramps docs](https://developer.moneygram.com/moneygram-developer/docs/integrate-moneygram-ramps) to learn about the technical requirements for integrating MoneyGram Ramps into an existing wallet or creating a new wallet application. --- ## Scaffold Stellar **Scaffold Stellar** is a developer toolkit for building decentralized applications (dApps) and smart contracts on the Stellar blockchain. It helps you go from idea to working full-stack dApp faster — by providing CLI tools, reusable contract templates, a smart contract registry, and a modern frontend. Visit the [Scaffold Stellar](https://scaffoldstellar.org) homepage for guides, docs, and more. ## Prerequisites Before you begin, make sure you have the following installed: | Tool | Description | Install Link | | --- | --- | --- | | [Rust & Cargo](https://www.rust-lang.org/tools/install) | For writing and compiling smart contracts | `curl https://sh.rustup.rs -sSf \| sh` | | [Node.js & npm](https://nodejs.org) | For frontend development | Download from official site | | [Stellar CLI](https://github.com/stellar/stellar-cli) | For building, deploying, and interacting with smart contracts | [`Link for the repo`](https://github.com/stellar/stellar-cli) | | [Docker](https://www.docker.com/get-started) | For running a local Stellar network via the `stellar/quickstart` image | [Download Docker Desktop](https://www.docker.com/products/docker-desktop) | ## Getting Started ### Installation Install the required CLI tools: ```bash # Install stellar-scaffold CLI cargo install --locked stellar-scaffold-cli # Install registry CLI (to easily deploy your contract to the registry) cargo install --locked stellar-registry-cli ``` :::tip[For a faster install, use cargo-binstall] Instead of building from source, you can speed up the installation by getting the binary directly using [cargo-binstall](https://github.com/cargo-bins/cargo-binstall). This is especially useful in CI environments. ```bash # Install cargo-binstall (see other install methods in their README linked above) cargo install cargo-binstall # Install the binaries cargo binstall stellar-scaffold-cli stellar-registry-cli ``` ::: While the CLIs are installing, check out this intro to Scaffold Stellar! ### Creating a New Project 1. Initialize a new project: ```bash stellar scaffold init my-project cd my-project ``` This will create the project scaffold in the directory you specified with a few sample contracts. Or you can start from the [OpenZeppelin Wizard](./openzeppelin-contracts) to customize your contract and start a Scaffold Stellar project from there. 2. Start the app: ```bash npm start ``` You'll have a running dApp integrated with the starter contracts, ready to start building! Explore the `environments.toml` file to customize your development environment(s) that's set by your `.env` file. ## Project Structure When you run `stellar scaffold init`, it creates a full-stack project structure with example contracts and a modern frontend: ``` my-project/ ├── contracts/ # Rust smart contracts (compiled to WASM) ├── packages/ # Auto-generated TypeScript contract clients ├── src/ # React frontend code │ ├── components/ # Reusable UI components │ ├── contracts/ # Contract interaction logic │ ├── App.tsx # Main app component │ └── main.tsx # Entry point ├── environments.toml # Configuration per environment (dev/test/prod) ├── .env # Local environment variables ├── package.json # Frontend packages └── target/ # Build outputs ``` This template provides a ready-to-use frontend application with example smart contracts and their TypeScript clients. The frontend is set up with `Vite`, `React`, and includes basic components for interacting with the contracts. ## Features - **CLI Plugins for Stellar** - `stellar scaffold init`: Initialize new Stellar smart contract projects - `stellar scaffold upgrade`: Transform existing Stellar contract workspaces into Scaffold projects - `stellar scaffold build`: Build contracts and generate TypeScript clients - `stellar scaffold watch`: Development mode with hot reloading - `stellar registry`: Publish, deploy, and manage smart contracts - **Environment Management** - Environment-specific builds (development, testing, staging, production) - Seamless integration with both local and deployed contracts - Network configuration via `environments.toml` - Support for multiple deployment environments - **Smart Contract Registry** - On-chain publishing platform for Wasm binaries - Version management and contract naming - Contract verification and dependency management - Secure deployment workflow for testnet and mainnet ### More Resources For more information, check out the [project on GitHub](https://github.com/theahaco/scaffold-stellar)! ## Video Series Check out the [Scaffold Stellar video series on YouTube](https://www.youtube.com/playlist?list=PLmr3tp_7-7Gjj6gn5-bBn-QTMyaWzwOU5) for step-by-step tutorials and guides: - [Scaffold Stellar | A Boilerplate For Stellar Developers](https://www.youtube.com/watch?v=7wKD3d9w5d0) - Comprehensive walkthrough of Scaffold Stellar as a boilerplate for Stellar development. - [Scaffold Stellar: Getting Started and Guess the Number](https://www.youtube.com/watch?v=86hWe8Ragtg) - A practical tutorial that walks through improving the default "Guess the Number" dApp included in Scaffold Stellar. - [Building dApps with Scaffold Stellar](https://www.youtube.com/watch?v=t85jrhsTYV8) - An overview of the open source Scaffold Stellar development toolkit and how it streamlines the creation and management of Stellar apps. - [Live Demo of Scaffold Stellar](https://www.youtube.com/watch?v=0syGaIn3ULk) - A live demonstration showing Scaffold Stellar in action. - [Intro to Scaffold Stellar](https://www.youtube.com/watch?v=559ht4K4pkM) - Introduction to the Scaffold Stellar toolkit and its features. - [Which Frontend?](https://www.youtube.com/watch?v=pz7O54Oia_w) - Guide on choosing and working with frontend frameworks in Scaffold Stellar. - [Scaffold Stellar: Get started building](https://www.youtube.com/watch?v=H-M962aPuTk) - Learn how to install Scaffold Stellar, review included libraries and integrations, and get your development environment up and running. - [Rapid Application Development with Scaffold Stellar | Meridian 2025](https://www.youtube.com/watch?v=sx77r9qQOeE) - Introduction to Scaffold Stellar's rapid development environment, showing how to go from prototype to production. --- ## Explore SDKs for Blockchain Development with JavaScript, Python & More # SDKs The SDK section is split into three categories: - [Contract SDKs](./contract-sdks.mdx) that are used to build smart contracts that will be deployed to the Stellar network; - [Client & XDR SDKs](./client-sdks.mdx) that are used by applications to interact with the network; - [Build your own SDK](./build-your-own.mdx) that details the minimum requirements for building your own SDK. --- ## Build Your Own Contract SDK :::note This is for building an SDK for writing smart contracts. ::: Soroban currently has one SDF-maintained SDK for writing contracts in Rust, which can be found [here][soroban-sdk]. A community-maintained SDK is available for writing contracts in AssemblyScript, which can be found [here][as-soroban-sdk]. To build SDKs for other languages a few things need to be included in the SDK to provide contracts with the foundation they need to accept inputs, decode them, store data, call other contracts, etc. Below is a list of functionality a Soroban SDK needs to support those things, as well as some details on what an SDK can provide in regards to testing capabilities. ## Functionality ### Value Conversions - [Val] encode/decode - [Object] encode/decode - [Symbol] encode/decode - [Option] encode/decode - [Error] encode/decode ### Host Functions The host functions defined in [env.json] are functions callable from within the Wasm Guest environment. These need to be available to contracts to call, in some form, ideally wrapped so that contracts have a nicer interface. Host functions have friendly names in the file above, such as `get_ledger_version`, however in the Wasm they are only importable via short names, such as `x.4`. The letter proceeding the dot is the module, and the value after the dot is the function name. The mappins are available in env.rs. ### SDK Types All the types in [soroban-sdk](https://docs.rs/soroban-sdk) should be supported. Notably: - [Map] - [Vec] - [Bytes] ### User Defined Types Contracts should be able to create user defined types, such as structs, unions, or enums, and have them be transmitted to the host for storing and transmitted back for loading. SDKs do this by converting objects to and from a `Val`. In the [soroban-sdk] this is referred to as a [Val]. #### Structs Structs with named fields should be translated into a `Map` with keys as `Symbol`s and the values as the field value, i.e. `Map`. Structs with unnamed fields should be translated into a `Vec` with the values as the elements. i.e. `Vec`. #### Unions Unions (or enums in some languages) with named variants and unit or tuple values should be translated into a `Vec` with the first element as a `Symbol` of the name of the variant, and zero or more additional elements representing a value stored with the variant. #### Enums Enums with integer values should be translated into a `u32`. ### User Defined Errors Errors are `u32` values that are translated into a [Error]. ### Environment Meta Generation Contracts must contain a Wasm custom section with name `contractenvmetav0` and containing a serialized [`SCEnvMetaEntry`]. The interface version stored within should match the version of the host functions supported. To view the Environment Meta, please consider using Stellar Lab's Contract Explorer, or Stellar CLI's command `stellar contract info env-meta --contract-id `. ### Contract Meta Generation Contracts can optionally include a custom Wasm section named `contractmetav0`, which contains a serialized [`SCMetaEntry`]. This section is not used by the network itself, but allows contracts to embed arbitrary metadata, including contract name, version, author, supported interfaces, source repo, or home domain. Applications and tooling can read this metadata to provide richer developer experiences, better indexing, or enhanced contract discovery. To add metadata to Contract Meta, please use the Stellar CLI command `stellar contract build --meta `. To view the Contract Meta, please use the Stellar Lab's Contract Explorer, or the Stellar CLI command `stellar contract info meta --contract-id `. ### Contract Spec Generation Contracts should contain a Wasm custom section with name `contractspecv0` and containing a serialized stream of [`SCSpecEntry`]. There should be a `SCSpecEntry` for every function, struct, and union exported by the contract. To view the Contract Meta, please use the Stellar Lab's Contract Explorer, or the Stellar CLI command `stellar contract info interface --contract-id `. ## Testing Any Soroban SDK ideally provides a test environment for executing contract functions in the context of a Soroban runtime environment. The [soroban-sdk] does this by embedding the Soroban environment Rust library, [soroban-env-host]. The test environment should include: - Invoking contract functions. - Integration testing across multiple contracts. [soroban-sdk]: https://docs.rs/soroban-sdk [as-soroban-sdk]: https://github.com/Soneso/as-soroban-sdk [soroban-env-host]: https://github.com/stellar/rs-soroban-env [val]: https://github.com/stellar/rs-soroban-env/blob/main/soroban-env-common/src/val.rs [error]: https://github.com/stellar/rs-soroban-env/blob/main/soroban-env-common/src/error.rs [object]: https://github.com/stellar/rs-soroban-env/blob/main/soroban-env-common/src/object.rs [symbol]: https://github.com/stellar/rs-soroban-env/blob/main/soroban-env-common/src/symbol.rs [option]: https://github.com/stellar/rs-soroban-env/blob/main/soroban-env-common/src/option.rs [env.json]: https://github.com/stellar/rs-soroban-env/blob/main/soroban-env-common/env.json [map]: https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-sdk/src/map.rs [vec]: https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-sdk/src/vec.rs [bytes]: https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-sdk/src/bytes.rs [`scenvmetaentry`]: https://github.com/stellar/stellar-xdr/blob/next/Stellar-contract-env-meta.x [`scspecentry`]: https://github.com/stellar/stellar-xdr/blob/next/Stellar-contract-spec.x [`scmetaentry`]: https://github.com/stellar/stellar-xdr/blob/next/Stellar-contract-meta.x --- ## Simplify Blockchain Development with SDKs for Java, Python, and More # Client & XDR SDKs Client and XDR SDKs are used by applications to interact with the network. :::note For SDKs for building smart contracts, see [Contract SDKs](./contract-sdks.mdx). ::: All SDKs are open-source; file a GitHub issue or pull request in the specific SDK repository if you have questions or suggestions. Each SDK has its own source code and documentation. Learn how to use a specific SDK by referring to the documentation. ## JavaScript SDK [JavaScript SDK](https://github.com/stellar/js-stellar-sdk) | [Docs](https://stellar.github.io/js-stellar-sdk) | [NPM](https://www.npmjs.com/package/@stellar/stellar-sdk) **The JavaScript SDK is maintained by SDF.** `stellar-sdk` is the JavaScript library for communicating with Stellar RPC and Horizon. It supports building transactions on the Stellar network. It is used for building Stellar apps either in the browser or a Node.js environment. It provides: - A networking layer API for Stellar RPC methods and the Horizon API. - Facilities for building and signing transactions, for communicating with an RPC instance, for communicating with a Horizon instance, and for submitting transactions or querying network state. ## Python SDK [Python SDK](https://github.com/StellarCN/py-stellar-base) | [Docs](https://stellar-sdk.readthedocs.io/en/latest) | [Examples](https://github.com/StellarCN/py-stellar-base/tree/master/examples) **The Python SDK is maintained by dedicated community developers.** `py-stellar-base` is a Python library for communicating with a Stellar Horizon server and Stellar RPC. It is used for building Stellar apps on Python. It supports Python 3.10+ as well as PyPy 3.11. It provides: - A networking layer API for Horizon endpoints. - A networking layer API for Stellar RPC methods. - Facilities for building and signing transactions, for communicating with a Stellar Horizon or Stellar RPC instance, and for submitting transactions or querying network state and history. ## Rust Functionality for interacting with Stellar data can be found in the following Rust crates: - `stellar-xdr` – [Code](https://github.com/stellar/rs-stellar-xdr) | [Docs](https://docs.rs/stellar-xdr) Provides [XDR] encode/decode and the reference implementation of [XDR-JSON]. **Maintained by SDF.** - `stellar-strkey` – [Code](https://github.com/stellar/rs-stellar-strkey) | [Docs](https://docs.rs/stellar-strkey) Provides Stellar Strkey (Address) [SEP-23] encoding/decoding. **Maintained by SDF.** [XDR]: ../../learn/fundamentals/data-format/xdr [XDR-JSON]: ../../learn/fundamentals/data-format/xdr-json.mdx [SEP-23]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md - `soroban-client` - [Code](https://github.com/rahul-soshte/rs-soroban-client) | [Docs](https://docs.rs/soroban-client/latest/soroban_client) A Rust library for interacting with the Soroban smart contract environment. It provides APIs to build and submit transactions, communicate with Stellar RPC servers, and supports all classic Stellar operations. **Maintained by a dedicated community developer.** - `rs-stellar-rpc-client` - [Code](https://github.com/stellar/rs-stellar-rpc-client) Rust Stellar RPC client. **Maintained by SDF.** ## iOS SDK [iOS SDK](https://github.com/Soneso/stellar-ios-mac-sdk) | [Docs](https://github.com/Soneso/stellar-ios-mac-sdk/tree/master/docs) | [Smart Contract Docs](https://github.com/Soneso/stellar-ios-mac-sdk/blob/master/soroban.md) **Maintained by a dedicated community developer.** The `stellar-ios-mac-sdk` is an open source Stellar SDK for iOS & Mac. It provides APIs to build transactions and connect to Horizon. It also provides functionality to deploy and invoke Soroban smart contracts and communicates with the Stellar RPC Server. The iOS SDK is maintained by dedicated community developer, Soneso. ## Flutter SDK [Flutter SDK](https://github.com/Soneso/stellar_flutter_sdk) | [Docs](https://github.com/Soneso/stellar_flutter_sdk/blob/master/soroban.md) **Maintained by a dedicated community developer.** The `stellar-flutter-sdk` is an open source Stellar SDK for Flutter developers. It provides APIs to build transactions and connect to Horizon. It also provides functionality to deploy and invoke Soroban smart contracts and communicates with the Stellar RPC Server. The Flutter Stellar SDK is maintained by dedicated community developer, Soneso. ## PHP SDK [PHP SDK](https://github.com/Soneso/stellar-php-sdk) | [Docs](https://github.com/Soneso/stellar-php-sdk/blob/main/soroban.md) **Maintained by a dedicated community developer.** The `stellar-php-sdk` is an open source Stellar SDK for PHP developers. It provides APIs to build transactions and connect to Horizon. It also provides functionality to deploy and invoke Soroban smart contracts and communicates with the Stellar RPC Server. The PHP Stellar SDK is maintained by dedicated community developer, Soneso. ## Kotlin Multiplatform SDK [KMP SDK](https://github.com/Soneso/kmp-stellar-sdk) | [Docs](https://github.com/Soneso/kmp-stellar-sdk/tree/main/docs) | [Smart Account Docs](https://github.com/Soneso/kmp-stellar-sdk/tree/main/docs/smart-accounts) **Maintained by a dedicated community developer.** The `kmp-stellar-sdk` is an open source Kotlin Multiplatform Stellar SDK. It targets JVM (Android, desktop, server-side), iOS, macOS, and JavaScript (browser, Node.js) from a single Kotlin codebase. It provides APIs to build transactions and connect to Stellar RPC and Horizon. It also provides functionality to deploy and invoke Soroban smart contracts and supports OpenZeppelin smart account integration with WebAuthn passkey authentication. The Kotlin Multiplatform Stellar SDK is maintained by a dedicated community developer, Soneso. ## Java SDK [Java SDK](https://github.com/lightsail-network/java-stellar-sdk) | [Docs](https://lightsail-network.github.io/java-stellar-sdk) **Maintained by a dedicated community developer.** `java-stellar-sdk` provides APIs to build transactions and connect to Horizon and also provides functionality to deploy and invoke Soroban smart contracts and communicates with the Stellar RPC Server. ## Go **The Go SDK is maintained by SDF.** This SDK is split up into separate packages, all of which you can find in the [Go SDK](https://github.com/stellar/go-stellar-sdk). The key libraries are: - `txnbuild` [SDK](https://github.com/stellar/go-stellar-sdk/tree/main/txnbuild) | [Docs](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild): enables the construction, signing, and encoding of Stellar transactions. - `Horizon Client` [SDK](https://github.com/stellar/go-stellar-sdk/tree/main/clients/horizonclient) | [Docs](https://godoc.org/github.com/stellar/go-stellar-sdk/clients/horizonclient): provides a web client for interfacing with Horizon server REST endpoints to retrieve ledger information and submit transactions built with `txnbuild`. - `RPC Client` [SDK](https://github.com/stellar/go-stellar-sdk/tree/main/clients/rpcclient) | [Docs](https://pkg.go.dev/github.com/stellar/go-stellar-sdk/clients/rpcclient): provides an SDK wrapper to invoke RPC endpoints. - [Ingest SDK](../../data/indexers/build-your-own/ingest-sdk/README.mdx): acquire and parse data from the Stellar network. ## C# .NET [C# .NET SDK](https://github.com/Beans-BV/dotnet-stellar-sdk) | [Docs](https://elucidsoft.github.io/dotnet-stellar-sdk) **This SDK is maintained by dedicated community developers.** --- ## Build smart contracts that will be deployed to the Stellar network # Contract SDKs Contract SDKs are used to build smart contracts that will be deployed to the Stellar network. :::note For Client and XDR SDKs, visit this [page](./client-sdks.mdx). ::: All SDKs are open-source; file a GitHub issue or pull request in the specific SDK repository if you have questions or suggestions. Each SDK has its own source code and documentation. Learn how to use a specific SDK by referring to the documentation. ## Soroban Rust SDK [Rust SDK](https://github.com/stellar/rs-soroban-sdk) | [Docs](https://docs.rs/soroban-sdk) **The Rust SDK is maintained by SDF.** The `soroban-sdk` Rust crate contains the Soroban Rust SDK for building smart contracts for Stellar. Report issues and share feedback about the `soroban-sdk` [here](https://github.com/stellar/rs-soroban-sdk/issues/new/choose). **Add `soroban-sdk` as a dependency** by using [crates.io](https://crates.io/crates/soroban-sdk) to find the version of the most recent SDK release. Add the following sections to the `Cargo.toml` to import the `soroban-sdk` and replace `$VERSION` with the released version. ```toml [dependencies] soroban-sdk = $VERSION [dev_dependencies] soroban-sdk = { version = $VERSION, features = ["testutils"] } ``` ## Solidity SDK [Hyperledger Solang compiler](https://github.com/hyperledger-solang/solang) | [Docs](https://solang.readthedocs.io/en/v0.3.4) **The Solang compiler is maintained by the Hyperledger community.** Solang is an llvm-based compiler for Solidity that can target multiple blockchains, including Stellar. The supported Solidity examples can be found within the [Solang repository](https://github.com/hyperledger-solang/solang/tree/main/examples/soroban). You can report issues and add requests for features to the Solang repository [here](https://github.com/hyperledger-solang/solang/issues/new/choose). Solang compiler also provides a Web IDE that you can use to compile, deploy and interact with Solidity contracts on Soroban. You can access the Web IDE [here](https://solang.io). ## AssemblyScript SDK [AssemblyScript SDK](https://github.com/Soneso/as-soroban-sdk) **The AssemblyScript SDK is maintained by dedicated community developers.** The `as-soroban-sdk` is an open source SDK that supports writing programs for the Soroban smart contract platform by using the AssemblyScript programming language. The AssemblyScript Soroban SDK is maintained by dedicated community developer, Soneso. Report issues and share feedback [here](https://github.com/Soneso/as-soroban-sdk/issues/new). ## OpenZeppelin Contract and Extension Crates OpenZeppelin Contracts are published in four crates: - Stellar Macros: https://crates.io/crates/stellar-macros - Stellar Access Control: https://crates.io/crates/stellar-access - Stellar Contract Utilities: https://crates.io/crates/stellar-contract-utils - Stellar Tokens: https://crates.io/crates/stellar-tokens Refer to the [OpenZeppelin for Stellar Contracts](../openzeppelin-contracts.mdx#openzeppelin-stellar-contracts-and-utilities) page for additional information. ## Stellar Axelar Std Derive Rust Crate Axelar has created a Rust crate with useful macros for Stellar smart contract development. Please see Rust Crate [`stellar_axelar_std_derive`](https://axelarnetwork.github.io/axelar-amplifier-stellar/stellar_axelar_std_derive/index.html) for Attribute Macros and Derive Macros, and additional information. ## Stellar Multicall / Router SDK The Stellar Router SDK is a lightweight multicall contract that enables developers to execute multiple Soroban contract calls within a single transaction. While Soroban normally allows only one contract invocation per transaction, the Router bundles multiple calls through a single entry point, effectively enable complex, multi-step workflows to be executed atomically. Stellar Router SDK: https://github.com/Creit-Tech/Stellar-Router-SDK --- ## Validators: Role, Setup, and Importance in Network Security & Consensus # Validators Introduction Stellar is a peer-to-peer network made up of nodes, which are computers that keep a common distributed [ledger](../learn/fundamentals/stellar-data-structures/ledgers.mdx), and that communicate to validate and add [transactions](../learn/fundamentals/transactions/operations-and-transactions.mdx) to it. Nodes use a program called Stellar Core — an implementation of the [Stellar Consensus Protocol](../learn/fundamentals/stellar-consensus-protocol.mdx) — to stay in sync as they work to agree on the validity of transaction sets and to apply them to the ledger. Generally, nodes reach consensus, apply a transaction set, and update the ledger every 3-5 seconds. This section of the docs explains how to run a validator node, which participates in consensus to validate transactions and determine network settings. A validator node _should not_ be used for network data access and transaction submission. There are two varieties of _non-validating_ nodes that can be used for those purposes, each of which has its own process for set up, interaction, maintenance, and monitoring. They are: 1. [**Stellar RPC Nodes**](../data/apis/rpc/README.mdx) can be used for simulating and/or submitting transactions, as well as exposing an RPC service to query and retrieve current network state. This is the best choice for real-time use-cases. 2. [**Galexie Nodes**](../data/indexers/build-your-own/galexie/README.mdx) can be used for retrieving and storing network data en masse for further processing. Notably, it does _not_ support transaction submission so is more suitable for indexers or analytics use-cases. If you are interested in running a validator node — because you issue an asset that you would like to help secure through transaction validation, because you want to help increase network health and decentralization, or because you want to participate in network governance — then this section of the docs is for you. It explains the technical and operational aspects of installing, configuring, and maintaining a Stellar Core validator node, and should help you figure out the best way to set up your Stellar integration. :::tip[Interested in Tier 1?] If your organization is evaluating or planning to join the Tier 1 quorum — the core group of organizations whose validators bear the safety and liveness of the network — see [Tier 1 Organizations](./tier-1-orgs.mdx) for requirements, estimated costs, and a step-by-step onboarding path. The Admin Guide below covers the technical setup for a single validator; the Tier 1 page covers what it takes to run three and join the quorum. ::: ## Node Setup Process The basic flow, which you can navigate through using the "Admin Guide" on the left, goes (roughly) like this: ### Initial Setup 1. Use the information on this _Introduction_ page to determine which [type of node](#types-of-nodes) you want to run. 2. [Prerequisite](./admin-guide/prerequisites.mdx) software must be installed (and configured according to your needs). 3. [Install](./admin-guide/installation.mdx) the Stellar Core software on your instance. 4. [Configure](./admin-guide/configuring.mdx) the Stellar Core software to suit your needs and environment. 5. [Prepare](./admin-guide/environment-preparation.mdx) your node instance and environment. This includes the optional step of setting your node up to [publish history archives](./admin-guide/publishing-history-archives.mdx) of the ledger. 6. [Start your node](./admin-guide/running-node.mdx) and join the network. 7. [Logging](./admin-guide/logging.mdx) and [monitoring](./admin-guide/monitoring.mdx) should be appropriately set up and used to meet your needs. ### Ongoing Requirements 8. [Maintenance](./admin-guide/maintenance.mdx) is required from time to time to keep your node up-to-date and participating in the network. 9. [Network upgrades](./admin-guide/network-upgrades.mdx) require validator consensus, and you will need to consider casting a vote in the event of a protocol upgrade. 10. [Soroban settings](./admin-guide/soroban-settings.mdx) are network-wide, configurable, and changes can be proposed by anyone. Similar to protocol upgrades, changes to these settings will require validator consensus, so you should be prepared to participate. ### Other Information - Stellar Core uses a robust [command line](./admin-guide/commands.mdx) tool to control and operate a node. We've gathered information on some of the most-used commands, and linked to further, more comprehensive CLI documentation. - We've collected some miscellaneous helpful and [advanced](./admin-guide/advanced.mdx) information that could be useful as you understand and implement your core node. ## Types of validator nodes {/* #types-of-nodes */} There are two types of validator nodes, and they perform the same basic functions: they run Stellar Core, connect to peers, submit transactions, and store the state of the ledger. The difference is this: a **Basic Validator** does not publish a history archive; a **Full Validator** does. :::info Non-validating nodes, like Stellar RPC or Galexie, bundle an optimized "Captive" Core to serve their operational needs. ::: ### Basic Validator #### Validating, no public archive A Basic Validator keeps track of the ledger and submits transactions for possible inclusion, but it is _not_ configured to publish history archives. It does require a secret key, and is [configured to participate in consensus](./admin-guide/configuring.mdx#validating-node) by voting on — and signing off on — changes to the ledger, meaning it supports the network and increases decentralization. The advantage: signatures can serve as official endorsements of specific ledgers in real time. That’s important if, for instance, you issue an asset on Stellar that represents a real-world asset: you can let your customers know that you will only honor transactions and redeem assets from ledgers signed by your validator, and in the unlikely scenario that something happens to the network, you can use your node as the final arbiter of truth. Setting up your node as a validator allows you to resolve any questions _up front and in writing_ about how you plan to deal with disasters and disputes. ### Full Validator #### Validating, offers public archive A Full Validator is the same as a Basic Validator except that it also publishes a [History Archive](./admin-guide/environment-preparation.mdx) containing snapshots of the ledger, including all transactions and their results. A Full Validator writes to an internet-facing blob store — such as AWS or Azure — so it's a bit more expensive and complex to run, but it also does the most to support the network’s resilience and decentralization. When other nodes join the network — or experience difficulty and temporarily fall out of sync — they can consult archives offered by Full Validators to catch up on the history of the network. Redundant archives prevent a single point of failure, and allow network participants to verify the veracity of a given history. Generally, organizations that run Full Validators are also part of — or on track to join — [Tier 1](./tier-1-orgs.mdx), which is a core group of network participants who run three Full Validators to contribute maximum redundancy. --- ## Admin Guide(4) Stellar Core is the program nodes use to communicate with other nodes to create and maintain the Stellar peer-to-peer network. It's an implementation of the Stellar Consensus Protocol configured to construct a chain of ledgers guaranteed to be in agreement across all participating nodes at all times. These pages describe various aspects of installing, configuring, and maintaining a `stellar-core` node. --- ## Advanced This page contains information that is useful to know but that should not stop somebody from running a node. ## Creating Your Own Private Network The [stellar-core GitHub repository] holds [a `testnet.md` file] which contains a short tutorial demonstrating how to configure and run a short-lived, isolated test network. ## Runtime Information: Start and Stop Stellar-core can be started directly from the command line, or through a supervision system such as `init`, `upstart`, or `systemd`. Stellar-core can be gracefully exited at any time by delivering `SIGINT` or pressing `CTRL-C`. It can be safely, forcibly terminated with `SIGTERM` or `SIGKILL`. The latter may leave a stale lock file in the `BUCKET_DIR_PATH`, and you may need to remove this file before stellar-core will restart. Broadly speaking, all components are designed to recover from abrupt termination. Stellar-core can also be packaged in a container system such as Docker, so long as `BUCKET_DIR_PATH` and the database are stored on persistent volumes. For an example, see [`stellar/quickstart`]. ## In-depth Architecture The [stellar-core GitHub repository] also contains [the `architecture.md` file], which describes how stellar-core is structured internally, how it is intended to be deployed, and the collection of servers and services needed to get the full functionality and performance. ## Reproducible Performance Testing With Stellar Supercluster [Stellar Supercluster] is a tool that enables running simulated networks of stellar-core nodes on Kubernetes clusters. One use case for Supercluster is running reproducible performance tests. The Supercluster GitHub repository contains a few documents to help users run theoretical maximum Transaction Per Second (TPS) tests: - [`doc/getting-started.md`] details dependencies for building and running Supercluster. - [`doc/eks.md`] details how to build a supercluster-compatible EKS cluster on AWS. - [`doc/theoretical-max-tps.md`] explains how to run the theoretical max TPS test on an EKS cluster. It also contains a table of results from SDF's own theoretical max TPS runs, as well as the configurations used to achieve those results. ### Other Supercluster Resources Supercluster is useful beyond reproducible performance testing. Some helpful resources to learn more about Supercluster include: - [`doc/measuring-transaction-throughput.md`] details additional maximum TPS tests beyond the theoretical max TPS test. - [`doc/missions.md`] lists all tests (referred to as "missions") that Supercluster supports. [stellar-core github repository]: https://github.com/stellar/stellar-core [a `testnet.md` file]: https://github.com/stellar/stellar-core/blob/master/docs/software/testnet.md [`stellar/quickstart`]: https://github.com/stellar/quickstart [the `architecture.md` file]: https://github.com/stellar/stellar-core/blob/master/docs/architecture.md [Stellar Supercluster]: https://github.com/stellar/supercluster/tree/main [`doc/getting-started.md`]: https://github.com/stellar/supercluster/blob/main/doc/getting-started.md [`doc/eks.md`]: https://github.com/stellar/supercluster/blob/main/doc/eks.md [`doc/theoretical-max-tps.md`]: https://github.com/stellar/supercluster/blob/main/doc/theoretical-max-tps.md [`doc/measuring-transaction-throughput.md`]: https://github.com/stellar/supercluster/blob/main/doc/measuring-transaction-throughput.md [`doc/missions.md`]: https://github.com/stellar/supercluster/blob/main/doc/missions.md --- ## Commands Stellar Core can be controlled using a robust CLI. :::info We will cover a selection of the _essential_ commands and syntax here, but the **very best resource** for utilizing the `stellar-core` command line is located in the [stellar-core GitHub repo]. Additionally, while the commands on this page are _CLI_ commands, there is an additional set of [_HTTP_ endpoint commands] that provide further administrative control over a running core node. ::: ## Get `--help` Anywhere The `--help` (aliases: `-h` or `-?`) option can be specified at _any place_ in the command line. It will show you the help message for the relevant command. Some example useage is as follows: ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg --help sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg run --help sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg --help new-db sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg catchup --help ``` ## Essential Commands For all stellar-core commands, options can _only_ by placed after the command. ### `new-db` The **`new-db`** command creates or restores the local database to the genesis ledger. #### `new-db` Options - `--minimal-for-in-memory-mode`: Reset the special database used only for in-memory mode. (see the `--in-memory` flag in [`run` options](#run-options)) ### `run` The **`run`** command will run the `stellar-core` node. #### `run` Options - `--disable-bucket-gc`: Keeps all, even old, buckets on disk. - `--metadata-output-stream `: Filename or file-descriptor number `fd:N` to stream metadata to. - `--wait-for-consensus`: Wait to hear from the network before voting, for validating nodes only. Certain features, such as `in-memory` mode options, have been deprecated, so they aren't listed here. ### `catchup` The **`catchup`** command will execute a catchup from history archives without connecting to the network. #### `catchup` Options - ``: (required) Destination ledger is any valid number or `current` and ledger count is any valid number or `max`. - `--archive `: Archive name to be used for catchup. Use `any` to select randomly. - `--trusted-checkpoint-hashes `: Get destination ledger hash from trusted output of `verify-checkpoints`. - `--output-file `: Output file. - `--disable-bucket-gc`: Keeps all, even old, buckets on disk. - `--extra-verification`: Verify all files from the archive for the catchup range. - `--trusted-hash `: Hash of the ledger to catchup to. - `--force-untrusted-catchup`: Force unverified catchup. - `--metadata-output-stream `: Filename or file-descriptor number `fd:N` to stream metadata to. - `--force-back`: Force ledger state to a previous state, preserving older historical data. :::info To reiterate, this page covers a selection of the _essential_ commands, but we've only scratched the surface. The **very best, most comprehensive resource** for utilizing the `stellar-core` command line is located in the [stellar-core GitHub repo]. ::: [stellar-core GitHub repo]: https://github.com/stellar/stellar-core/blob/master/docs/software/commands.md [_HTTP_ endpoint commands]: https://github.com/stellar/stellar-core/blob/master/docs/software/commands.md#http-commands --- ## Configuring(3) :::info Before attempting to configure stellar-core, it is highly recommended to first try running a private network or joining the test network. ::: ## Configuration Basics After you've [installed](./installation.mdx) Stellar Core, your next step is to complete a configuration file that specifies crucial things about your node — like whether it connects to the Testnet or the Mainnet public network, what database it writes to, and which other nodes are in its [quorum set](#choosing-your-quorum-set). All configuration for stellar-core is done with a [TOML](https://github.com/toml-lang/toml) file. By default, Stellar Core loads that file from `./stellar-core.cfg` and Debian packages will use the `/etc/stellar/stellar-core.cfg` file. You can specify a different file to load on the command line: ```bash stellar-core --conf betterfile.cfg ``` When installing using official Debian packages systemd unit file is configured to use `/etc/stellar/stellar-core.cfg` file. The examples in these docs don't specify `--conf betterfile.cfg` for the sake of brevity. This page will walk you through the key fields you'll need to include in your config file to get your node up and running. :::info This page attempts (as strictly as is possible) to focus on the specific fields and values that you may need to modify in your Stellar Core configuration file. To keep this page concise, we try to avoid conceptual context and background information. You will likely find some of that related information on the [prerequisites](./prerequisites.mdx) or [environment preparation](./environment-preparation.mdx) pages, if you'd like. ::: ### Example Configurations While we're looking at some of the config basics on this page, we've written this content to work best in conjunction with concrete config examples, so as you read through it, you may want to review the following: - The [complete example config] is not a real configuration, but thoroughly documents all possible configuration elements, as well as their default values. It's got every knob you can turn and every setting you can tweak along with detailed explanations of how to turn and tweak them. You don't need to put everything from the complete example config into your config file. Fields you omit will assume the default setting, and the default setting will generally serve you well. There are a few required fields, though, and this page will explain what they are. - If you want to connect to the Testnet network, check out the [example Testnet config]. As you can see, most of the fields from the [complete example config] are omitted since the default settings work fine. You can easily tailor this config to meet your Testnet needs. - If you want to connect to the Mainnet network, check out this [example Mainnet config] for a full validator. It includes a properly crafted quorum set with all the current [Tier 1 validators](../tier-1-orgs.mdx), which is a good place to start for most configurations. This node is set up to both [validate](#validating-node) and write history to a [public archive](./environment-preparation.mdx#configuring-to-publish-data-to-an-archive), but you can disable either feature by adjusting this config so it's a little lighter. Auditing of the P2P network is enabled by default, see the [overlay topology](./monitoring.mdx#overlay-topology-survey) section for more detail if you'd like to disable it ### Network Passphrase Use the `NETWORK_PASSPHRASE` field to specify whether your node connects to the Testnet or the Mainnet [network](../../networks/README.mdx). - `NETWORK_PASSPHRASE="Test SDF Network ; September 2015"` - `NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"` For more about the Network Passphrase and how it works, check out the [Networks section](../../networks/README.mdx#network-passphrases). ### Database You specify your node's database by using the aptly named `DATABASE` field of your config file, which you can can read more about in the [complete example config][complete-example-database]. It defaults to an in-memory database, but you can specify a path as per the example. ### Buckets The flat XDR files of Stellar Core are placed in a directory specified in the config file as `BUCKET_DIR_PATH`, which defaults to `buckets`. ## Validating Node :::note If you don't intend for your node to participate in consensus votes, you can skip ahead to configuring your [quorum set](#choosing-your-quorum-set) ::: If you want to validate, you must generate a public/private key for your node. Nodes shouldn't share keys. You should carefully _secure your private key_. If it is compromised, someone can send false messages to the network and those messages will look like they came from you. Generate a key pair for your validating node like this: ```bash stellar-core gen-seed ``` Your output should look something like: ```text Secret seed: SBAAOHEU4WSWX6GBZ3VOXEGQGWRBJ72ZN3B3MFAJZWXRYGDIWHQO37SY Public: GDMTUTQRCP6L3JQKX3OOKYIGZC6LG2O6K2BSUCI6WNGLL4XXCIB3OK2P ``` Add this secret seed to your config file, and mark the node as "validator": ```toml NODE_SEED="SBAAOHEU4WSWX6GBZ3VOXEGQGWRBJ72ZN3B3MFAJZWXRYGDIWHQO37SY mynode" NODE_IS_VALIDATOR=true NODE_HOME_DOMAIN= [[HOME_DOMAINS]] HOME_DOMAIN= QUALITY="MEDIUM" ``` :::note[Tier 1 quality rating] The example above uses `QUALITY="MEDIUM"`, which is appropriate for a new validator building a track record. If your organization is a [Tier 1 participant](../tier-1-orgs.mdx) (or aspiring to become one), you should declare your own organization as `QUALITY="HIGH"`. Note that `HIGH` quality requires [publishing a history archive](./publishing-history-archives.mdx) — the requirement is programmatically enforced. Declaring a lower quality level significantly reduces your weight in [leader election](#impact-of-validator-quality-on-nomination) and may limit your participation in consensus. ::: If you don't include a `NODE_SEED` or set `NODE_IS_VALIDATOR=true`, your node will still watch SCP and see all the data in the network, but it will not send validation messages. If you run multiple validators, make sure to set a common `HOME_DOMAIN` for them by setting the `NODE_HOME_DOMAIN` property to the same value. This will ensure your nodes get grouped correctly during [quorum set generation](#home-domains-array). You also need to include your other nodes in in your config file's [`VALIDATORS` array](#validators-array). If you want other validators to add your node to their quorum sets, you should also share your public key (`GDMTUTQ...`) by publishing a `stellar.toml` file on your home domain following specifications laid out by [SEP-20]. ## Choosing Your Quorum Set To create your quorum set, Stellar Core relies on two arrays of tables: `[[HOME_DOMAINS]]` and `[[VALIDATORS]]`. Check out the example config's [`HOME_DOMAINS` array] and [`VALIDATORS` array] to see them in action. :::info It is beneficial to take a brief detour here and explore some background information of validator quorums and network consensus. If you'd like, you can skip ahead and begin creating your [home domains array](#home-domains-array) now. Otherwise, read on, my friend! ::: No matter what kind of node you run — Basic or Full Validator — you need to select a quorum set, which consists of validators (grouped by organization) that your node checks with to determine whether to apply a transaction set to a ledger. If you want to know more about how quorum sets work, check this article about [how Stellar approaches quorums]. If you want to see what a quorum set consisting of all the Tier 1 validators looks like — a tried and true setup — check out the [public network config for a Full Validator]. A good quorum set: - aligns with your organization's priorities, - has enough redundancy to handle arbitrary node failures, and - maintains good quorum intersection. Since crafting a good quorum set is a difficult thing to do, stellar core _automatically_ generates a quorum set for you based on structured information you provide in your config file. You choose the validators you want to trust; stellar core configures them into an optimal quorum set. To generate a quorum set, stellar core: - Groups validators run by the same organization into a subquorum - Sets the threshold for each of those subquorums - Gives weights to those subquorums based on quality While this does not absolve you of all responsibility — you still need to pick trustworthy validators and keep an eye on them to ensure that they're consistent and reliable — it does make your life easier and reduces the chances for human error. ### Validator Discovery When you add a validating node to your quorum set, it's generally because you trust the _organization_ running the node: you trust SDF, not some anonymous Stellar public key. In order to create a self-verified link between a node and the organization that runs it, a validator declares a home domain on-chain using a [`set_options` operation](../../learn/fundamentals/transactions/list-of-operations.mdx#set-options), and publishes organizational information in a `stellar.toml` file hosted on that domain. To find out how that works, take a look at [SEP-20]. As a result of that link, you can look up a node by its Stellar public key and check the `stellar.toml` file to find out who runs it. It's possible to do that manually, but you can also just consult the list of nodes on [Obsrvr Radar](https://radar.withobsrvr.com/). If you decide to trust an organization, you can use that list to collect the information necessary to add their nodes to your configuration. When you look at that list, you will discover that the most reliable organizations actually run more than one validator, and adding all of an organization's nodes to your quorum set creates the redundancy necessary to sustain arbitrary node failure. When an organization with a trio of nodes takes one down for maintenance, for instance, the remaining two nodes vote on the organization's behalf, and the organization's network presence persists. ### Home Domains Array `[[HOME_DOMAINS]]` defines a superset of validators: when you add nodes hosted by the same organization to your configuration, they share a home domain, and the information in the `[[HOME_DOMAINS]]` table, specifically the quality rating, will automatically apply to every one of those validators. For each organization you want to add, create a separate `[[HOME_DOMAINS]]` table, and complete the following required fields: | Field | Requirements | Description | | --- | --- | --- | | HOME_DOMAIN | string | URL of home domain linked to a group of validators | | QUALITY | string | Rating for this organization's nodes: `HIGH`, `MEDIUM`, or `LOW` | Here is an example `[[HOME_DOMAINS]]` array, which creates two `[[HOME_DOMAINS]]` tables: ```toml [[HOME_DOMAINS]] HOME_DOMAIN="testnet.stellar.org" QUALITY="HIGH" [[HOME_DOMAINS]] HOME_DOMAIN="some-other-domain" QUALITY="LOW" ``` ### Validators Array For each node you would like to add to your quorum set, complete a `[[VALIDATORS]]` table with the following fields: | Field | Requirements | Description | | --- | --- | --- | | NAME | string | A unique alias for the node | | QUALITY | string | Rating for node (required unless specified in `[[HOME_DOMAINS]]`): `HIGH`, `MEDIUM`, or `LOW`. | | HOME_DOMAIN | string | URL of home domain linked to validator | | PUBLIC_KEY | string | Stellar public key associated with validator | | ADDRESS | string | Peer:port associated with validator (optional) | | HISTORY | string | archive GET command associated with validator (optional) | If the node's `HOME_DOMAIN` aligns with an organization defined in the `[[HOME_DOMAINS]]` array, the quality rating specified there will apply to the node. If you're adding an individual node that is _not_ covered in that array, you'll need to specify the `QUALITY` here. Here is an example, which creates three `[[VALIDATORS]]` tables: ```toml [[VALIDATORS]] NAME="sdftest1" HOME_DOMAIN="testnet.stellar.org" PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" ADDRESS="core-testnet1.stellar.org" HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_001/{0} -o {1}" [[VALIDATORS]] NAME="sdftest2" HOME_DOMAIN="testnet.stellar.org" PUBLIC_KEY="GCUCJTIYXSOXKBSNFGNFWW5MUQ54HKRPGJUTQFJ5RQXZXNOLNXYDHRAP" ADDRESS="core-testnet2.stellar.org" HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_002/{0} -o {1}" [[VALIDATORS]] NAME="rando-node" QUALITY="LOW" HOME_DOMAIN="rando.com" PUBLIC_KEY="GC2V2EFSXN6SQTWVYA5EPJPBWWIMSD2XQNKUOHGEKB535AQE2I6IXV2Z" ADDRESS="core.rando.com" ``` :::info[Important Note] Your quorum set is automatically configured based on the information you provide in the `[[VALIDATORS]]` and/or `[[HOME_DOMAINS]]` tables. Removing a validator will result in a new quorum set being generated and may have unintended consequence for you and other network participants. Be sure to carefully consider the implications of removing a validator from your configuration and follow the guidance to [coordinate with other validators](../tier-1-orgs.mdx#coordinate-with-other-validators) before making changes. ::: ### Validator Quality `QUALITY` is a required field for each node you add to your quorum set. Whether you specify it for a suite of nodes in a `[[HOME_DOMAINS]]` table, or for a single node in a `[[VALIDATORS]]` table, it means the same thing, and you have the same three rating options: `HIGH`, `MEDIUM`, or `LOW`. #### HIGH Quality **HIGH** quality validators are given the most weight in automatic quorum set configuration. Before assigning a high quality rating to a node, make sure it has low latency and good uptime, and that the organization running the node is reliable and trustworthy. A high quality validator: - publishes an archive, and - belongs to a suite of nodes that provide redundancy. Choosing redundant nodes is good practice. The archive requirement is programmatically enforced. #### MEDIUM Quality **MEDIUM** quality validators are nested below high quality validators, and their combined weight is equivalent to a _single high quality entity_. If a node doesn't publish an archive, but you deem it reliable, or have an organizational interest in including it in your quorum set, give it a medium quality rating. #### LOW Quality **LOW** quality validators are nested below medium quality validators, and their combined weight is equivalent to a _single medium quality entity_. Should they prove reliable over time, you can upgrade their rating to medium to give them a bigger role in your quorum set configuration. ### Automatic Quorum Set Generation :::info[Important Note] It is ideal to configure at least 4 entities in your quorum if you'll be using automatic quorum set generation. The tradeoff here is about fault tolerance: a quorum with fewer than 4 entities will tolerate **zero** node failures. Take this moment to ensure your configured quorum meets your required fault tolerance. ::: Once you add validators to your configuration, stellar core automatically generates a quorum set using the following rules: - Validators with the same home domain are automatically grouped together and given a threshold requiring a simple majority (`2f+1`) - Heterogeneous groups of validators are given a threshold assuming byzantine failure (`3f+1`) - Entities are grouped by `QUALITY` and nested from `HIGH` to `LOW` - `HIGH` quality entities are at the top, and are given decision-making priority - The combined weight of `MEDIUM` quality entities equals a single `HIGH` quality entity - The combined weight of `LOW` quality entities equals a single `MEDIUM` quality entity ![Diagram Automatic Quorum Set Generation](/assets/diagrams/validator_complete.png) _Diagram: Depiction of the nested quality levels and how they interact._ ### Quorum and Overlay Network In order to get in sync and run consensus, your validator needs to be able to connect to the Stellar Network. In most cases, it is sufficient to simply rely on node's built-in peer discovery. However, your validator can be configured to connect to specific peers via `KNOWN_PEERS` and `PREFERRED_PEERS` in the config file. These can be either domain names or IPs. This can be useful for troubleshooting. Additionally, configuring `PREFERRED_PEER_KEYS` with the keys from your quorum set might be a good idea to give priority to the nodes that allow you to reach consensus. Without those settings, your validator depends on other nodes on the network to forward you the right messages, which is typically done as a best effort. ### Impact of Validator Quality on Nomination The Stellar Consensus Protocol uses validator quality levels in determining which validator should nominate the next transaction set for inclusion in a ledger. This process of choosing the validator that will nominate the next transaction set is called _leader election_. To start, the leader election algorithm assigns organizations and validators weights as follows: - Let $O_q$ be the set of all organizations with a given quality level $q$. $O_q$ includes all explicitly defined organizations in the configuration file at quality level $q$ as well as a single virtual organization containing $O_r$ where $r$ is the next quality level below $q$. - All organizations of some quality level $q$ have weight $w_q$. - If an organization of quality level $q$ has $n$ nodes, then the weight of each node is $\frac{w_q}{n}$. - $w_\top$ is `1`, where $\top$ is the highest quality value assigned to any organization. - $w_\texttt{LOW} = 0$. - This allows a validator to participate in some SCP activities without being trusted to nominate transaction sets. - For all other $q$ values, $w_q = \frac{w_p}{10 \times \left|\ O_p\ \right|}$ where $p$ is the next quality value above $q$. The leader election algorithm then assigns each validator a probability of winning leader election of the validator's weight divided by the total weight of all validators. Altogether, these win probabilities ensure that Stellar Core's leader election algorithm has the following properties: 1. Higher quality organizations have a greater chance of winning leader election than lower quality organizations. 2. Organizations of equal quality have an equal chance of winning leader election. #### Validator Nomination Weight Example Let there be 3 `HIGH` quality organizations, 2 `MEDIUM` quality organizations, and 1 `LOW` quality organization. The weight of each organization is as follows: - Each `HIGH` quality organization has a weight of `1`. - Each `MEDIUM` quality organization has a weight of `1/(10 * 3)`, or `1/30`. - Each `LOW` quality organization has a weight of `0`. [complete example config]: https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg [example testnet config]: https://github.com/stellar/quickstart/blob/master/testnet/core/etc/stellar-core.cfg [example mainnet config]: https://github.com/stellar/packages/blob/master/docs/examples/pubnet-validator-full/stellar-core.cfg [complete-example-database]: https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg#L48 [`HOME_DOMAINS` array]: https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg#L671 [`VALIDATORS` array]: https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg#L688 [SEP-20]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md [how Stellar approaches quorums]: https://stellar.org/blog/developers/why-quorums-matter-and-how-stellar-approaches-them [public network config for a Full Validator]: https://github.com/stellar/packages/blob/master/docs/examples/pubnet-validator-full/stellar-core.cfg --- ## Environment Preparation ## Initialize the Database and Local State After configuring your [database](./configuring.mdx#database) and [buckets](./configuring.mdx#buckets) settings, before running Stellar Core for the first time, you must initialize the database: ```bash stellar-core new-db ``` This command will initialize the database, as well as the bucket directory, and then exit. You can also use this command if your database gets corrupted and you want to restart it from scratch. ### Automatic Maintenance Some tables in stellar-core are used to publish ledger data to history archives. If not managed properly, those tables will grow without bounds. To avoid this, a built-in scheduler will delete data from old ledgers that are not used anymore by other parts of the system. By default, stellar-core will perform this automatic maintenance. The configuration fields that control the automatic maintenance behavior are: - `AUTOMATIC_MAINTENANCE_PERIOD`, - `AUTOMATIC_MAINTENANCE_COUNT` If you need to regenerate the metadata, the simplest way is to replay ledgers for the range you're interested in after (optionally) clearing the database with the `new-db` command [referenced earlier](#initialize-the-database-and-local-state). In some cases automatic maintenance has just too much work to do in order to get back to the nominal state. This can occur following large catchup operations such as when performing a full catchup that may create a backlog of 10s of millions of ledgers. If this happens, database performance can be restored. The node will require some downtime while you perform the following recovery commands: 1. run the `maintenance` http command manually with a large number of ledgers, and 2. perform a database maintenance operation such as `VACUUM FULL` to reclaim/rebuild the database as needed. ### Metadata Snapshots and Restoration Some deployments of Stellar Core will want to retain metadata for the _entire history_ of the network. This metadata can be quite large and computationally expensive to regenerate anew by replaying ledgers in stellar-core from an empty initial database state, as described in the previous section. This can be especially costly if it must be run more than once. For instance, when bringing a new node online. Or when needing to reingest historical data to include more meta in your stored data (as happened in [Protocol 23](https://stellar.org/blog/developers/stellar-events-retroactive-events)). :::info Due to the very large size requirements, we **recommend against** retaining metadata for the whole network history unless absolutely necessary for your use-case. ::: Some operators therefore prefer to shut down their stellar-core processes and _take filesystem-level snapshots_ or _database-level dumps_ of the contents of Stellar Core's database and bucket directory, after metadata generation has occurred the first time. Such snapshots can then be restored, putting stellar-core in a state containing metadata without performing full replay. Any reasonably recent state will do — if such a snapshot is a little old, stellar-core will replay ledgers from whenever the snapshot was taken to the current network state anyways — but this procedure can greatly accelerate restoring validator nodes, or cloning them to create new ones. ## History Archives Stellar Core normally interacts with one or more history archives, which are configurable facilities where [Full Validators](../README.mdx#full-validator) store flat files containing history checkpoints: bucket files and history logs. History archives are usually off-site commodity storage services such as Amazon S3, Google Cloud Storage, Azure Blob Storage, or custom SCP/SFTP/HTTP servers. Use command templates in the config file to give the specifics of which services you will use and how to access them. The [example config] will demonstrate how to configure a history archive through command templates. ### Configuring to Get Data from an Archive No matter what kind of node you're running, you should configure it to `get` history from one or more public archives. You can configure any number of archives to download from: Stellar Core will automatically round-robin between them. When you're [choosing your quorum set](./configuring.mdx#choosing-your-quorum-set), you should include high-quality nodes — which, by definition, publish archives — and add the location for each node's archive in the `HISTORY` field in the [validators array](./configuring.mdx#validators-array). :::note If you notice a lot of errors related to downloading archives, you should ensure all archives in your configuration are up-to-date. You can review the [example Mainnet configuration] to see how you might use up-to-date Tier 1 validators for their history archives. ::: ### Configuring to Publish Data to an Archive Archive sections can also be configured with `put` and `mkdir` commands to cause the instance to publish to that archive (for nodes configured as [full validators](../README.mdx#full-validator)). The very first time you want to use your archive, _before starting your node_, you need to initialize it with: ```bash stellar-core new-hist ``` :::note In a secure, production, environment Stellar Core should be run as a dedicated user. Debian packages will ensure the `stellar` user exists on the system. For this reason you may need to run commands as the stellar user, for example: ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg new-hist ``` ::: More detailed guidance and strategies for publishing history archives can be found in the [publishing history archives](./publishing-history-archives.mdx) page. Please check there for more information. :::info[IMPORTANT:] - Make sure that you configure both `put` and `mkdir` if `put` doesn't automatically create sub-folders. - Writing to the same archive from different nodes is not supported and will result in undefined behavior, _potentially data loss_. - Do not run `new-hist` on an existing archive unless you want to erase it. ::: ## Other Preparation In addition, you should ensure that your operating environment is also functional. This means you will have considered and prepared the following. - [Clock synchronization](./prerequisites.mdx#clock-synchronization) via an NTP service such as chrony - [Logging](./logging.mdx) and log rotation - [Monitoring](./monitoring.mdx) and alerting infrastructure [example config]: https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg [example Mainnet configuration]: https://github.com/stellar/packages/blob/master/docs/examples/pubnet-validator-full/stellar-core.cfg --- ## Installing(3) There are three common ways to install and run Stellar Core: 1. **Using published [packages](#package-based-installation).** For production use, we recommend installing Stellar Core using published packages. 2. **Building from [source](#installing-from-source).** In some uncommon situations, or if you want to sacrifice convenience for maximum control, building from source may be necessary. 3. **Use a [Docker image](#docker-based-installation).** Using a Docker image is the quickest and easiest method, so it's a good choice for a lot of developers. ## Release Version Whichever method you use, you should make sure to install the latest [release](https://github.com/stellar/stellar-core/releases) since these builds are backwards compatible and are cumulative. The version number scheme that we follow is `protocol_version.release_number.patch_number`, where: - `protocol_version` is the maximum protocol version supported by that release (all versions are 100% backward compatible), - `release_number` is bumped when a set of new features or bug fixes not impacting the protocol are included in the release, and - `patch_number` is used when a critical fix has to be deployed. ## Package-Based Installation If you are using a recent LTS version of Ubuntu, we provide [`stellar-core`](https://github.com/stellar/stellar-core) and [`stellar-rpc`](https://github.com/stellar/stellar-rpc) in Debian binary package format. The packages are cryptographically signed by the Stellar Development Foundation and files can be validated on the system to confirm they were not tampered with. Debian packages utilize operating system built-in cryptographic verification during upgrades which mitigates many supply chain attacks. SDF package signing key fingerprint is **AEAF 01EE A6CA FCEF DDAE 8AA7 0463 8272 A136 B5A6** (A136B5A6) Currently we do not publish packages for RPM based Linux distributions. If you use such distribution we recommend using [docker images](#docker-based-installation) ### Configure SDF Apt Repository On The System ```bash sudo install -d /etc/apt/keyrings sudo curl -fsSL https://apt.stellar.org/SDF.asc -o /etc/apt/keyrings/SDF.asc sudo chmod a+r /etc/apt/keyrings/SDF.asc echo "deb [signed-by=/etc/apt/keyrings/SDF.asc] https://apt.stellar.org $(lsb_release -cs) stable" | sudo tee -a /etc/apt/sources.list.d/SDF.list ``` Optionally you can add testing repository. This is not recommended for production systems but may be useful for non-production systems using testnet: ```bash echo "deb [signed-by=/etc/apt/keyrings/SDF.asc] https://apt.stellar.org $(lsb_release -cs) stable testing" | sudo tee -a /etc/apt/sources.list.d/SDF.list ``` ### Install packages We publish multiple packages for convenience. | Package | Dependencies | Comments | | --- | --- | --- | | stellar-core | none | installs stellar-core binary, systemd service, logrotate script, documentation | | stellar-core-utils | none | installs useful command line tools (stellar-core-cmd, stellar-core-gap-detect) | | stellar-core-prometheus-exporter | none | installs a Prometheus exporter to facilitate ingesting stellar-core metrics | | stellar-core-postgres | stellar-core, PostgreSQL | configures a PostgreSQL server, creates a Stellar DB,role and system user, the default stellar-core configuration contained in this package will connect to the Testnet | | stellar-archivist | none | installs stellar-archivist cli tool for managing stellar-core History archives | To install a chosen package run: ```bash # To install stellar-core sudo apt-get update && apt-get install ``` ## Installing From Source The Stellar Core source code repository contains extensive and thorough instructions to build the software from source. Please [check out `INSTALL.md`](https://github.com/stellar/stellar-core/blob/master/INSTALL.md) for more information. ## Docker-Based Installation ### Development Environments SDF maintains a [quickstart image](https://github.com/stellar/quickstart) that runs a Stellar Core validator, a "Captive Core" bundled with Stellar RPC, and all other necessary components to support local development. It's a quick way to set up a default, non-validating, ephemeral configuration that should work for most developers. Additionally, the quickstart image can be spun up pre-configured for use as a Mainnet, Testnet, Futurenet, or Local network node. :::info The quickstart image is not intended to serve as a production-level instance node. Please plan your production instance(s) carefully. ::: ### Production Environments SDF also maintains a Stellar-Core-only standalone image, [`stellar/stellar-core`](https://hub.docker.com/r/stellar/stellar-core). Example usage: ```bash docker run stellar/stellar-core:latest help docker run stellar/stellar-core:latest gen-seed ``` To run the Stellar Core daemon you need to provide a configuration file: ```bash # Initialize postgres DB (see DATABASE config option) docker run -v "/path/to/config/dir:/etc/stellar/" stellar/stellar-core:latest new-db # Run stellar-core daemon in the background docker run -d -v "/path/to/config/dir:/etc/stellar/" stellar/stellar-core:latest run ``` The image utilizes deb packages so it's possible to confirm the checksum of the `stellar-core` binary in the docker image matches what is in the cryptographically signed deb package. See [package based installation section](#package-based-installation) for information. To calculate this checksum in the docker image you can run: ```bash docker run --entrypoint=/bin/sha256sum stellar/stellar-core:latest /usr/bin/stellar-core ``` --- ## Logging(Admin-guide) Stellar Core sends logs to standard error and `stellar-core.log` by default, configurable with the `LOG_FILE_PATH` field. Log messages are classified by progressive _priority levels_: `TRACE`, `DEBUG`, `INFO`, `WARNING`, `ERROR`, and `FATAL`. The logging system only emits those messages at or above its configured logging level. Log messages at different priority levels can be color-coded on standard error by setting `LOG_COLOR=true` in the config file. By default they are not color-coded. The log level can be controlled by configuration, the `--ll` command-line flag, or adjusted dynamically by administrative (HTTP) commands. To do so, run: ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command "ll?level=debug" ``` while your node is running. Log levels can also be adjusted on a partition-by-partition basis through the administrative interface. For example the history system can be set to `DEBUG`-level logging by running: ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command "ll?level=debug&partition=history" ``` Against a running system. :::info Please take a look at the [HTTP Commands reference] for more information about the partitions available for use in the `ll` command. ::: The default log level is `INFO`, which is moderately verbose and should emit progress messages every few seconds under normal operation. [HTTP Commands reference]: https://github.com/stellar/stellar-core/blob/master/docs/software/commands.md#http-commands --- ## Maintenance Maintenance here refers to anything involving taking your validator temporarily out of the network (to apply security patches, system upgrade, etc). As an administrator of a validator, you must ensure that the maintenance you are about to apply to the validator is safe for the overall network and for your validator. Safe means that the other validators that depend on yours will not be affected too much when you turn off your validator for maintenance and that your validator will continue to operate as part of the network when it comes back up. If you are changing some settings that may impact network wide settings such as protocol version, review [the network upgrades page](./network-upgrades.mdx). If you're changing your quorum set configuration, also read the [section on what to do](#special-considerations-during-quorum-set-updates). ## Recommended Steps to Perform as Part of a Maintenance We recommend performing the following steps in order (repeat sequentially as needed if you run multiple nodes). 1. Advertise your intention to others that may depend on you. Some coordination is required to avoid situations where too many nodes go down at the same time. 2. Dependencies should assess the health of their quorum 3. If there is no objection, take your instance down 4. When done, start your instance that should rejoin the network 5. The instance will be completely caught up when it's both `Synced` and _there is no backlog in uploading history_. ## Special Considerations During Quorum Set Updates When you join the ranks of node operators, it's also important to join the conversation. The best way to do that: follow the `#validator` channel on the [Stellar Developer Discord](https://discord.gg/stellardev). If you can't do that for some reason, sign up for the [Stellar Validators Google Group](https://groups.google.com/forum/#!forum/stellar-validators). Sometimes an organization needs to make changes that will impact the quorum sets of others: - taking a validator down for long period of time - adding new validators to their pool In both cases, it's crucial to stage the changes to preserve quorum intersection and general good health of the network: - Be careful about removing too many nodes from your quorum set _before_ the nodes are taken down. If different people remove different sets the remaining sets may not overlap between nodes and may cause network splits. - Watch out for adding too many nodes in your quorum set at the same time. If not done carefully, this can cause those nodes to overpower your configuration. Recommended steps are for the entity that adds/removes nodes to do so first between their own nodes, and then have people reflect those changes gradually (over several rounds) in their respective quorum configurations. --- ## Monitoring(4) Once your node is up and running, it's important to keep an eye on it to make sure it stays afloat and continues to contribute to the health of the overall network. To help with that, Stellar Core exposes vital information that you can use to monitor your node and diagnose potential problems. You can access this information using commands and inspecting Stellar Core's output. The first half of this page will cover this approach. You can also connect [Prometheus](#using-prometheus) to make monitoring easier, combine it with [Alertmanager](#configure-notifications-using-alertmanager) to automate notification, and use pre-built [Grafana dashboards](#visualize-metrics-using-grafana) to create visual representations of your node's well-being. :::info [**Obsrvr Radar**](https://radar.withobsrvr.com/), a community-run monitoring dashboard, can also be useful for viewing network health as a whole. You should keep a very close eye on your own node(s) using some of the tools suggested on this page, but monitoring performance of all the network's nodes can also be useful to understand how they all interact with each other. ::: However you decide to monitor, the most important thing is that you have a system in place to ensure that your integration keeps ticking. ## General Node Information If you run `sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'info'`, the output will look something like this: ```json { "info": { "build": "v20.4.0", "ledger": { "age": 0, "baseFee": 100, "baseReserve": 100000000, "closeTime": 0, "hash": "39c2a3cd4141b2853e70d84601faa44744660334b48f3228e0309342e3f4eb48", "maxTxSetSize": 100, "num": 1, "version": 20 }, "network": "Public Global Stellar Network ; September 2015", "peers": { "authenticated_count": 8, "pending_count": 2 }, "protocol_version": 20, "quorum": { "node": "GCRQF", "qset": { "agree": 22, "cost": 37256128, "delayed": 0, "disagree": 0, "fail_at": 6, "hash": "5c464e", "lag_ms": 1942, "ledger": 51251628, "missing": 1, "phase": "EXTERNALIZE" }, "transitive": { "critical": null, "intersection": true, "last_check_ledger": 51251539, "node_count": 24 } }, "startedOn": "2024-04-15T16:16:25Z", "state": "Synced!" } } ``` Some notable fields from this `info` endpoint are: - `build`: the build number for this Stellar Core instance - `ledger`: a representation of the local state of your node, which may be different from the network state if your node was disconnected from the network for example. Some important sub-fields: - `age`: time elapsed since this ledger closed (during normal operation less than 10 seconds) - `num`: ledger number - `version`: protocol version of this ledger - `network` the [network passphrase]([Networks section](../../networks/README.mdx#network-passphrases)) for the network this core instance is using - `peers`: information on the connectivity to the network - `authenticated_count`: the number of live connections - `pending_count`: the number of connections that are not fully established yet - `protocol_version`: the maximum version of the protocol that this instance recognizes - `state`: the node's synchronization status relative to the network - `quorum`: summary of the state of the SCP protocol participants, which is the same information returned by the `quorum` command ([see below](#quorum-health)). ### Quick-Reference Health Indicators The table below summarizes the most important fields to check at a glance. For full details on each, see the explanations above and the [quorum health](#quorum-health) section below. | Field | Where | Healthy | Investigate | | --- | --- | --- | --- | | `state` | `info` | `Synced!` | Any other value — node is not participating in consensus | | `ledger.age` | `info` | < 10 seconds | > 10 seconds — node may be falling behind | | `peers.authenticated_count` | `info` | ≥ 8 | < 3 — limited connectivity, may miss messages | | `quorum.qset.phase` | `info` | `EXTERNALIZE` | Other phases for extended periods — consensus stalled | | `quorum.qset.fail_at` | `info` / `quorum` | ≥ 2 | ≤ 1 — one more failure will halt your node | | `quorum.qset.missing` | `info` / `quorum` | None or few | Multiple nodes — check if quorum peers are down | | `quorum.transitive.intersection` | `info` / `quorum` | `true` | `false` — **critical**, network at risk of splitting | ## Peer Information The `peers` command returns information on the peers your node is connected to. This list is the result of both inbound connections from other peers and outbound connections from this node to other peers. If `compact=false` is used in the command, then it also returns some extra metrics on each peer such as the number of dropped messages. ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'peers' ``` The output will look something like: ```json { "authenticated_peers": { "inbound": [ { "address": "18.234.41.75", "elapsed": 6, "flow_control": { "local_capacity": { "flood": 200, "reading": 200 }, "local_capacity_bytes": { "flood": 300000 }, "peer_capacity": 175, "peer_capacity_bytes": 291340 }, "id": "SDF 1", "latency": 172, "olver": 32, "ver": "stellar-core 20.4.0 (7fc7671b8bc1ccc3b1f16a6ab83bc9f671db8b70)" } ], "outbound": [ { "address": "3.238.239.100:11625", "elapsed": 105, "flow_control": { "local_capacity": { "flood": 200, "reading": 200 }, "local_capacity_bytes": { "flood": 300000 }, "peer_capacity": 175, "peer_capacity_bytes": 291340 }, "id": "SDF 3", "latency": 172, "olver": 32, "ver": "stellar-core 20.4.0 (7fc7671b8bc1ccc3b1f16a6ab83bc9f671db8b70)" }, { "address": "85.190.254.217:11625", "elapsed": 295, "flow_control": { "local_capacity": { "flood": 200, "reading": 200 }, "local_capacity_bytes": { "flood": 300000 }, "peer_capacity": 169, "peer_capacity_bytes": 288408 }, "id": "SatoshiPay Frankfurt", "latency": 282, "olver": 32, "ver": "stellar-core 20.4.0 (7fc7671b8bc1ccc3b1f16a6ab83bc9f671db8b70)" } ] }, "pending_peers": { "inbound": ["211.249.63.74:11625", "45.77.5.118:11625"], "outbound": ["178.21.47.226:11625", "178.131.109.241:11625"] } } ``` ## Overlay Topology Survey There is a survey mechanism in the overlay that allows a validator to request connection information from other nodes on the network. The survey can be triggered from a validator, and will flood through the network like any other message, but will request information from other nodes about which nodes it is connected to and a brief summary of their per-connection traffic volumes. By default, a node will relay or respond to a survey message if the message originated from a node in the receiving node's transitive quorum. This behavior can be overridden by setting the `SURVEYOR_KEYS` field in the config file to a more restrictive set of nodes to relay or respond to. Set `SURVEYOR_KEYS` to `["$self"]` to opt-out of responding to survey requests entirely. The survey works in two phases: the collecting phase, and the reporting phase. During the collecting phase, nodes record information about themselves and their peers, such as the number of messages sent to a given peer. During the reporting phase, the surveyor requests the results of the collecting phase from nodes on the network. The surveyor begins the collecting phase by broadcasting a `TimeSlicedSurveyStartCollectingMessage`. The surveyor ends the collecting phase and initiates the reporting phase by broadcasting a `TimeSlicedSurveyStopCollectingMessage`. These "start/stop collecting" messages ensure that the collecting phase is roughly equal in duration for all nodes present during the entire collecting phase. We recommend sending the "stop collecting" message about 20 minutes after the "start collecting" message. If 30 minutes elapse without receiving a "stop collecting" message, the survey will automatically transition to the reporting phase. Additionally, the "stop/start collecting" messages contain a `nonce` field identifying the survey instance. The nonce in the "stop collecting" message must match the nonce from the "start collecting" message. The surveyor should choose a random 32-bit unsigned integer for the nonce. During the reporting phase, the surveyor sends `TimeSlicedSurveyRequestMessage`s to individual nodes to gather the information the node recorded during the collecting phase. ### Overlay Survey Script To simplify running an overlay survey, stellar-core ships with a script [`OverlaySurvey.py`](https://github.com/stellar/stellar-core/blob/master/scripts/OverlaySurvey.py) in the [`scripts` directory](https://github.com/stellar/stellar-core/tree/master/scripts). This script walks the network using the overlay survey HTTP endpoints to build a graph containing the topology of the overlay network. The script outputs this graph both in JSON format, as well as GraphML. You can analyze the GraphML file using a GraphML viewer such as [Gephi](https://gephi.org). An example usage of the survey script to run an overlay survey is as follows: ```bash $ python3 OverlaySurvey.py survey -n http://127.0.0.1:11626 -c 20 -sr sr.json -gmlw gmlw.graphml ``` The arguments this example uses are: - sub command `survey` - run survey and analyze - `-n NODE`, `--node NODE` - address of initial survey node - `-c DURATION`, `--collectDuration DURATION` - duration of survey collecting phase in minutes - `-gmlw GRAPHMLWRITE`, `--graphmlWrite GRAPHMLWRITE` - output file for graphml file - `-sr SURVEYRESULT`, `--surveyResult SURVEYRESULT` - output file for survey results Therefore, this example will run a survey from a stellar-core node running on the local machine with a collecting phase duration of 20 minutes and output the results to `sr.json` and `gmlw.graphml`. #### Attaching to a Running Survey Use the `--startPhase` option to attach the script to an already running survey. This may be necessary if something happened during the running of the script that caused the script to terminate early (such as losing connection with the surveyor node). `--startPhase` has three possible values: - `startCollecting`: Start a survey from the beginning of the collecting phase. This is the default value when `--startPhase` is unspecified. It indicates you would like to start a new survey from the beginning (that is, you are not attaching the script to an existing survey). - `stopCollecting`: Immediately broadcast a `TimeSlicedSurveyStopCollectingMessage` for the currently running survey and begin surveying individual nodes for results. Use this option if your survey is currently in the collecting phase and you'd like to move it to the reporting phase. - `surveyResults`: Begin surveying individual nodes for results. Use this option if your survey is in the reporting phase. #### More Survey Script Options The survey script contains additional subcommands and options to further analyze the survey results. You can find a complete list of subcommands by running: ```bash $ python3 OverlaySurvey.py -h ``` From there, you can run: ```bash $ python3 OverlaySurvey.py -h ``` for more info about any given subcommand. ### Example Survey Command Using HTTP Endpoints This section walks through an example of running an overlay survey by calling the survey HTTP endpoints directly. We highly recommend using the overlay survey script instead. This section may be useful to anyone who wants to modify the survey script, or anyone who is curious about the lower-level details of how the survey works and the data it includes. In this example, we have three nodes `GBBN`, `GDEX`, and `GBUI` (we'll refer to them by the first four letters of their public keys). We will execute the commands below from `GBUI`, and note that `GBBN` has `SURVEYOR_KEYS=["$self"]` in its config file, so `GBBN` will not relay or respond to any survey messages. ```bash # 1. Begin the surveyor collecting phase sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'startsurveycollecting?nonce=1234' # 2. Stop the surveyor collecting phase, and begin the reporting phase sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'stopsurveycollecting?nonce=1234' # 3. Request survey results from the `GBBN` node sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'surveytopologytimesliced?node=GBBNXPPGDFDUQYH6RT5VGPDSOWLZEXXFD3ACUPG5YXRHLTATTUKY42CL&inboundpeerindex=0&outboundpeerindex=0' # 4. Request survey results from the `GDEX` node sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'surveytopologytimesliced?node=GDEXJV6XKKLDUWKTSXOOYVOYWZGVNIKKQ7GVNR5FOV7VV5K4MGJT5US4&inboundpeerindex=0&outboundpeerindex=0' # 3. Retrieve and display the results of issued survey commands sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'getsurveyresult' ``` Once the responses are received, the `getsurveyresult` command will return a result like this: ```json { "backlog": [], "badResponseNodes": null, "surveyInProgress": true, "topology": { "GBBNXPPGDFDUQYH6RT5VGPDSOWLZEXXFD3ACUPG5YXRHLTATTUKY42CL": null, "GDEXJV6XKKLDUWKTSXOOYVOYWZGVNIKKQ7GVNR5FOV7VV5K4MGJT5US4": { "inboundPeers": [ { "bytesRead": 26392, "bytesWritten": 26960, "duplicateFetchBytesRecv": 0, "duplicateFetchMessageRecv": 0, "duplicateFloodBytesRecv": 10424, "duplicateFloodMessageRecv": 43, "messagesRead": 93, "messagesWritten": 96, "nodeId": "GBBNXPPGDFDUQYH6RT5VGPDSOWLZEXXFD3ACUPG5YXRHLTATTUKY42CL", "secondsConnected": 22, "uniqueFetchBytesRecv": 0, "uniqueFetchMessageRecv": 0, "uniqueFloodBytesRecv": 11200, "uniqueFloodMessageRecv": 46, "version": "v12.2.0-46-g61aadd29" }, { "bytesRead": 32204, "bytesWritten": 31212, "duplicateFetchBytesRecv": 0, "duplicateFetchMessageRecv": 0, "duplicateFloodBytesRecv": 11200, "duplicateFloodMessageRecv": 46, "messagesRead": 115, "messagesWritten": 112, "nodeId": "GBUICIITZTGKL7PUBHUPWD67GDRAIYUA4KCOH2PUIMMZ6JQLNVA7C4JL", "secondsConnected": 23, "uniqueFetchBytesRecv": 176, "uniqueFetchMessageRecv": 2, "uniqueFloodBytesRecv": 14968, "uniqueFloodMessageRecv": 62, "version": "v12.2.0-46-g61aadd29" } ], "numTotalInboundPeers": 2, "numTotalOutboundPeers": 0, "maxInboundPeerCount": 64, "maxOutboundPeerCount": 8, "addedAuthenticatedPeers": 0, "droppedAuthenticatedPeers": 0, "p75SCPFirstToSelfLatencyMs": 72, "p75SCPSelfToOtherLatencyMs": 112, "lostSyncCount": 0, "isValidator": false, "outboundPeers": null } } } ``` In this example, note that the node `GBBN` under the `topology` field has a `null` value because it's configured to not respond to the survey message. Some notable fields from this `getsurveyresult` endpoint are: - `backlog`: List of nodes for which the survey request are yet to be sent - `badResponseNodes`: List of nodes that sent a malformed response - `topology`: Map of nodes to connection information - `inboundPeers`/`outboundPeers`: List of connection information by nodes - `averageLatencyMs`: Average latency with this peer in milliseconds. - `bytesRead`: The total number of bytes read from this peer. - `bytesWritten`: The total number of bytes written to this peer. - `duplicateFetchBytesRecv`: The number of bytes received that were duplicate transaction sets and quorum sets. - `duplicateFetchMessageRecv`: The count of duplicate transaction sets and quorum sets received from this peer. - `duplicateFloodBytesRecv`: The number of bytes received that were transactions and SCP votes duplicates. - `duplicateFloodMessageRecv`: The count of duplicate transactions and SCP votes received from this peer. - `messagesRead`: The total number of messages read from this peer. - `messagesWritten`: The total number of messages written to this peer. - `nodeId`: Node's public key. - `secondsConnected`: The total number of seconds this peer has been connected to the surveyed node. - `uniqueFetchBytesRecv`: The number of bytes received that were unique transaction sets and quorum sets. - `uniqueFetchMessageRecv`: The count of unique transaction sets and quorum sets received from this peer. - `uniqueFloodBytesRecv`: The number of bytes received that were unique transactions and SCP votes. - `uniqueFloodMessageRecv`: The count of unique transactions and SCP votes received from this peer. - `version`: stellar-core version. - `numTotalInboundPeers`/`numTotalOutboundPeers`: The number of total inbound and outbound peers this node is connected to. The response will have a random subset of 25 connected peers per direction (inbound/outbound). These fields tell you if you're missing nodes so you can send another request out to get another random subset of nodes. - `maxInboundPeerCount`/`maxOutboundPeerCount`: The number of total inbound and outbound peers that this node can accept. These fields correspond to stellar-core configurations `MAX_ADDITIONAL_PEER_CONNECTIONS` and `TARGET_PEER_CONNECTIONS`, respectively. - `addedAuthenticatedPeers`: The number of authenticated peers added. - `droppedAuthenticatedPeers`: The number of authenticated peers dropped. - `p75SCPFirstToSelfLatencyMs`: 75th percentile latency to hear about new SCP messages in milliseconds. - `p75SCPSelfToOtherLatencyMs`: 75th percentile latency for other nodes to hear this node's SCP messages in milliseconds. - `lostSyncCount`: The number of times this node lost sync. - `isValidator`: Is this node a validator? ## Quorum Health To help node operators monitor their quorum sets and maintain the health of the overall network, Stellar Core also provides metrics on other nodes in your quorum set. You should monitor them to make sure they're up and running, and that your quorum set is maintaining good overlap with the rest of the network. ### Quorum Set Diagnostics The `quorum` command allows you to diagnose problems with the quorum set of the local node. If you run: ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'quorum' ``` The output will look something like: ```json { "node": "GCTSF", "qset": { "agree": 6, "cost": 20883268, "delayed": null, "disagree": null, "fail_at": 2, "fail_with": ["sdf_watcher1", "sdf_watcher2"], "hash": "d5c247", "lag_ms": { "sdf_watcher1": 192, "sdf_watcher2": 215, "sdf_watcher3": 79, "stronghold1": 321, "eno": 266, "tempo.eu.com": 225, "satoshipay": 249 }, "ledger": 24311847, "missing": ["stronghold1"], "phase": "EXTERNALIZE", "value": { "t": 3, "v": [ "sdf_watcher1", "sdf_watcher2", "sdf_watcher3", { "t": 3, "v": ["stronghold1", "eno", "tempo.eu.com", "satoshipay"] } ] } }, "transitive": { "critical": [["GDM7M"]], "intersection": true, "last_check_ledger": 24311536, "node_count": 21 } } ``` This output has two main sections: `qset` and `transitive`. The former describes the node and its quorum set. The latter describes the transitive closure of the node's quorum set. ### Per-node Quorum-set Information Entries to watch for in the `qset` section, which describe the node and its quorum set, are: - `agree`: the number of nodes in the quorum set that seem to be up and running as expected. The local node has no reason to believe that this node is `delayed`, `disagree` or `missing`. Note that `agree` has nothing to do with SCP terms such as "accept" or "confirming". - `delayed`: the nodes that are participating in consensus but seem to be behind. - `disagree`: the nodes that are participating but disagree with this instance. - `fail_at`: the number of failed nodes that _would_ cause this instance to halt. - `fail_with`: an example of such potential failure. - `missing`: the nodes that seem down during this consensus round. - `value`: the quorum set used by this node (`t` is the threshold expressed as a number of nodes). In the example above, 6 nodes are functioning properly, one is down (`stronghold1`), and the instance will fail if any two nodes still working (or one node and one inner-quorum-set) fail as well. If a node is stuck in state `Joining SCP`, this command allows to quickly find the reason: - too many validators missing (down or without a good connectivity), solutions are: - [adjust your quorum set](./configuring.mdx#choosing-your-quorum-set) (thresholds, grouping, etc.) based on the nodes that are not missing - try to get a [better connectivity path](./configuring.mdx#quorum-and-overlay-network) to the missing validators - network split would cause SCP to stick because of nodes that disagree. This would happen if either there is a bug in SCP, the network does not have quorum intersection, or the disagreeing nodes are misbehaving (compromised, etc.). Note that the node not being able to reach consensus does not mean that the network as a whole will be unable to reach consensus (and the opposite is true, the network may fail because of a different set of validators failing). You can get a sense of the quorum set health of a different node using using: ```bash # the `NAME` of a validator sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'quorum?node=$sdf1' # OR the `PUBLIC_KEY` of a validator sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'quorum?node=@GABCDE' ``` Overall network health can be evaluated by walking through all nodes and looking at their health. Note that this is only an approximation, as remote nodes may not have received the same messages (in particular: `missing` for other nodes is not reliable). ### Transitive Closure Summary Information When showing quorum-set information about the local node rather than some other node, a summary of the transitive closure of the quorum set is also provided in the `transitive` field. This has several important sub-fields: - `last_check_ledger`: the last ledger in which the transitive closure was checked for quorum intersection. This will reset when the node boots and whenever a node in the transitive quorum changes its quorum set. It may lag behind the last-closed ledger by a few ledgers depending on the computational cost of checking quorum intersection. - `node_count`: the number of nodes in the transitive closure, which are considered when calculating quorum intersection. - `intersection`: whether or not the transitive closure enjoyed quorum intersection at the most recent check. This is of **utmost importance** in preventing network splits. It should always be true. If it is ever false, one or more nodes in the transitive closure of the quorum set is _currently_ misconfigured, and the network is at risk of splitting. Corrective action should be taken immediately, for which two additional sub-fields will be present to help suggest remedies: - `last_good_ledger`: this will note the last ledger for which the `intersection` field was evaluated as true; if some node reconfigured at or around that ledger, reverting that configuration change is the easiest corrective action to take. - `potential_split`: this will contain a pair of lists of validator IDs, which is a potential pair of disjoint quorums allowed by the current configuration. In other words, a possible split in consensus allowed by the current configuration. This may help narrow down the cause of the misconfiguration: likely it involves too-low a consensus threshold in one of the two potential quorums, and/or the absence of a mandatory trust relationship that would bridge the two. - `critical`: an "advance warning" field that lists nodes that _could cause_ the network to fail to enjoy quorum intersection, if they were misconfigured sufficiently badly. In a healthy transitive network configuration, this field will be `null`. If it is non-`null` then the network is essentially "one misconfiguration" (of the quorum sets of the listed nodes) away from no longer enjoying quorum intersection, and again, corrective action should be taken: careful adjustment to the quorum sets of _nodes that depend on_ the listed nodes, typically to strengthen quorums that depend on them. ### Detailed Transitive Quorum Analysis The quorum endpoint can also retrieve detailed information about the transitive quorum. This is a format that's easier to process than what `scp` returns, as it doesn't contain all SCP messages. ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'quorum?transitive=true' ``` The output looks something like: ```json { "critical": null, "intersection": true, "last_check_ledger": 121235, "node_count": 4, "nodes": [ { "distance": 0, "heard": 121235, "node": "GB7LI", "qset": { "t": 2, "v": ["sdf1", "sdf2", "sdf3"] }, "status": "tracking", "value": "[ txH: d99591, ct: 1557426183, upgrades: [ ] ]", "value_id": 1 }, { "distance": 1, "heard": 121235, "node": "sdf2", "qset": { "t": 2, "v": ["sdf1", "sdf2", "sdf3"] }, "status": "tracking", "value": "[ txH: d99591, ct: 1557426183, upgrades: [ ] ]", "value_id": 1 }, { "distance": 1, "heard": 121235, "node": "sdf3", "qset": { "t": 2, "v": ["sdf1", "sdf2", "sdf3"] }, "status": "tracking", "value": "[ txH: d99591, ct: 1557426183, upgrades: [ ] ]", "value_id": 1 }, { "distance": 1, "heard": 121235, "node": "sdf1", "qset": { "t": 2, "v": ["sdf1", "sdf2", "sdf3"] }, "status": "tracking", "value": "[ txH: d99591, ct: 1557426183, upgrades: [ ] ]", "value_id": 1 } ] } ``` The output begins with the same summary information as in the `transitive` block of the non-transitive query (if queried for the local node), but also includes a `nodes` array that represents a walk of the transitive quorum centered on the query node. Notable fields contained in this response are: - `node`: the identity of the validator - `distance`: how far that node is from the root node (i.e., how many quorum set hops) - `heard`: the latest ledger sequence number at which this node cast a vote - `qset`: the node's quorum set - `status`: one of `behind|tracking|ahead` (compared to the root node) or `missing|unknown` (when there are no recent SCP messages for that node) - `value_id`: a unique ID for what the node is voting for (allows you to quickly tell if nodes are voting for the same thing) - `value`: what the node is voting for ## Using Prometheus Monitoring `stellar-core` using Prometheus is by far the simplest solution, especially if you already have a Prometheus server within your infrastructure. Prometheus is a free and open source time-series database with a simple yet incredibly powerful query language `PromQL`. Prometheus is also tightly integrated with Grafana, so you can render complex visualisations with ease. In order for Prometheus to scrape `stellar-core` application metrics, you will need to install the stellar-core-prometheus-exporter (`apt-get install stellar-core-prometheus-exporter`) and configure your Prometheus server to scrape this exporter (default port: `9473`). On top of that grafana can be used to visualize metrics. ### Install a Prometheus Server Within your Infrastructure Installing and configuring a Prometheus server is out of scope of this document, however it is a fairly simple process: Prometheus is a single Go binary which you can download from https://prometheus.io/docs/prometheus/latest/installation/. ### Install the `stellar-core-prometheus-exporter` The stellar-core-prometheus-exporter is an exporter that scrapes the `stellar-core` metrics endpoint (`http://localhost:11626/metrics`) and renders these metrics in the Prometheus text-based format available for Prometheus to scrape and store in its time series database. The exporter needs to be installed on every Stellar Core node you wish to monitor. ```bash apt-get install stellar-core-prometheus-exporter ``` You will need to open up port `9473` between your Prometheus server and all your Stellar Core nodes for your Prometheus server to be able to scrape metrics. ### Point Prometheus to stellar-core-prometheus-exporter Pointing your Prometheus instance to the exporter can be achieved by manually configuring a scrape job; however, depending on the number of hosts you need to monitor this can quickly become unwieldy. Luckily, the process can also be automated using Prometheus' various "service discovery" plugins. For example with AWS hosted instance you can use the `ec2_sd_config` plugin. #### Manual ```yaml - job_name: "stellar-core" scrape_interval: 10s scrape_timeout: 10s static_configs: - targets: [ "core-node-001.example.com:9473", "core-node-002.example.com:9473", ] # stellar-core-prometheus-exporter default port is 9473 - labels: application: "stellar-core" ``` #### Using Service Discovery (EC2) ```yaml - job_name: stellar-core scrape_interval: 10s scrape_timeout: 10s ec2_sd_configs: - region: eu-west-1 port: 9473 relabel_configs: # ignore stopped instances - source_labels: [__meta_ec2_instance_state] regex: stopped action: drop # only keep with `core` in the Name tag - source_labels: [__meta_ec2_tag_Name] regex: "(.*core.*)" action: keep # use Name tag as instance label - source_labels: [__meta_ec2_tag_Name] regex: "(.*)" action: replace replacement: "${1}" target_label: instance # set application label to stellar-core - source_labels: [__meta_ec2_tag_Name] regex: "(.*core.*)" action: replace replacement: stellar-core target_label: application ``` ### Create Alerting Rules Once Prometheus scrapes metrics we can add alerting rules. Recommended rules are [**here**](https://github.com/stellar/packages/blob/master/docs/stellar-core-alerting.rules) (require Prometheus 2.0 or later). Copy rules to _/etc/prometheus/stellar-core-alerting.rules_ on the Prometheus server and add the following to the prometheus configuration file to include the file: ```yaml rule_files: - "/etc/prometheus/stellar-core-alerting.rules" ``` Rules are documented in-line,and we strongly recommend that you review and verify all of them as every environment is different. ### Configure Notifications Using Alertmanager Alertmanager is responsible for sending notifications. Installing and configuring an Alertmanager server is out of scope of this document, however it is a fairly simple process. Official documentation is [here](https://github.com/prometheus/alertmanager). All recommended alerting rules have "severity" label: - **critical** normally require immediate attention. They indicate an ongoing or very likely outage. We recommend that critical alerts notify administrators 24x7 - **warning** normally can wait until working hours. Warnings indicate problems that likely do not have production impact but may lead to critical alerts or outages if left unhandled The following example alertmanager configuration demonstrates how to send notifications using different methods based on severity label: ```yaml global: smtp_smarthost: localhost:25 smtp_from: alertmanager@example.com route: receiver: default-receiver group_by: [alertname] group_wait: 30s group_interval: 5m repeat_interval: 1h routes: - receiver: critical-alerts match: severity: critical - receiver: warning-alerts match: severity: warning receivers: - name: critical-alerts pagerduty_configs: - routing_key: - name: warning-alerts slack_configs: - api_url: https://hooks.slack.com/services/slack/warning/channel/webhook - name: default-receiver email_configs: - to: alerts-fallback@example.com ``` In the above examples alerts with severity "critical" are sent to pagerduty and warnings are sent to slack. ### Useful Exporters You may find the below exporters useful for monitoring your infrastructure as they provide incredible insight into your operating system and database metrics. Installing and configuring these exporters is out of the scope of this document but should be relatively straightforward. - [node_exporter](https://prometheus.io/docs/guides/node-exporter) can be used to track all operating system metrics. - [postgresql_exporter](https://github.com/wrouesnel/postgres_exporter) can be used to monitor the local stellar-core database. ### Visualize Metrics Using Grafana Once you've configured Prometheus to scrape and store your stellar-core metrics, you will want a nice way to render this data for human consumption. Grafana offers the simplest and most effective way to achieve this. Installing Grafana is out of scope of this document but is a very simple process, especially when using the [prebuilt apt packages](https://grafana.com/docs/installation/debian/#apt-repository) We recommend that administrators import the following two dashboards into their Grafana deployments: | Dashboard | Grafana ID | Purpose | | --- | --- | --- | | [Stellar Core Monitoring](https://grafana.com/grafana/dashboards/10603) | `10603` | Key metrics, node status, and common problems. Start here for troubleshooting. | | [Stellar Core Full](https://grafana.com/grafana/dashboards/10334) | `10334` | All metrics from the exporter. For in-depth analysis. | To import: in Grafana, go to **Dashboards → Import**, enter the dashboard ID, and select your Prometheus data source. --- ## Upgrading the Network The network itself has network wide settings that can be updated. This is performed by validators voting for and agreeing to new values the same way that consensus is reached for transaction sets, etc. A node can be configured to vote for upgrades using the `upgrades` endpoint . See [Commands](./commands.mdx) for more information. The network settings are: - the version of the protocol used to process transactions; - the maximum number of transactions that can be included in a given ledger close. Separate setting for Soroban and classic transactions; - the cost (fee) associated with processing operations; - the base reserve used to calculate the lumen balance needed to store non-Soroban data in the ledger; and - generalized Soroban network settings stored in `ConfigSettingsEntries`. When the network time is later than the `upgradetime` specified in the `upgrades` command, the validator will vote to update the network to the value specified in that `upgrades` command. If the network time is passed the `upgradetime` by more than 12 hours, the upgrade will be ignored. When a validator is armed to change network values, the output of `info` will contain information about the vote. For a new value to be adopted, the same level of consensus between nodes needs to be reached as for transaction sets. ## Important Notes on Network Wide Settings Changes to network wide settings have to be orchestrated properly between validators as well as non validating nodes. The general process validators have used to propose and vote on network settings is as follows: 1. A change is vetted between operators (changes can be bundled). 2. An effective date in the future is picked for the change to take effect (controlled by the `upgradetime` parameter of the `upgrades` command). 3. If applicable, communication is sent out to all network users. This careful orchestration plays an important part in the Stellar network's functioning. This planning process is undertaken to avoid nodes misbehaving, socialize new features included in a protocol upgrade so the ecosystem is aware and can take advantage of them, and it also gives the ecosystem time to ready themselves for breaking changes that can have an effect (either directly or indirectly) on their Stellar integrations. An improper plan may cause issues such as: - nodes missing consensus (aka "getting stuck"), and having to use history to rejoin - network reconfiguration taking effect at a non deterministic time (causing fees to change ahead of schedule for example) For more information, take a look at how versioning takes into account network upgrades in [the `versioning.md` document](https://github.com/stellar/stellar-core/blob/master/docs/versioning.md) in the stellar-core GitHub repository. ## Upgrading Soroban Settings The mechanism to update Soroban settings is more complex than updating something like the `baseReserve`. The `upgrades` endpoint in stellar-core will require validators to vote on a serialized `ConfigUpgradeSetKey`, which contains a contractID and the SHA-256 hash of the `ConfigUpgradeSet` that will be applied to the existing settings. The serialized `ConfigUpgradeSet` must exist in the ledger as `Temporary` `ContractData` under the contractID specified earlier and with the `SCVal` `Bytes` key that contains the SHA-256 hash of the `ConfigUpgradeSet`. This means that someone wishing to propose a setting upgrade will need to create a smart contract that writes the `ConfigUpgradeSet` bytes into `ContractData` (remember, a `Temporary` entry is required), invoke it to write the upgrade xdr, and then share the serialized `ConfigUpgradeSetKey` to vote on. We have much more detail on the [next page](./soroban-settings.mdx) about _how_ to craft an upgrade proposal for these settings. Be sure to read and understand that. ## Example Upgrade Command By way of example, here is the `upgrades` command used to upgrade the protocol version to version 9 on January-31-2018. ```bash # arm the node to vote for the upgrade sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'upgrades?mode=set&upgradetime=2018-01-31T20:00:00Z&protocolversion=9' # view the status of the node sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command info ``` At this point `info` will tell you that the node is setup to vote for this upgrade: ```json "status" : [ "Armed with network upgrades: upgradetime=2018-01-31T20:00:00Z, protocolversion=9" ] ``` --- ## Prerequisites(3) You can install Stellar Core a [number of different ways](./installation.mdx), and once you do, you can [configure](./configuring.mdx) it to participate in the network on a several [different levels](../README.mdx#types-of-nodes): it can be either a Basic Validator or a Full Validator. No matter how you install Stellar Core or what kind of node you run, however, you need to set up and connect to the peer-to-peer network and store the state of the ledger in a SQL [database](./configuring.mdx#database). ## Hardware Requirements :::info CPU, RAM, Disk and network depends on network activity. If you decide to collocate certain workloads, you will need to take this into account. ::: :::tip[For Tier 1 organizations] [Tier 1 organizations](../tier-1-orgs.mdx) run three geographically dispersed Full Validators, each meeting these requirements independently. Each node needs its own hardware, its own unique validator key, and its own history archive. Plan for three times the resources listed below, spread across different data centers or cloud regions. See [Tier 1 Organizations](../tier-1-orgs.mdx) for the full requirements and onboarding path. ::: Stellar Core is designed to run on relatively modest hardware so that a whole range of individuals and organizations can participate in the network, and basic nodes should be able to function pretty well without tremendous overhead. That said, the more you ask of your node, the greater the requirements. The following recommendations were verified against production nodes in April 2024. Hardware requirements grow with network activity; check the [stellar-core releases](https://github.com/stellar/stellar-core/releases) for any notes on updated requirements. | Node Type | CPU | RAM | Disk | AWS SKU | Google Cloud SKU | | --- | --- | --- | --- | --- | --- | | Core Validator Node | 8 vCPUs @ 3.4 GHz | 16 GB | 100 GB NVMe SSD\* (10,000 IOPS) | [c5d.2xlarge] | [n4-highcpu-8] | PostgreSQL co-located on the same machine performs well at this spec — a separate database host is not required for a single validator. _\* Disk sizing assumes a 30-day retention window (`AUTOMATIC_MAINTENANCE_COUNT` at default). See [Storage](#storage) below for details._ {/* Last verified: April 2024. If you're an SDF maintainer updating this, bump the date in the paragraph above. */} ## Stellar Network Access Stellar Core interacts with the peer-to-peer network to keep a distributed ledger in sync, which means that your node needs to make certain [TCP ports](https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_ports) available for inbound and outbound communication. ### Inbound A Stellar Core node needs to allow all IPs to connect to its `PEER_PORT` over TCP. You can specify a port when you [configure] Stellar Core, but most people use the default, which is **11625**. ### Outbound A Stellar Core node needs to connect to other nodes on the internet via their `PEER_PORT` over TCP. You can find information about other nodes' `PEER_PORT`s on a network explorer like [Obsrvr Radar](https://radar.withobsrvr.com), but most use the default port for this as well, which is (again) **11625**. ## Internal System Access Stellar Core also needs to connect to certain internal systems, though exactly how this is accomplished can vary based on your setup. ### Inbound - Stellar Core exposes an _unauthenticated_ HTTP endpoint on its `HTTP_PORT`. You can specify a port when you [configure] Stellar Core, but most people use the default, which is **11626**. - The `HTTP_PORT` is used by other systems (such as Stellar RPC) to submit transactions, so this port may have to be exposed to the rest of your internal IP addresses. - It's also used to query Stellar Core [info](./commands.mdx) and provide [metrics](./monitoring.mdx). - And to perform administrative commands such as [scheduling upgrades](./network-upgrades.mdx) and changing log levels - For more on that, see [commands](./commands.mdx) :::note[Note on exposing the HTTP endpoint] If you need to expose this endpoint to other hosts in your local network, we strongly recommended you use an intermediate reverse proxy server to implement authentication. Don't expose the HTTP endpoint to the raw and cruel open internet. ::: ### Outbound - Stellar Core requires access to a database (PostgreSQL, for example). If that database resides on a different machine on your network, you'll need to allow that connection. You'll specify the database when you [configure] Stellar Core. - Your node needs outbound NTP (UDP port 123) so that its clock stays synchronized and Stellar Core can check for clock drift — see [Clock Synchronization](#clock-synchronization) below. - You can safely block all other connections. ## Clock Synchronization Stellar Core depends on an accurate system clock: ledgers close on network time, scheduled [network upgrades](./network-upgrades.mdx) are armed relative to it, and consensus includes an optimization that requires validators' clocks across the network to be closely synchronized. Every validator must run an NTP synchronization service. The node logs a warning when it detects clock drift: ```text Local clock is off by ms versus NTP server ``` If you see this warning, fix your clock synchronization. Even if you have never seen it, we strongly recommend running an NTP service on every node. We recommend [chrony](https://chrony-project.org/). Install it with your distribution's package manager: ```bash # Ubuntu / Debian sudo apt update sudo apt install chrony # RHEL / CentOS / Rocky / Alma / Fedora sudo dnf install chrony # (older systems may use: sudo yum install chrony) ``` After installing, enable and start the service: ```bash sudo systemctl enable chronyd sudo systemctl start chronyd ``` On some Ubuntu/Debian systems the service is named `chrony` rather than `chronyd`: ```bash sudo systemctl enable chrony sudo systemctl start chrony ``` ### Verify Synchronization Run `timedatectl` and confirm the output includes: ```text System clock synchronized: yes NTP service: active ``` Then check chrony itself: ```bash chronyc tracking chronyc sources -v ``` `chronyc tracking` should report `Leap status : Normal`, and `chronyc sources -v` should show at least one reachable time source, typically marked with `^*` or `^+`. ## Storage Stellar Core's local storage needs come from two sources: the **buckets directory** (which serves as the primary database backend under BucketListDB, the default since stellar-core 21.0) and a much smaller **SQL database** for metadata. Both are managed entirely by Stellar Core. Local disk usage stays bounded over time — see [Why local disk stays bounded](#why-local-disk-stays-bounded) below. ### How storage breaks down Approximate sizes for a current default-config validator on Mainnet. These figures should be treated as planning estimates rather than precise measurements. | Component | Approximate size | Notes | | --- | --- | --- | | Buckets directory (BucketListDB) | 20–40 GB | Primary store for live ledger state since stellar-core 21.0. | | SQL database | A few GB | Post-BucketListDB, used only for non-ledger metadata, transaction history within the retention window, and some DEX queries. Most ledger state tables are dropped at migration. | | WAL logs, temp files | 5–15 GB | PostgreSQL write-ahead logs and temporary space during maintenance operations. SQLite users will see lower numbers. | A working set of roughly 30–60 GB is typical. The 100 GB local NVMe included with the recommended `c5d.2xlarge` (and comparable on Hetzner, OVH, Contabo, and others) leaves comfortable operational headroom on top of that — room for debug captures, re-syncs, and unforeseen operational needs. ### Why local disk stays bounded A common misconception is that validators need to provision storage proportional to network history. They do not. **Soroban (smart contract) state is bounded by [state archival](https://developers.stellar.org/docs/learn/encyclopedia/storage/state-archival).** Contract data and contract code entries carry a rent balance; when a Temporary entry's balance reaches zero, it is deleted from live state. Temporary entries are cheaper to create than Persistent entries and dominate total contract data volume, so as they expire and are removed, a significant amount of state is freed up — keeping the live state on disk from growing unboundedly. **Classic ledger entries — accounts, trustlines, offers, claimable balances, liquidity pool shares, and data entries — do not expire.** They persist on the live state indefinitely. They grow slowly, though, because [reserve requirements](../../learn/fundamentals/stellar-data-structures/accounts.mdx#base-reserves-and-subentries) act as anti-spam friction on creation. In practice, the resulting working set still lands in the 30–60 GB range cited above, so local validator state stays compact even as cumulative network history grows. **History archives live on object storage, not on the validator.** Full validators publish history archives to a separate object store (S3, R2, Backblaze B2, etc.) — that's where the multi-TB archive data lives. The validator process itself doesn't hold the archive on its local disk. See [Publishing History Archives](./publishing-history-archives.mdx) for the recommended setup. **`CATCHUP_COMPLETE=true` is almost never the right choice.** This setting makes the node sync the entire ledger from genesis on startup and is rarely appropriate for a validator. The standard pattern for new validators — including new Tier 1 candidates — is to sync against current network state, publish a history archive forward from that point, and use `stellar-archivist mirror` to backfill historical data into the published archive as a separate operation. The validator's local disk requirements are determined by the live state model above, not by historical depth. ### Database Even with BucketListDB as the primary store, Stellar Core still requires a SQL database — either SQLite or PostgreSQL (recommended for production) — for metadata and transaction history. The SQL database is consulted during consensus and modified atomically when a transaction set is applied to the ledger. Access patterns are random, fine-grained, and fast. If you're using PostgreSQL, we recommend you configure your local database to be accessed over a Unix domain socket, as well as updating the below PostgreSQL configuration parameters: ```text # !!! DB connection should be over a Unix domain socket !!! # shared_buffers = 25% of available system ram # effective_cache_size = 50% of available system ram # max_wal_size = 5GB # max_connections = 150 ``` ### Buckets Stellar Core stores ledger state in the form of flat XDR files called "buckets." These files are used for hashing and transmission of ledger differences to history archives. Under BucketListDB (the default since stellar-core 21.0), the `buckets` directory also serves as the primary database backend — making it the largest single component of validator storage. Buckets should be stored on a fast, local disk with sufficient space for several times the current ledger size. NVMe SSDs with 10,000+ IOPS are recommended for production validators. Network-attached or remote storage is not recommended; latency on the buckets path directly affects consensus performance. {/* Maintenance note for SDF docs maintainers: the size figures in the breakdown table above are estimates and should be re-verified periodically. The most reliable way to get current numbers is to ask one or more current Tier 1 operators (#validator on the Stellar Dev Discord) to share du -sh output of the buckets directory, the SQL database (PostgreSQL: pg_database_size, SQLite: file size), and the full stellar-core data directory. Last estimated: May 2026. Reflects post-BucketListDB defaults (stellar-core 21.0+) and the state archival design described in CAP-46-12 / CAP-57. NOTE: As of the original state archival announcement, the user-facing rent interface was live on mainnet but archived entries were not yet being deleted from validators. If full archival eviction has not yet shipped at the time this page is updated, the "Why local disk stays bounded" section may need to soften "are deleted from the validator" to "will be deleted from the validator under the state archival protocol." */} ## Kubernetes considerations We currently do not recommend running validator nodes in Kubernetes. Standard VM-based deployments (bare metal or cloud instances) are the well-tested path for production validators. If you choose to use Kubernetes regardless, consider the following: - Sensitive data such as node seeds will be stored in Kubernetes etcd. Consider consuming credentials using tools like [vault agent](https://developer.hashicorp.com/vault/docs/platform/k8s/injector) or the [AWS Secrets Store CSI driver](https://github.com/aws/secrets-store-csi-driver-provider-aws) to improve security - Consider how external traffic will reach the pods. Tier 1 nodes need public DNS names and necessary ports must be accessible from the internet - Validators have unique seeds and history archive configurations, so each pod will require its own specific configuration - Ensure that sufficient resources are always available to the pods - Depending on how history archives are published, you may need to fork docker images to include extra tooling [configure]: ./configuring.mdx [c5d.2xlarge]: https://aws.amazon.com/ec2/instance-types/c5/ [n4-highcpu-8]: https://cloud.google.com/compute/docs/general-purpose-machines#n4-highcpu --- ## Publishing History Archives If you want to run a [Full Validator](../README.mdx#full-validator), you need to set up your node to publish a history archive. You can host an archive using a blob store such as Amazon's S3 or Digital Ocean's spaces, or you can simply serve a local archive directly via an HTTP server such as Nginx or Apache. If you're setting up a [Basic Validator](../README.mdx#basic-validator), you can skip this section. No matter what kind of node you're planning to run, make sure to set it up to `get` history, which is covered in [Environment Preparation](./environment-preparation.mdx). :::caution[One archive per node] Each node must publish to its own dedicated archive. Writing to the same archive from multiple nodes is not supported and will result in undefined behavior, potentially including data loss. If you're running multiple Full Validators (as [Tier 1 organizations](../tier-1-orgs.mdx) do), configure a separate archive for each — for example, `history-a.example.com`, `history-b.example.com`, and `history-c.example.com`. ::: ## Caching and History Archives The _primary_ cost of running a validator will very likely be egress bandwidth. A crucial part of your strategy to manage those costs should be caching. You can significantly reduce these data transfer costs by using common caching techniques or a CDN. Three simple rules apply to caching the History archives: 1. Do not cache the archive state file `.well-known/stellar-history.json` (`Cache-Control: no-cache`) 2. Do not cache HTTP 4xx responses (`Cache-Control: no-cache`) 3. Cache everything else for as long as possible (**> 1 day**) ## Local History Archive Using nginx Here, we'll demonstrate how you can configure your node to store its history files in the local filesystem, and then publish that history using nginx for our webserver software. First, you must add a history configuration stanza to your `/etc/stellar/stellar-core.cfg` configuration file. ```toml [HISTORY.local] get="cp /mnt/xvdf/stellar-core-archive/node_001/{0} {1}" put="cp {0} /mnt/xvdf/stellar-core-archive/node_001/{1}" mkdir="mkdir -p /mnt/xvdf/stellar-core-archive/node_001/{0}" ``` Then, you must run Stellar Core's `new-hist` command to create the local history archive. ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg new-hist local ``` This command creates the history archive structure: ```bash $ tree -a /mnt/xvdf/stellar-core-archive/ /mnt/xvdf/stellar-core-archive └── node_001 ├── history │ └── 00 │ └── 00 │ └── 00 │ └── history-00000000.json └── .well-known └── stellar-history.json 6 directories, 2 files ``` Now that the history archive's file structure is ready, you can configure a virtual host in nginx to serve the local archive. ```nginx server { listen 80; root /mnt/xvdf/stellar-core-archive/node_001/; server_name history.example.com; # do not cache 404 errors error_page 404 /404.html; location = /404.html { add_header Cache-Control "no-cache" always; } # do not cache history state file location ~ ^/.well-known/stellar-history.json$ { add_header Cache-Control "no-cache" always; try_files $uri =404; } # cache entire history archive for 1 day location / { add_header Cache-Control "max-age=86400"; try_files $uri =404; } } ``` ## Amazon S3 History Archive Now, let's demonstrate a configuration where your node stores its history files using Amazon's S3 service. You can then publish that history using an Amazon S3 static site, or again use nginx for your webserver software. This time, using nginx, we'll include some proxy and CDN configuration, as well. Start by adding a history configuration stanza to your `/etc/stellar/stellar-core.cfg` configuration file. ```toml [HISTORY.s3] get='curl -sf http://history.example.com/{0} -o {1}' # Cached HTTP endpoint put='aws s3 cp --region us-east-1 {0} s3://bucket.name/{1}' # Direct S3 access ``` Then, you must run Stellar Core's `new-hist` command to create and initialize the S3 archive. ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg new-hist s3 ``` These S3 history files can be served with something as simple as an Amazon S3 static site. Optionally, you may want to place a reverse proxy and CDN in front of the S3 static site (we'll use nginx for this example). ```nginx server { listen 80; root /srv/nginx/history.example.com; index index.html index.htm; server_name history.example.com; # use google nameservers for lookups resolver 8.8.8.8 8.8.4.4; # bucket.name s3 static site endpoint set $s3_bucket "bucket.name.s3-website-us-east-1.amazonaws.com"; # do not cache 404 errors error_page 404 /404.html; location = /404.html { add_header Cache-Control "no-cache" always; } # do not cache history state file location ~ ^/.well-known/stellar-history.json$ { add_header Cache-Control "no-cache" always; proxy_intercept_errors on; proxy_pass http://$s3_bucket; proxy_read_timeout 120s; proxy_redirect off; proxy_buffering off; proxy_set_header Host $s3_bucket; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # cache history archive for 1 day location / { add_header Cache-Control "max-age=86400"; proxy_intercept_errors on; proxy_pass http://$s3_bucket; proxy_read_timeout 120s; proxy_redirect off; proxy_buffering off; proxy_set_header Host $s3_bucket; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ## Backfilling a History Archive Given the choice, it's best to configure your history archive _prior to_ your node's initial sync with an existing network. That way your validator's history publishes as you join, and subsequently sync with, the network. However, if you have not published an archive during the node's initial sync, the steps required to create a history archive for an existing validator — in other words, to upgrade a Basic Validator to a Full Validator — are quite straightforward. First, you'll need to stop your `stellar-core` instance: ```bash systemctl stop stellar-core # modify this if not using systemctl ``` Then, add the history archive configuration to your node's `/etc/stellar/stellar-core.cfg` configuration file. ```toml [HISTORY.local] get="cp /mnt/xvdf/stellar-core-archive/node_001/{0} {1}" put="cp {0} /mnt/xvdf/stellar-core-archive/node_001/{1}" mkdir="mkdir -p /mnt/xvdf/stellar-core-archive/node_001/{0}" ``` Next, you must run Stellar Core's `new-hist` command to create and initialize the local archive. (This is done as the `stellar` user.) ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg new-hist local ``` Now you can start your Stellar Core instance again: ```bash systemctl start stellar-core # modify this if not using systemctl ``` As you allow your node to join the network again, you can watch it start publishing a few checkpoints to the newly created archive. ```log 2019-04-25T12:30:43.275 GDUQJ [History INFO] Publishing 1 queued checkpoints [16895-16895]: Awaiting 0/0 prerequisites of: publish-000041ff ``` At this stage your validator is successfully publishing its history, which enables other users to join the network using your archive. ## Complete History Archive The [stellar-archivist](https://github.com/stellar/go-stellar-sdk/tree/main/tools/stellar-archivist) command line tool can be used to mirror, scan, and repair existing archives. Using the [SDF package repositories](https://github.com/stellar/packages), you can install `stellar-archivist` by running: ```bash apt-get install stellar-archivist ``` If you decide to publish a complete archive — which enables other users to join the network from the genesis ledger — you can use `stellar-archivist` to add all missing history data to your partial archive, and to verify the state and integrity of your archive. For example: ```bash stellar-archivist scan file:///mnt/xvdf/stellar-core-archive/node_001 ``` ```log 2019/04/25 11:42:51 Scanning checkpoint files in range: [0x0000003f, 0x0000417f] 2019/04/25 11:42:51 Checkpoint files scanned with 324 errors 2019/04/25 11:42:51 Archive: 3 history, 2 ledger, 2 transactions, 2 results, 2 scp 2019/04/25 11:42:51 Scanning all buckets, and those referenced by range 2019/04/25 11:42:51 Archive: 30 buckets total, 30 referenced 2019/04/25 11:42:51 Examining checkpoint files for gaps 2019/04/25 11:42:51 Examining buckets referenced by checkpoints 2019/04/25 11:42:51 Missing history (260): [0x0000003f-0x000040ff] 2019/04/25 11:42:51 Missing ledger (260): [0x0000003f-0x000040ff] 2019/04/25 11:42:51 Missing transactions (260): [0x0000003f-0x000040ff] 2019/04/25 11:42:51 Missing results (260): [0x0000003f-0x000040ff] 2019/04/25 11:42:51 No missing buckets referenced in range [0x0000003f, 0x0000417f] 2019/04/25 11:42:51 324 errors scanning checkpoints ``` As you can tell from the output of the `scan` command, some history, ledger, transactions, and results are missing from the local history archive. You can repair the missing data using stellar-archivist's `repair` command combined with a known full archive — such as the SDF public history archive: ```bash stellar-archivist repair http://history.stellar.org/prd/core-testnet/core_testnet_001/ file:///mnt/xvdf/stellar-core-archive/node_001/ ``` ```log 2019/04/25 11:50:15 repairing http://history.stellar.org/prd/core-testnet/core_testnet_001/ -> file:///mnt/xvdf/stellar-core-archive/node_001/ 2019/04/25 11:50:15 Starting scan for repair 2019/04/25 11:50:15 Scanning checkpoint files in range: [0x0000003f, 0x000041bf] 2019/04/25 11:50:15 Checkpoint files scanned with 244 errors 2019/04/25 11:50:15 Archive: 4 history, 3 ledger, 263 transactions, 61 results, 3 scp 2019/04/25 11:50:15 Error: 244 errors scanning checkpoints 2019/04/25 11:50:15 Examining checkpoint files for gaps 2019/04/25 11:50:15 Repairing history/00/00/00/history-0000003f.json 2019/04/25 11:50:15 Repairing history/00/00/00/history-0000007f.json 2019/04/25 11:50:15 Repairing history/00/00/00/history-000000bf.json ... 2019/04/25 11:50:22 Repairing ledger/00/00/00/ledger-0000003f.xdr.gz 2019/04/25 11:50:23 Repairing ledger/00/00/00/ledger-0000007f.xdr.gz 2019/04/25 11:50:23 Repairing ledger/00/00/00/ledger-000000bf.xdr.gz ... 2019/04/25 11:51:18 Repairing results/00/00/0e/results-00000ebf.xdr.gz 2019/04/25 11:51:18 Repairing results/00/00/0e/results-00000eff.xdr.gz 2019/04/25 11:51:19 Repairing results/00/00/0f/results-00000f3f.xdr.gz ... 2019/04/25 11:51:39 Repairing scp/00/00/00/scp-0000003f.xdr.gz 2019/04/25 11:51:39 Repairing scp/00/00/00/scp-0000007f.xdr.gz 2019/04/25 11:51:39 Repairing scp/00/00/00/scp-000000bf.xdr.gz ... 2019/04/25 11:51:50 Re-running checkpoing-file scan, for bucket repair 2019/04/25 11:51:50 Scanning checkpoint files in range: [0x0000003f, 0x000041bf] 2019/04/25 11:51:50 Checkpoint files scanned with 5 errors 2019/04/25 11:51:50 Archive: 264 history, 263 ledger, 263 transactions, 263 results, 241 scp 2019/04/25 11:51:50 Error: 5 errors scanning checkpoints 2019/04/25 11:51:50 Scanning all buckets, and those referenced by range 2019/04/25 11:51:50 Archive: 40 buckets total, 2478 referenced 2019/04/25 11:51:50 Examining buckets referenced by checkpoints 2019/04/25 11:51:50 Repairing bucket/57/18/d4/bucket-5718d412bdc19084dafeb7e1852cf06f454392df627e1ec056c8b756263a47f1.xdr.gz 2019/04/25 11:51:50 Repairing bucket/8a/a1/62/bucket-8aa1624cc44aa02609366fe6038ffc5309698d4ba8212ef9c0d89dc1f2c73033.xdr.gz 2019/04/25 11:51:50 Repairing bucket/30/82/6a/bucket-30826a8569cb6b178526ddba71b995c612128439f090f371b6bf70fe8cf7ec24.xdr.gz ... ``` A final scan of the local archive confirms that it has been successfully repaired ```bash stellar-archivist scan file:///mnt/xvdf/stellar-core-archive/node_001 ``` ```log 2019/04/25 12:15:41 Scanning checkpoint files in range: [0x0000003f, 0x000041bf] 2019/04/25 12:15:41 Archive: 264 history, 263 ledger, 263 transactions, 263 results, 241 scp 2019/04/25 12:15:41 Scanning all buckets, and those referenced by range 2019/04/25 12:15:41 Archive: 2478 buckets total, 2478 referenced 2019/04/25 12:15:41 Examining checkpoint files for gaps 2019/04/25 12:15:41 Examining buckets referenced by checkpoints 2019/04/25 12:15:41 No checkpoint files missing in range [0x0000003f, 0x000041bf] 2019/04/25 12:15:41 No missing buckets referenced in range [0x0000003f, 0x000041bf] ``` Finally, you can start your Stellar Core instance once again. ```bash systemctl start stellar-core ``` You should now have a complete history archive being written by your full validator. Congratulations! --- ## Running(3) ## Starting Your Node Once you've [set up your environment](./prerequisites.mdx), [configured your node](./configuring.mdx), set up your [quorum set](./configuring.mdx#choosing-your-quorum-set), and selected archives to `get` [history from](./environment-preparation.mdx#history-archives), you're ready to start Stellar Core. Use a command equivalent to: ```bash sudo systemctl start stellar-core ``` At this point, you're ready to observe your core node's activity as it joins the network. You may want to skip ahead and review the [Logging](./logging.mdx) page to familiarize yourself with Stellar Core's output. ## Interacting With Your Instance When your node is running, you can interact with Stellar Core via an administrative HTTP endpoint. Commands can be issued using command-line HTTP tools such as `curl`, or by running a CLI command such as ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg http-command ``` The HTTP endpoint is [not intended to be exposed to the public internet](./prerequisites.mdx#internal-system-access). It's typically accessed by administrators, or by a mid-tier application to submit transactions to the Stellar network. Details of available _HTTP_ endpoint commands can be found in the [stellar-core-GitHub repo](https://github.com/stellar/stellar-core/blob/master/docs/software/commands.md#http-commands). Additionally, [the commands page](./commands.mdx) contains an overview of some of the most useful _CLI_ commands (non-HTTP commands) for managing a running core node. ## Joining the Network Your node will go through the following phases as it joins the network, and you can query for the output below by using the info endpoint mentioned [here](./monitoring.mdx#general-node-information) : ### Establish Connection to Other Peers. You should see `authenticated_count` increase. ```json "peers" : { "authenticated_count" : 3, "pending_count" : 4 }, ``` ### Observing Consensus Until the node sees a quorum, it will say: ```json "state" : "Joining SCP" ``` After observing consensus, a new field `quorum` will display information about network decisions. At this point the node will switch to a "_Catching up_" state: ```json "quorum" : { "qset" : { "ledger" : 22267866, "cost" : 20883268, "agree" : 5, "delayed" : 0, "disagree" : 0, "fail_at" : 3, "hash" : "980a24", "lag_ms" : 430, "missing" : 0, "phase" : "EXTERNALIZE" }, "transitive" : { "intersection" : true, "last_check_ledger" : 22267866, "node_count" : 21 } }, "state" : "Catching up", ``` ### Catching up This is a phase where the node downloads data from any configured archives. This phase begins with something like: ```json "state" : "Catching up", "status" : [ "Catching up: Awaiting checkpoint (ETA: 35 seconds)" ] ``` And will then move through the various phases of downloading and applying state, such as: ```json "state" : "Catching up", "status" : [ "Catching up: downloading ledger files 20094/119803 (16%)" ] ``` For the fastest sync — including for new Full Validators and Tier 1 candidates — leave `CATCHUP_RECENT` at its default and do **not** set `CATCHUP_COMPLETE=true`. Your node will sync against current network state in minutes to hours rather than weeks. `CATCHUP_COMPLETE=true` makes the node replay the _entire history_ of the network on startup, which takes weeks and is almost never the right choice for a validator — see [Why local disk stays bounded](./prerequisites.mdx#why-local-disk-stays-bounded). The standard pattern for new validators that want to publish a complete history archive is to sync against current state first, publish forward from that point, and use the [`stellar-archivist mirror`](https://github.com/stellar/go-stellar-sdk/tree/main/tools/stellar-archivist) tool to backfill historical data into the published archive as a separate offline operation. See the [complete example configuration] for more details. :::info The `CATCHUP_COMPLETE` and `CATCHUP_RECENT` config fields are mutually exclusive. If the `CATCHUP_COMPLETE` field is set to `true`, the `CATCHUP_RECENT` field will be ignored. ::: ### Synced When the node is done catching up, its state will change to: ```json "state" : "Synced!" ``` [complete example configuration]: https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg --- ## Soroban Settings Soroban has a large collection of settings stored on-ledger that can be modified through a validator vote. Here you can find out how to propose a new settings upgrade as well as how to examine a proposed upgrade. You can also look at the [Commands page](./commands.mdx) for more details on the stellar-core commands used below. ## Propose a Settings Upgrade This section will describe how to propose a settings upgrade, but take a look at the [Upgrading the Network page](./network-upgrades.mdx#upgrading-soroban-settings) for more information on how the settings upgrade mechanism works internally. :::info If you are being asked to vote for an upgrade, please move on to the [Examine a Proposed Upgrade](#examine-a-proposed-upgrade) section for details on how to accomplish that. ::: ### Frozen ledger keys [CAP-77](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0077.md) has been introduced in protocol 26 to allow validators to make certain ledger keys "frozen", meaning that they will not be accessible for reading or writing. This is a mechanism that can be used to quarantine corrupted data in case of data corruption bugs, or remove access to stolen funds in case of security incidents. Freezing/unfreezing the keys follows the standard upgrade procedure described below. The only caveat is that the upgrade JSON must contain hex-encoded `LedgerKey` XDR in the sets of keys to freeze and/or unfreeze, for example: ```json { "updated_entry": [ { "frozen_ledger_keys_delta": { "keys_to_freeze": [ "000000000000000000000000000000000000000000000000000000000000000000000000" ], "keys_to_unfreeze": [ "000000000000000000000000000000000000000000000000000000000000000000000001" ] } } ] } ``` Since most tools encode XDR as base64, you can use a helper script in the [stellar-core repository](https://github.com/stellar/stellar-core/blob/master/scripts/frozen_keys_base64_to_hex.py). ## Helper Script A script to help you create the transactions below is available [in the stellar-core GitHub repo](https://github.com/stellar/stellar-core/blob/master/scripts/settings-helper.sh), with usage details [also available](https://github.com/stellar/stellar-core/blob/master/scripts/README.md). We've saved the information below for now, because it's important to be aware of how the underlying process to generate the transactions works in case the script has some issues. ### 1. Create an Upgrade Set The `stellar` CLI tool allows for the use of a JSON input file to encode a collection of Soroban settings into an base64-encoded string representing the XDR type `ConfigUpgradeSet`. You can use the [`pubnet_phase1.json`] and [`pubnet_phase2.json`] files as a starting point to formulate your upgrade proposal. You can also pull directly from a running core node using `http-command 'sorobaninfo?format=upgrade_xdr' | stellar-xdr decode --type ConfigUpgradeSet --output json-formatted`. This will allow you to get the exact settings running on that core node in JSON format, making it easier to change only the value you want to. Once you have your JSON file with updated values, use the following command to create the required upgrade set XDR: ```bash stellar xdr encode --type ConfigUpgradeSet path/to/upgrade.json ``` The output of the phase 2 JSON file, for example, would look like: ```text AAAAAgAAAAEAAAAAHc1lAAAAAAAF9eEAAAAAAAAAABkCgAAAAAAAAgAAAMgAB6EgAAAAfQABEXAAAAAoAAIIAAAAABkAAQQAAAAAAAAAGGoAAAAAAAAnEAAAAAAAAAb6AAAAAukO3QD///////0NfwAAAAAAAOFfAAAD6A== ``` :::info `stellar` CLI can be installed using brew or cargo: ```bash brew install stellar-cli # OR cargo install --locked stellar-cli ``` You can also download a [precompiled binary] of the latest release for your system from GitHub. ::: ### 2. Generate Settings Upgrade Transactions You can use stellar-core's `get-settings-upgrade-txs` command to create a series of transactions that will: 1. Restore the Wasm entry from the next step (if it exists). 2. Upload a simple smart contract's Wasm bytecode to the network that allows for `Temporary` ledger entries to be created, keyed using `Bytes` `SCVal`s. 3. Create's a contract instance from the previously uploaded Wasm bytecode. 4. Invoke the newly created contract instance's `write` function with your (validated) base64-encoded upgrade set. You're required to provide four arguments for this command: - `PUBLIC_KEY`: (positional) the public key that will be the source for these transactions. Note that you will be submitting transactions, so the account for the public key specified must exist and have the funds to pay the transaction fees. - `SEQUENCE_NUMBER`: (positional) the _current_ sequence number for the specified public key. - `NETWORK_PASSPHRASE`: (positional) the network passphrase for the network these transactions will be submitted to. - `--xdr AAAA...`: the base64-encoded upgrade set generated in [step 1](#1-create-an-upgrade-set). - `--signtxs`: (optional) if provided, this command will prompt you for a secret key and will return _signed_ transactions that can be submitted to the network right away. For example, the command for the phase 2 upgrade might have looked like this: ```bash sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg get-settings-upgrade-txs \ GAUQW73V52I2WLIPKCKYXZBHIYFTECS7UPSG4OSVUHNDXEZJJWFXZG56 \ 73014444032 \ "Public Global Stellar Network ; September 2015" \ --xdr AAAAAgAAAAE... \ --signtxs ``` This command's output will include three sets of `TransactionEnvelope`s and corresponding transaction `Hash`es, followed by a base64-encoded `ConfigUpgradeSetKey` XDR string: ```bash showLineNumbers # restore Wasm `TransactionEnvelope` (truncated here) AAAAAgAAAABSvbro8P/XAQP+eTvW1TnB4+r4vZx5fKj+DsWuWN3gjQX14QAAA...LgymJ36XIBrUVMU2wg= # restore Wasm transaction `Hash` 3a2457d3fc081ab3a72dfe7ee2236f3a282c62f21d7e3dbdb5b13ac0d09c8647 # upload Wasm `TransactionEnvelope` (truncated here) AAAAAgAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEc...wF9wL68IAAAAdkJxSgpyRStTvbSA9jgs= # upload Wasm transaction `Hash` 19c49f18e5442db9d626f7485c34ecb0cd938034255515099b37acebdb6677a7 # create contract instance `TransactionEnvelope` (truncated here) AAAAAgAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEc...AAd0OB3n3Yadews= # create contract instance transaction `hash` 9e70cbff631247638fae96b9d996d8d22b6fa75208380d5f5d714a57c0a90947 # invoke contract `TransactionEnvelope` (truncated here) AAAAAgAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEc...F9wX14QAAAAARAAAAAwAAAAAAAAAAAAAAAQAAAAAAAAAYAAAAAAAAAAGd/IhWQcE2UdzIof7ygqCuAmYD8ycsJbB # invoke contract transaction `Hash` 4f6457d3fc081ab3a72dfe7ee2236f3a282c62f21d7e3dbdb5b13ac0d09c8647 # base64-encoded `ConfigUpgradeSetKey` nfyIVkHBNlHcyKH+8oKgrgJmA/MnLCW3E4Fhg4XYTkqZa2MyqzRdB2+mN3DOKUFKtZIAXp6o3DHrkgR0mo7rUw== ``` ### 3. Submit Settings Upgrade Transactions These four transactions can then be submitted to the network through your node. Replace the blob placeholders below with the `TransactionEnvelope`s in lines 1, 3, 5, and 7. :::note If you didn't provide the `--signtxs` parameter to the `get-settings-upgrade-txs` command, don't forget to sign them before submitting to the network. ::: ```bash http-command 'tx?blob=' http-command 'tx?blob=' http-command 'tx?blob=' http-command 'tx?blob=' ``` ### 4. Verify Proposed Upgrades You can verify that the proposed upgrades have been set up using stellar-core's `dumpproposedsettings` command, providing the `ConfigUpgradeSetKey` XDR string from line 9 above: ```bash http-command 'dumpproposedsettings?blob=' ``` ### 5. Schedule the Upgrades Now you can schedule the upgrade on all the required validators using stellar-core's `upgrades` command, providing the `ConfigUpgradeSetKey` output from line 9 above and an agreed upon time in the future: ```bash http-command 'upgrades?mode=set&upgradetime=YYYY-MM-DDTHH:MM:SSZ&configupgradesetkey=' ``` ### 6. Update Stellar Expert One of the most-used methods of "watching" an upgrade take place for the network's Soroban settings is the [Protocol History page] on [stellar.expert]. In order to make sure that page gets updated with the proposed upgrades, please fill out a PR against [this repository]. ### Debugging Once the four transactions are run, you should see the proposed upgrade when running the `dumpproposedsettings` command ([see step 4 above](#4-verify-proposed-upgrades)). If you don't, then either one or more of the transactions above failed during application, or the upgrade is invalid. If any of the transactions above fail during submission, you should get a `TransactionResult` as a response along with the reason for the failure. The failure will most likely be due to one of the following reasons: - Resources are too low. You'll need to increase the hardcoded resources in [`SettingsUpgradeUtils.cpp`]. - Fee or refundable fee is too low. You'll need to increase them in [`SettingsUpgradeUtils.cpp`]. - Wasm has expired. You'll need to restore the Wasm. You should confirm what caused the failure by looking at the `TransactionResult` of the failed transaction using the [Stellar Lab](https://lab.stellar.org) or a block explorer. If the transactions succeeded but the `dumpproposedsettings` command still returns an error, then the upgrade is invalid. The error reporting here needs to be improved, but the validity checks happen [here](https://github.com/stellar/stellar-core/blob/3007c595c2fe9b53502aa0daf8089d119a9c37cb/src/herder/Upgrades.cpp#L1451). ## Examine a Proposed Upgrade You can use stellar-core's `dumpproposedsettings` command along with a base64-encoded `ConfigUpgradeSetKey` XDR string to query a proposed upgrade: ```bash http-command 'dumpproposedsettings?blob=A6MvjFLujnqaZa5hacafWyYwhpk4cgRpyu0z6ilZ0pm1S7fmjSNnsyjGwGodLGiD8ss8S1AHiOBBb6GQbOeMbw==' ``` ## Examine Current Settings You can also get the current Soroban settings to compare against by using stellar-core's `sorobaninfo`. ```bash http-command 'sorobaninfo?format=detailed' ``` [`pubnet_phase1.json`]: https://github.com/stellar/stellar-core/blob/master/soroban-settings/pubnet_phase1.json [`pubnet_phase2.json`]: https://github.com/stellar/stellar-core/blob/master/soroban-settings/pubnet_phase2.json [precompiled binary]: https://github.com/stellar/stellar-cli/releases/latest [protocol history page]: https://stellar.expert/explorer/pubnet/protocol-history [stellar.expert]: https://stellar.expert [this repository]: https://github.com/stellar-expert/staged-soroban-upgrades [`settingsupgradeutils.cpp`]: https://github.com/stellar/stellar-core/blob/master/src/main/SettingsUpgradeUtils.cpp --- ## Tier 1 Organizations Tier 1 organizations are a group of organizations that bear the safety and liveness of the Stellar network on their shoulders. They earn this role because most other organizations on the network require agreement from them — by including them in their quorum sets — in order to commit to a new ledger.[^1] To become a Tier 1 organization, a team running validators must convince enough other organizations to trust them. SDF works closely with Tier 1 organizations to ensure network health, maintain robust quorum intersection, and build in redundancy to minimize disruptions. This guide outlines the requirements, costs, and process for organizations that want to join (or evaluate joining) the Tier 1 quorum. :::info[In short] Run three geographically dispersed Full Validators, achieve sustained 99.9%+ uptime visible on [Obsrvr Radar](https://radar.withobsrvr.com/), complete [SEP-20](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md) and [SEP-1](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) self-verification, and coordinate actively with the existing Tier 1 community. Joining is not a unilateral SDF decision — every existing Tier 1 organization independently decides whether to add you to their quorum set. Expect a process measured in months, not weeks. ::: :::info[Not ready for Tier 1?] Running a single [Basic or Full Validator](./README.mdx) is a meaningful contribution to network decentralization and a great way to build operational experience. The [Admin Guide](./admin-guide/README.mdx) covers everything you need for single-node setup. This page covers the additional requirements for running three Full Validators as a Tier 1 organization. ::: ## What Tier 1 Requires | Requirement | Details | | --- | --- | | **Full Validators** | 3, each [publishing a separate history archive](./admin-guide/publishing-history-archives.mdx) | | **Geographic dispersion** | Nodes in different data centers or cloud regions | | **Self-verification** | [SEP-20](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md) on-chain identity linking + [SEP-1](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) stellar.toml | | **Uptime** | 99.9%+ target with 24/7 [monitoring and alerting](./admin-guide/monitoring.mdx) | | **Coordination** | Active communication with other Tier 1 organizations | | **Trust** | Other Tier 1 organizations must choose to include you in their quorum sets | ## Why Three Validators On Stellar, validators choose to trust _organizations_ when they configure their quorum set. If you are a trustworthy organization, you want your presence on the network to persist even if a node fails or you take it down for maintenance. A trio of validating nodes allows that to happen: other participants can require ⅔ of your nodes to agree. If one has issues, the other two still vote on your organization's behalf. To ensure redundancy, those three Full Validators must be **geographically dispersed** — different data centers, ideally different cloud regions or providers. If all three are in the same facility, a single outage takes out your entire organization's voting power. ## Estimated Costs Running three Full Validators is more affordable than many newcomers expect, but the spread between cost-conscious and managed-cloud setups is wide. The dominant cost driver is bandwidth associated with serving your history archive — and that varies significantly by provider, by archive consumption patterns, and by whether you use a CDN or an egress-free object store. The table below groups setups into three archetypes that reflect actual choices made by current Tier 1 operators. Per-node figures assume a single Full Validator publishing its own history archive; three-node figures assume a Tier 1 deployment with geographic dispersion. | Setup archetype | Per node / month | 3 nodes / month | Typical providers | | --- | --- | --- | --- | | **Lean** | \$60–200 | \$180–600 | Contabo, Hetzner; archive on Cloudflare R2 or Backblaze B2 | | **Standard** | \$200–500 | \$600–1,500 | Hetzner, OVH, DigitalOcean, Akamai; archive on Backblaze, Wasabi, or self-hosted | | **Hyperscaler** | \$500–1,000+ | \$1,500–3,000+ | AWS, GCP, Azure; archive on S3/GCS/Blob without a CDN | _Last reviewed: May 2026. Sanity-check against current provider pricing before budgeting. Costs reported by current Tier 1 operators range from roughly \$160 to \$1,800 per month for three nodes._ ### What Drives the Variance **History archive bandwidth is the largest swing factor.** Validators that are heavily consumed as a catch-up source — by other validators, archivers, RPC nodes, and ecosystem services — can serve 10–30 TB per month from a single archive. Whether that costs \$0 or \$2,700 depends almost entirely on your provider: - **Egress-free object storage** (Cloudflare R2, Backblaze B2 with Cloudflare in front) charges nothing for outbound bandwidth. Several Tier 1 operators use this approach specifically to remove archive bandwidth from the cost equation. - **Bundled bandwidth allowances** (Hetzner includes 20 TB/month in EU regions; OVH includes generous traffic on dedicated servers) cover most realistic archive workloads at no marginal cost. - **Hyperscaler egress** (\$0.09/GB on AWS, similar on GCP and Azure) turns a 10 TB archive month into a \$900 line item, before any compute or storage. **Archive consumption is uneven across your three nodes.** Operators commonly report large variance between sibling nodes — one archive may serve tens of TB per month while another in the same fleet serves under 100 GB, depending on how each is configured in other operators' archive lists. Plan capacity for at least one of your three nodes to be heavily consumed. **Compute is a smaller line item than newcomers often assume.** A node meeting the recommended hardware spec (8 vCPU, 16 GB RAM, NVMe SSD) costs \$40–80/month on dedicated providers like Contabo, Hetzner, or OVH, and \$150–300/month on hyperscalers. Reserved or committed-use pricing on hyperscalers narrows this gap considerably. ### What Tier 1 Operators Actually Do Among current Tier 1 organizations, the dominant pattern is **dedicated or bare-metal providers with generous egress allowances**, paired with **egress-free or low-cost object storage for history archives**. Hetzner, OVH, Contabo, and DigitalOcean appear repeatedly across the cohort; AWS, GCP, and Azure are notably underrepresented. This is not a recommendation to avoid hyperscalers — some operators use them deliberately for compliance, geographic, or operational reasons — but it is a pattern worth understanding before committing to a stack. If you are evaluating providers, the `#validator` channel on the [Stellar Developer Discord](https://discord.gg/stellardev) is the best place to ask current operators what they are actually paying. ### Other Line Items The archetypes above bundle these into the totals, but they are worth listing for completeness: - **DNS / domain registration:** \$1–5/month total. Negligible. - **Monitoring:** \$0–50/month total. Self-hosted Prometheus + Grafana on one of your existing nodes is functionally free; managed services like Grafana Cloud or Datadog push to the high end. ## What SDF and Existing Tier 1 Organizations Evaluate Becoming Tier 1 is not a unilateral decision by SDF — it depends on the quorum set choices of all existing Tier 1 organizations. But SDF does steward the process. When evaluating candidates, the community considers: 1. **Organizational mission alignment** — Does your organization have a genuine stake in Stellar's success? Do you issue assets, process payments, or build infrastructure on the network? 2. **Identity and real-world presence** — Are you transparent about who you are? Public leadership, registered entity, visible operations? 3. **Operational excellence** — Can you demonstrate sustained high uptime (99.9%+)? Is your monitoring professional-grade? 4. **Geographic diversity** — Are your nodes in different regions than existing Tier 1 organizations? 5. **Jurisdictional diversity** — Are you in a different legal jurisdiction, reducing correlated regulatory risk? 6. **Infrastructure diversity** — Do you use different cloud providers, ISPs, or hardware than existing organizations? 7. **Economic diversity** — Does your business model differ from existing Tier 1 organizations? 8. **Responsiveness** — Do you respond quickly to incidents, upgrade requests, and coordination needs? No single dimension is disqualifying on its own, but organizations that strengthen the network across multiple dimensions are the strongest candidates. ## Step-by-Step Path to Tier 1 The checklist below covers the operational work. Completing it is necessary but not sufficient — see [What SDF and Existing Tier 1 Organizations Evaluate](#what-sdf-and-existing-tier-1-organizations-evaluate) above for the qualitative dimensions that determine whether existing Tier 1 organizations add you to their quorum sets. ### Phase 1: First Full Validator Work through the [Admin Guide](./admin-guide/README.mdx) to stand up a single Full Validator on Mainnet: - [ ] Review [prerequisites](./admin-guide/prerequisites.mdx) and provision a server - [ ] Enable [NTP clock synchronization](./admin-guide/prerequisites.mdx#clock-synchronization) - [ ] [Install](./admin-guide/installation.mdx) Stellar Core - [ ] [Configure](./admin-guide/configuring.mdx) for Mainnet with a validator key pair (`stellar-core gen-seed`) - [ ] [Prepare your environment](./admin-guide/environment-preparation.mdx) and initialize the database - [ ] Set up a [history archive](./admin-guide/publishing-history-archives.mdx) - [ ] [Start your node](./admin-guide/running-node.mdx) and sync to the network - [ ] Set up [monitoring](./admin-guide/monitoring.mdx) with Prometheus + Grafana ### Phase 2: Three Full Validators Scale to the Tier 1 architecture: - [ ] Provision 2 additional servers in **different geographic regions** - [ ] Generate **unique** key pairs for each node — never share seeds - [ ] Configure each with its own [history archive](./admin-guide/publishing-history-archives.mdx) (one archive per node) - [ ] Deploy the standard Tier 1 [quorum set](./admin-guide/configuring.mdx#choosing-your-quorum-set) on all three nodes, declaring your own organization as `HIGH` quality - [ ] Extend monitoring to cover all three nodes ### Phase 3: Identity and Verification Make your organization discoverable and verifiable: - [ ] Configure public DNS for each node (e.g., `core-a.example.com`, `core-b.example.com`, `core-c.example.com`) - [ ] Create funded Stellar accounts for each validator node - [ ] Set home domain on-chain for each ([SEP-20](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md)) - [ ] Publish a complete stellar.toml ([SEP-1](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md)) with organization info and all three `[[VALIDATORS]]` entries - [ ] Verify your nodes appear correctly on [Obsrvr Radar](https://radar.withobsrvr.com/) ### Phase 4: Build Trust (Ongoing) Earn the confidence of existing Tier 1 organizations: - [ ] Join the [Stellar Dev Discord](https://discord.gg/stellardev) `#validator` channel - [ ] Maintain 99.9%+ uptime for 3+ months, visible on Obsrvr Radar - [ ] Participate in upgrade coordination and governance discussions - [ ] Respond promptly to maintenance coordination from other validators - [ ] Engage with SDF about Tier 1 candidacy when your track record is established ### What Happens When You're Ready Once SDF and existing Tier 1 organizations agree you meet the bar: 1. Existing Tier 1 organizations update their quorum sets to include your validators 2. The standard Tier 1 quorum set configuration is updated to include your organization 3. You begin appearing as Tier 1 on network monitoring tools 4. You assume all the responsibilities described on this page This is not a unilateral decision by SDF. Every existing Tier 1 organization independently decides whether to trust you by adding you to their quorum set. ## Working with the Tier 1 Community Coordinate with other validators when you make significant changes. Specifically, let them know when you plan to: - **Take a node down for maintenance** — so a critical mass of nodes don't go offline simultaneously - **Make changes to your quorum set** — so they can respond, adjust, and ensure quorum intersection is maintained - **Upgrade to a new Stellar Core version** — especially when the release involves protocol upgrades or Soroban settings votes For the Stellar network to expand safely, validators must coordinate off-chain to maintain good quorum intersection. Never change your quorum set without discussing it with other Tier 1 organizations first. The `#validator` channel on the [Stellar Dev Discord](https://discord.gg/stellardev) is the primary place to do this coordination — and also where prospective Tier 1 candidates can introduce themselves and get guidance from existing operators and SDF. As Stellar grows and more businesses build on the network, Tier 1 organizations will be crucial to healthy expansion of the network. ## Resources | Resource | Link | | --- | --- | | Stellar Dev Discord (`#validator`) — primary coordination channel for validator operators. Release announcements, upgrade coordination, incident response. | [discord.gg/stellardev](https://discord.gg/stellardev) | | stellar-core GitHub releases — official release notes | [github.com/stellar/stellar-core/releases](https://github.com/stellar/stellar-core/releases) | | Obsrvr Radar — public validator uptime and quorum monitoring | [radar.withobsrvr.com](https://radar.withobsrvr.com/) | | Admin Guide | [Admin Guide](./admin-guide/README.mdx) | | Example Mainnet Full Validator config | [stellar/packages on GitHub](https://github.com/stellar/packages/blob/master/docs/examples/pubnet-validator-full/stellar-core.cfg) | | SEP-20 (Self-Verification) | [stellar-protocol on GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md) | | SEP-1 (stellar.toml) | [stellar-protocol on GitHub](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) | [^1]: The notion of Tier 1 organization can be defined precisely, but this is besides the point of this page.