Looking for a quick start? See Quick Start to get running in 15 minutes.
This guide covers the fundamentals of the Stellar iOS/macOS SDK.
- Installation
- Basic Concepts
- KeyPair Management
- Account Operations
- Transaction Building
- Connecting to Networks
- Soroban RPC
- Error Handling
- Best Practices
- Next Steps
Add the SDK to your project using Swift Package Manager. In Xcode, go to File > Add Package Dependencies and enter the repository URL:
https://github.com/nicklama/stellar-ios-mac-sdk
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/nicklama/stellar-ios-mac-sdk.git", from: "2.0.0")
]Requirements: iOS 13+ / macOS 12+, Swift 5.7+.
Stellar has multiple networks with unique passphrases:
import stellarsdk
let testnet = Network.testnet // Development (free test XLM via Friendbot)
let pubnet = Network.public // Production (real assets)
let future = Network.futurenet // Upcoming protocol featuresEvery Stellar account has:
- Account ID (public key): Starts with
G. Safe to share. - Secret Seed (private key): Starts with
S. Keep secret!
An account must hold at least 1 XLM to exist (the base reserve).
Stellar supports two types of assets:
- Native (XLM): The built-in currency used for fees and account reserves.
- Issued assets: Tokens created by any account (the "issuer"). To hold an issued asset, you must first establish a trustline to the issuer.
import stellarsdk
// Native XLM
let xlm = Asset(type: AssetType.ASSET_TYPE_NATIVE)!
// Issued asset (code + issuer account)
let usdc = Asset(
type: AssetType.ASSET_TYPE_CREDIT_ALPHANUM4,
code: "USDC",
issuer: try! KeyPair(accountId: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
)!A transaction groups one or more operations that execute atomically. Common operations:
CreateAccountOperation-- Create a new accountPaymentOperation-- Send assetsChangeTrustOperation-- Establish a trustlineManageSellOfferOperation-- Place a DEX order
Manage cryptographic keys for signing transactions and identifying accounts.
Create a new wallet with a random keypair. The account ID is your public address; the secret seed is your private key for signing transactions.
import stellarsdk
let keyPair = try! KeyPair.generateRandomKeyPair()
let accountId = keyPair.accountId // GCFXHS4GXL6B... (public)
let secretSeed = keyPair.secretSeed! // SAV76USXIJOB... (private)If you already have a secret seed (from a backup or another wallet), you can restore the full keypair. This lets you sign transactions.
import stellarsdk
// Restore keypair from seed (can sign transactions)
let keyPair = try! KeyPair(secretSeed: "SDJHRQF4GCMIIKAAAQ6IHY42X73FQFLHUULAPSKKD4DFDM7UXWWCRHBE")You can create a keypair from just an account ID (public key). This is useful for verifying signatures or specifying destinations, but you can't sign transactions without the secret seed.
import stellarsdk
// Public key only (cannot sign)
let keyPair = try! KeyPair(accountId: "GCZHXL5HXQX5ABDM26LHYRCQZ5OJFHLOPLZX47WEBP3V2PF5AVFK2A5D")For wallet backup and recovery. The SDK supports 12 or 24 word phrases:
import stellarsdk
// Generate mnemonic -- choose your preferred length:
let mnemonic = WalletUtils.generate24WordMnemonic() // 24 words (recommended)
// or: let mnemonic = WalletUtils.generate12WordMnemonic() // 12 words
// Store these words securely -- they control all derived accounts
// Derive multiple accounts from one mnemonic
let keyPair0 = try! WalletUtils.createKeyPair(mnemonic: mnemonic, passphrase: nil, index: 0) // First account
let keyPair1 = try! WalletUtils.createKeyPair(mnemonic: mnemonic, passphrase: nil, index: 1) // Second account
// Restore from existing words
let words = "your twelve or twenty four word phrase goes here ..."
let restoredKeyPair = try! WalletUtils.createKeyPair(mnemonic: words, passphrase: nil, index: 0)Create accounts, fund them, and query their data from the network.
On testnet, Friendbot gives you 10,000 free test XLM to experiment with. This is the easiest way to get started.
import stellarsdk
let sdk = StellarSDK.testNet()
let keyPair = try! KeyPair.generateRandomKeyPair()
let response = await sdk.accounts.createTestAccount(accountId: keyPair.accountId)
switch response {
case .success(let details):
print("Funded: \(details)")
case .failure(let error):
print("Error: \(error)")
}On the public network, there's no Friendbot. You need an existing funded account to create new accounts using the CreateAccountOperation. The new account receives a starting balance from the source account.
import stellarsdk
let sdk = StellarSDK.publicNet()
let sourceKeyPair = try! KeyPair(secretSeed: "SAPS66IJDXUSFDSDKIHR4LN6YPXIGCM5FBZ7GE66FDKFJRYJGFW7ZHYF")
let newKeyPair = try! KeyPair.generateRandomKeyPair()
// Source account must already exist and have enough XLM for the new account's starting balance + fees
let accDetailsResponse = await sdk.accounts.getAccountDetails(accountId: sourceKeyPair.accountId)
switch accDetailsResponse {
case .success(let sourceAccount):
do {
let createOp = try CreateAccountOperation(
sourceAccountId: nil,
destinationAccountId: newKeyPair.accountId,
startBalance: 10.0 // Starting balance in XLM
)
let transaction = try Transaction(
sourceAccount: sourceAccount,
operations: [createOp],
memo: Memo.none
)
try transaction.sign(keyPair: sourceKeyPair, network: Network.public)
let submitResult = await sdk.transactions.submitTransaction(transaction: transaction)
switch submitResult {
case .success(let details):
print("Account created: \(newKeyPair.accountId)")
print("Hash: \(details.transactionHash)")
case .destinationRequiresMemo(let destinationAccountId):
print("Destination \(destinationAccountId) requires memo")
case .failure(let error):
print("Error: \(error)")
}
} catch {
print("Error: \(error)")
}
case .failure(let error):
print("Error: \(error)")
}Load an account from the network to check its balances, sequence number, and signers. Always verify an account exists before sending payments to it.
import stellarsdk
let sdk = StellarSDK.testNet()
let accountId = "GCQHNQR2VM5OPXSTWZSF7ISDLE5XZRF73LNU6EOZXFQG2IJFU4WB7VFY"
let accDetailsResponse = await sdk.accounts.getAccountDetails(accountId: accountId)
switch accDetailsResponse {
case .success(let account):
print("Sequence: \(account.sequenceNumber)")
// List balances
for balance in account.balances {
switch balance.assetType {
case AssetTypeAsString.NATIVE:
print("XLM: \(balance.balance)")
default:
print("\(balance.assetCode!): \(balance.balance)")
}
}
// List signers
for signer in account.signers {
print("Signer: \(signer.key) (weight: \(signer.weight))")
}
case .failure(let error):
print("Error: \(error)")
}Construct transactions by adding operations, setting fees, and preparing for submission.
Transactions are built by passing operations and options to the Transaction initializer:
import stellarsdk
// sourceAccount loaded via await sdk.accounts.getAccountDetails(...)
// operation1, operation2 built via operation constructors (see below)
let transaction = try Transaction(
sourceAccount: sourceAccount,
operations: [operation1, operation2],
memo: Memo.text("Payment reference"),
maxOperationFee: 200 // 200 stroops per operation
)Each operation type has its own constructor. Build the operations first, then add them to the transaction. Operations execute in order.
import stellarsdk
// Build operations
let paymentOp = try PaymentOperation(
sourceAccountId: nil,
destinationAccountId: "GDESTINATION...",
asset: Asset(type: AssetType.ASSET_TYPE_NATIVE)!,
amount: 100.50
)
let trustAsset = Asset(
type: AssetType.ASSET_TYPE_CREDIT_ALPHANUM4,
code: "USD",
issuer: try! KeyPair(accountId: "GISSUER...")
)!
let changeTrustAsset = ChangeTrustAsset(
type: trustAsset.type,
code: trustAsset.code,
issuer: trustAsset.issuer
)!
let trustOp = ChangeTrustOperation(
sourceAccountId: nil,
asset: changeTrustAsset,
limit: nil
)
// Add operations to transaction
let transaction = try Transaction(
sourceAccount: sourceAccount,
operations: [trustOp, paymentOp], // First: establish trustline, Then: send payment
memo: Memo.none
)Transactions need a valid signature before the network accepts them. The signature proves the source account authorized the transaction. Use the correct network passphrase when signing -- testnet and public have different passphrases, and a mismatch causes the transaction to fail.
import stellarsdk
// After building a transaction, sign it with the source account's keypair
// Use the correct network -- testnet and public have different passphrases!
try transaction.sign(keyPair: sourceKeyPair, network: Network.testnet)
// Multi-sig accounts: add signatures from all required signers
// try transaction.sign(keyPair: keyPairA, network: Network.testnet)
// try transaction.sign(keyPair: keyPairB, network: Network.testnet)
// Submit to the network
let submitResult = await sdk.transactions.submitTransaction(transaction: transaction)
switch submitResult {
case .success(let details):
print("Hash: \(details.transactionHash)")
case .destinationRequiresMemo(let accountId):
print("Destination \(accountId) requires memo")
case .failure(let error):
print("Error: \(error)")
}Here's a full example that sends 100 XLM on testnet. It loads the sender's account, builds a payment, signs it, and submits to the network.
import stellarsdk
let sdk = StellarSDK.testNet()
let senderKeyPair = try! KeyPair(secretSeed: "SA52PD5FN425CUONRMMX2CY5HB6I473A5OYNIVU67INROUZ6W4SPHXZB")
let destination = "GCRFFUKMUWWBRIA6ABRDFL5NKO6CKDB2IOX7MOS2TRLXNXQD255Z2MYG"
let accDetailsResponse = await sdk.accounts.getAccountDetails(accountId: senderKeyPair.accountId)
switch accDetailsResponse {
case .success(let senderAccount):
do {
let paymentOp = try PaymentOperation(
sourceAccountId: nil,
destinationAccountId: destination,
asset: Asset(type: AssetType.ASSET_TYPE_NATIVE)!,
amount: 100
)
let transaction = try Transaction(
sourceAccount: senderAccount,
operations: [paymentOp],
memo: Memo.text("Coffee payment")
)
try transaction.sign(keyPair: senderKeyPair, network: Network.testnet)
let submitResult = await sdk.transactions.submitTransaction(transaction: transaction)
switch submitResult {
case .success(let details):
print("Payment sent! Hash: \(details.transactionHash)")
case .destinationRequiresMemo(let accountId):
print("Destination \(accountId) requires memo")
case .failure(let error):
print("Error: \(error)")
}
} catch {
print("Error: \(error)")
}
case .failure(let error):
print("Error: \(error)")
}The SDK connects to Horizon servers to query account data and submit transactions. Use testnet for development, public network for production.
import stellarsdk
// Testnet (https://horizon-testnet.stellar.org)
let testnetSdk = StellarSDK.testNet()
// Public network (https://horizon.stellar.org)
let publicSdk = StellarSDK.publicNet()
// Custom Horizon server
let customSdk = StellarSDK(withHorizonUrl: "https://horizon.your-company.com")Soroban is Stellar's smart contract platform. To interact with smart contracts, you connect to a Soroban RPC server instead of Horizon.
Create a SorobanServer instance to interact with the Soroban RPC endpoint.
import stellarsdk
// Testnet
let server = SorobanServer(endpoint: "https://soroban-testnet.stellar.org")
// Mainnet
let mainnetServer = SorobanServer(endpoint: "https://soroban.stellar.org")Check if the Soroban RPC server is running and see which ledger range it has available.
import stellarsdk
let server = SorobanServer(endpoint: "https://soroban-testnet.stellar.org")
let healthResponse = await server.getHealth()
switch healthResponse {
case .success(let health):
if health.status == HealthStatus.HEALTHY {
print("Server is healthy")
print("Latest ledger: \(health.latestLedger)")
print("Oldest ledger: \(health.oldestLedger)")
}
case .failure(let error):
print("Error: \(error)")
}Get the current ledger sequence and protocol version. Useful for checking network status.
import stellarsdk
let server = SorobanServer(endpoint: "https://soroban-testnet.stellar.org")
let ledgerResponse = await server.getLatestLedger()
switch ledgerResponse {
case .success(let ledger):
print("Ledger sequence: \(ledger.sequence)")
print("Protocol version: \(ledger.protocolVersion)")
case .failure(let error):
print("Error: \(error)")
}For deploying contracts, invoking functions, and handling Soroban transactions, see the Soroban Guide.
Network requests can fail for many reasons -- invalid account IDs, network issues, or server errors. The SDK returns result enums for all Horizon requests that you can pattern-match on.
import stellarsdk
let sdk = StellarSDK.testNet()
let response = await sdk.accounts.getAccountDetails(accountId: "GINVALIDACCOUNTID")
switch response {
case .success(let account):
print("Account: \(account.accountId)")
case .failure(let error):
switch error {
case .notFound(let message, _):
print("Not found: \(message)")
case .badRequest(let message, _):
print("Bad request: \(message)")
default:
print("Error: \(error)")
}
}When a transaction fails, the error response contains result codes explaining what went wrong -- both at the transaction level and for each operation.
import stellarsdk
let submitResult = await sdk.transactions.submitTransaction(transaction: transaction)
switch submitResult {
case .success(let details):
print("Success! Hash: \(details.transactionHash)")
case .destinationRequiresMemo(let accountId):
print("Destination \(accountId) requires memo")
case .failure(let error):
switch error {
case .badRequest(_, let errorResponse):
if let extras = errorResponse?.extras,
let resultCodes = extras.resultCodes {
print("Transaction: \(resultCodes.transaction ?? "unknown")")
if let opCodes = resultCodes.operations {
for (i, code) in opCodes.enumerated() {
print("Operation \(i): \(code)")
}
}
}
default:
print("Error: \(error)")
}
}| Code | Meaning |
|---|---|
tx_bad_seq |
Wrong sequence number. Reload account and retry. |
tx_insufficient_fee |
Fee too low. Increase maxOperationFee. |
tx_insufficient_balance |
Not enough XLM for operation + fees + reserves. |
op_underfunded |
Source lacks funds for payment amount. |
op_no_trust |
Destination lacks trustline for asset. |
op_line_full |
Destination trustline limit exceeded. |
op_no_destination |
Destination account doesn't exist. |
1. Never expose secret seeds
// Bad
print("Error with account: \(keyPair.secretSeed!)")
// Good
print("Error with account: \(keyPair.accountId)")2. Use testnet for development -- Always test against testnet first.
3. Set appropriate fees
import stellarsdk
let feeResponse = await sdk.feeStats.getFeeStats()
switch feeResponse {
case .success(let feeStats):
let recommendedFee = feeStats.lastLedgerBaseFee
print("Recommended fee: \(recommendedFee)")
case .failure(let error):
print("Error: \(error)")
}4. Handle errors gracefully -- Use the result enum pattern to handle all possible outcomes.
5. Verify destination exists -- Before payments, check if account exists. If not, use CreateAccountOperation.
6. Use memos for exchanges -- Many exchanges require a memo to credit your account.
- Quick Start -- First transaction in 15 minutes
- SDK Usage -- All operations, queries, and patterns
- SEP Protocols -- Authentication, deposits, cross-border payments
- Soroban Guide -- Smart contract interaction
Navigation: <- Quick Start | SDK Usage ->