Build Stellar blockchain applications in Swift using stellar-ios-mac-sdk. Use when generating Swift code for transaction building, signing, Horizon API queries, Soroban RPC, smart contract deployment and invocation, XDR encoding/decoding, and SEP protocol integration. Covers 26+ operations, 50 Horizon endpoints, 12 RPC methods, and 17 SEP implementations with Swift async/await and callback-based streaming patterns. Full Swift 6 strict concurrency support (all types Sendable).
The Stellar iOS/Mac SDK (stellarsdk) is a native Swift library for building Stellar applications on iOS 15+ and macOS 12+. It provides 100% Horizon API coverage (50/50 endpoints), 100% Soroban RPC coverage (12/12 methods), and 17 SEP implementations. All public APIs use Swift async/await with Swift 6 strict concurrency. The SDK has zero external dependencies.
Module name: stellarsdk (always lowercase in import statements)
.package(name: "stellarsdk", url: "[email protected]:Soneso/stellar-ios-mac-sdk.git", from: "3.4.6")
pod 'stellar-ios-mac-sdk', '~> 3.4.6'
All code examples below assume
import stellarsdk.If you can't find a constructor or method signature in this file or the topic references, grep
references/api_reference.md— it has all public class/method signatures.
Fundamental Stellar concepts and SDK patterns.
// Generate new keypair
let keyPair = try KeyPair.generateRandomKeyPair()
let accountId = keyPair.accountId // G-address
guard let secretSeed = keyPair.secretSeed else {
throw StellarSDKError.invalidArgument(message: "Failed to get secret seed")
}
// WARNING: Store secretSeed securely (iOS Keychain). Never log or hardcode it.
// From existing seed
let keyPair = try KeyPair(secretSeed: seed)
let publicOnly = try KeyPair(accountId: "GABC...") // public-only, cannot sign
// Fund testnet account (FriendBot)
let keyPair = try KeyPair.generateRandomKeyPair()
let sdk = StellarSDK.testNet()
let responseEnum = await sdk.accounts.createTestAccount(accountId: keyPair.accountId)
// Query account
let responseEnum = await sdk.accounts.getAccountDetails(accountId: accountId)
switch responseEnum {
case .success(let accountResponse):
print("Sequence: \(accountResponse.sequenceNumber)")
print("Subentry count: \(accountResponse.subentryCount)") // Trustlines, offers, data entries
for balance in accountResponse.balances {
print("Asset: \(balance.assetType), Balance: \(balance.balance)")
// WRONG: balance.assetType == AssetType.ASSET_TYPE_NATIVE (comparing String to Int32)
// CORRECT: balance.assetType == "native" (response fields are strings)
if balance.assetType == "native" {
print(" → Native XLM balance")
} else if balance.assetType == "credit_alphanum4" || balance.assetType == "credit_alphanum12" {
print(" → Custom asset: \(balance.assetCode ?? ""):\(balance.assetIssuer ?? "")")
}
}
case .failure(let error):
print("Error: \(error)")
}
// Native XLM
let xlm = Asset(type: AssetType.ASSET_TYPE_NATIVE)!
// Issued asset (4-char code)
let issuerKeyPair = try KeyPair(accountId: "GISSUER...")
let usdc = Asset(type: AssetType.ASSET_TYPE_CREDIT_ALPHANUM4,
code: "USDC",
issuer: issuerKeyPair)!
// From canonical form
let asset = Asset(canonicalForm: "USDC:GISSUER...")!
// Pre-configured: Network.testnet, Network.public, Network.futurenet
let testnetSdk = StellarSDK.testNet()
let publicSdk = StellarSDK.publicNet()
// Custom Horizon URL
let customSdk = StellarSDK(withHorizonUrl: "https://my-horizon.example.com")
Query patterns for retrieving blockchain data. All queries return result enums (.success/.failure).
let sdk = StellarSDK.testNet()
// Account details
let accountEnum = await sdk.accounts.getAccountDetails(accountId: "GABC...")
// Transactions for account
let txEnum = await sdk.transactions.getTransactions(
forAccount: "GABC...",
from: nil,
order: .descending,
limit: 10
)
// Pagination: use cursor from last record's pagingToken
switch txEnum {
case .success(let page):
if let lastRecord = page.records.last {
let nextPage = await sdk.transactions.getTransactions(
forAccount: "GABC...",
from: lastRecord.pagingToken,
order: .descending,
limit: 10
)
}
case .failure(let error):
print("Error: \(error)")
}
For all Horizon endpoints (50/50), advanced queries, and memo inspection: Horizon API Reference
Real-time update patterns using Server-Sent Events. You must hold a strong reference to the stream item or it will close immediately.
class PaymentMonitor {
private var streamItem: OperationsStreamItem? // Strong reference required!
private let sdk = StellarSDK.testNet()
func startStreaming(accountId: String) {
streamItem = sdk.payments.stream(
for: .paymentsForAccount(account: accountId, cursor: "now")
)
streamItem?.onReceive { response in
switch response {
case .response(let id, let operationResponse):
if let payment = operationResponse as? PaymentOperationResponse {
print("[\(id)] Payment: \(payment.amount) \(payment.assetCode ?? "XLM")")
}
case .error(let error):
print("Stream error: \(error?.localizedDescription ?? "unknown")")
default:
break
}
}
}
func stopStreaming() {
streamItem?.closeStream()
streamItem = nil
}
}
For reconnection patterns and all streaming endpoints: Horizon Streaming Guide
Complete transaction lifecycle: Build -> Sign -> Submit.
let sdk = StellarSDK.testNet()
// 1. Load sender account (AccountResponse conforms to TransactionAccount)
let accountEnum = await sdk.accounts.getAccountDetails(accountId: senderKeyPair.accountId)
guard case .success(let accountResponse) = accountEnum else { return }
// 2. Create payment operation
let paymentOp = try PaymentOperation(
sourceAccountId: nil,
destinationAccountId: "GDEST...",
asset: Asset(type: AssetType.ASSET_TYPE_NATIVE)!,
amount: 100.0 // Decimal type
)
// 3. Build transaction
let transaction = try Transaction(
sourceAccount: accountResponse,
operations: [paymentOp],
memo: Memo.text("Payment"),
maxOperationFee: 100
)
// 4. Sign
try transaction.sign(keyPair: senderKeyPair, network: Network.testnet)
// 5. Submit
let submitEnum = await sdk.transactions.submitTransaction(transaction: transaction)
switch submitEnum {
case .success(let response):
print("Success! Hash: \(response.transactionHash)")
case .destinationRequiresMemo(let accountId):
print("SEP-29: Destination \(accountId) requires memo")
case .failure(let error):
print("Failed: \(error)")
}
For all 26+ operations (ChangeTrust, ManageSellOffer, CreateAccount, etc.): Operations Reference
RPC endpoint patterns for Soroban smart contract queries.
let server = SorobanServer(endpoint: "https://soroban-testnet.stellar.org")
server.enableLogging = true // Optional: debug logging
// Health check
let healthEnum = await server.getHealth()
// Network info
let networkEnum = await server.getNetwork()
For all 12 RPC methods (getAccount, simulateTransaction, getEvents, etc.): RPC Reference
Contract deployment and invocation patterns using SorobanClient.
let keyPair = try KeyPair(secretSeed: secretSeed)
let rpcUrl = "https://soroban-testnet.stellar.org"
// Step 1: Install WASM
let wasmHash = try await SorobanClient.install(
installRequest: InstallRequest(
rpcUrl: rpcUrl,
network: Network.testnet,
sourceAccountKeyPair: keyPair,
wasmBytes: wasmData
)
)
// Step 2: Deploy instance
let client = try await SorobanClient.deploy(
deployRequest: DeployRequest(
rpcUrl: rpcUrl,
network: Network.testnet,
sourceAccountKeyPair: keyPair,
wasmHash: wasmHash,
constructorArgs: [SCValXDR.u32(1000)], // optional — see soroban_contracts.md
enableServerLogging: false
)
)
print("Contract ID: \(client.contractId)")
// Create client for existing contract
let client = try await SorobanClient.forClientOptions(
options: ClientOptions(
sourceAccountKeyPair: keyPair,
contractId: "CABC...",
network: Network.testnet,
rpcUrl: rpcUrl
)
)
// Invoke method (handles simulation, signing, submission)
let result = try await client.invokeMethod(
name: "hello",
args: [SCValXDR.symbol("world")]
)
For multi-auth workflows, low-level deploy/invoke, and contract authorization: Smart Contracts Guide
XDR is Stellar's binary serialization format.
// Encode transaction to XDR
let xdrBase64 = try transaction.encodedEnvelope()
// Decode XDR to transaction
let transaction = try Transaction(envelopeXdr: xdrBase64)
// Soroban contract values
let boolVal = SCValXDR.bool(true)
let u32Val = SCValXDR.u32(42)
let symbolVal = SCValXDR.symbol("transfer")
let addressVal = SCValXDR.address(try SCAddressXDR(accountId: "GABC...")) // SCAddressXDR throws, .address() does not
For all XdrSCVal types and encoding/decoding utilities: XDR Reference
let responseEnum = await sdk.accounts.getAccountDetails(accountId: "GINVALID...")
switch responseEnum {
case .success(let account):
print("Found: \(account.accountId)")
case .failure(let error):
switch error {
case .notFound(let message, _):
print("Account not found: \(message)")
case .rateLimitExceeded(let message, _):
print("Rate limited: \(message)")
default:
print("Other error: \(error)")
}
}
let submitEnum = await sdk.transactions.submitTransaction(transaction: transaction)
switch submitEnum {
case .success(let response):
print("Success: \(response.transactionHash)")
case .failure(let error):
if case .badRequest(_, let errorResponse) = error {
if let resultCodes = errorResponse?.extras?.resultCodes {
print("TX code: \(resultCodes.transaction ?? "unknown")")
print("Op codes: \(resultCodes.operations ?? [])")
}
}
case .destinationRequiresMemo:
print("SEP-29 memo required")
}
For comprehensive error catalog and solutions: Troubleshooting Guide
Never hardcode secret seeds. Use iOS Keychain for storage. Always verify transaction details before signing. Validate network passphrases to prevent mainnet accidents.
The SDK implements 17 Stellar Ecosystem Proposals (SEPs): SEP-01 (TOML), SEP-02 (Federation), SEP-05 (Key Derivation), SEP-10 (Web Auth), SEP-24 (Interactive deposit/withdrawal), and more.
Multi-signature accounts, sponsored reserves, claimable balances, liquidity pools, muxed accounts (M-addresses), fee-bump transactions, path payments.
External Resources:
Module name is lowercase:
// WRONG: import StellarSDK
// CORRECT:
import stellarsdk
Stream items must be retained:
// WRONG: stream closes immediately
func bad() {
let _ = sdk.payments.stream(for: .paymentsForAccount(account: "G...", cursor: nil))
}
// CORRECT: store as instance property
class Monitor {
var streamItem: OperationsStreamItem? // Strong reference
func start() {
streamItem = sdk.payments.stream(for: .paymentsForAccount(account: "G...", cursor: nil))
}
}
Amounts are Decimal, not String:
// Operations use Decimal
let payment = try PaymentOperation(
sourceAccountId: nil,
destinationAccountId: "GDEST...",
asset: Asset(type: AssetType.ASSET_TYPE_NATIVE)!,
amount: 100.0 // Decimal type
)
// Horizon responses return String balances with 7 decimal places
// e.g., "100.0000000" for 100 XLM — always 7 decimals, never "100"
let balance = accountResponse.balances[0]
guard let amountDecimal = Decimal(string: balance.balance) else { throw ... }
let payment = try PaymentOperation(..., amount: amountDecimal)
Sequence number is already Int64:
// WRONG: Int64(accountResponse.sequenceNumber)! -- compile error
// CORRECT: use directly
let account = try Account(
accountId: accountResponse.accountId,
sequenceNumber: accountResponse.sequenceNumber // Already Int64
)
Sequence number mutation: Transaction(sourceAccount:) increments the source account's sequence number internally. Reload the account before building a new transaction. Don't increment manually.
// CORRECT: reload account, Transaction increments sequence internally
let accountResponse = await sdk.accounts.getAccountDetails(accountId: accountId)
guard case .success(let account) = accountResponse else { return }
let tx = try Transaction(sourceAccount: account, operations: [op], memo: Memo.none)
// account.sequenceNumber is now N+1
// WRONG: manually incrementing — Transaction already does this
// account.incrementSequenceNumber() // now N+1
// let tx = try Transaction(sourceAccount: account, ...) // uses N+2 — tx_bad_seq!
Network passphrase must match SDK:
// WRONG: Mismatched network
let sdk = StellarSDK.publicNet()
try transaction.sign(keyPair: keyPair, network: Network.testnet) // ERROR!
// CORRECT: Match SDK and signing network
let sdk = StellarSDK.publicNet()
try transaction.sign(keyPair: keyPair, network: .public)
KeyPair from accountId is public-only (cannot sign):
// WRONG: Trying to sign with public-only KeyPair
let publicKeyPair = try KeyPair(accountId: "GABC...")
try transaction.sign(keyPair: publicKeyPair, network: Network.testnet) // ERROR!
// CORRECT: Load from secretSeed for signing
let signingKeyPair = try KeyPair(secretSeed: "SABC...")
try transaction.sign(keyPair: signingKeyPair, network: Network.testnet)
Insufficient signatures return op_bad_auth, not tx_bad_auth:
// Multi-sig auth failures appear in operation codes, not transaction code
let submitEnum = await sdk.transactions.submitTransaction(transaction: transaction)
switch submitEnum {
case .failure(let error):
if case .badRequest(_, let errorResponse) = error {
if let resultCodes = errorResponse?.extras?.resultCodes {
// WRONG: checking transaction code for auth failure
if resultCodes.transaction == "tx_bad_auth" { /* never matches */ }
// CORRECT: check operation codes for op_bad_auth
if let opCodes = resultCodes.operations, opCodes.contains("op_bad_auth") {
print("Insufficient signatures!")
}
}
}