# 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))
[](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!
{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.
:::

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).

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.

```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.

```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.

## 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!).

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).
:::

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.

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.

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.

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.

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.
:::

## 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:

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.

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.

### 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-github-codespaces]
[][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 (
);
};
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 (
+
```
### 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

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

## 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.

## 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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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-github-codespaces]
[][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 =