kmp-stellar-sdk

star 9

Build Stellar blockchain applications with the Soneso KMP (Kotlin Multiplatform) SDK. Covers keypair generation, transaction building, Horizon queries, Soroban smart contracts, smart accounts (OpenZeppelin) with passkey / WebAuthn authentication, XDR encoding, and SEP integrations. Use when the developer is working with Kotlin, KMP, or Android and mentions Stellar, blockchain, cryptocurrency, passkey, or smart wallet operations.

Soneso By Soneso schedule Updated 6/13/2026

name: kmp-stellar-sdk description: Build Stellar blockchain applications with the Soneso KMP (Kotlin Multiplatform) SDK. Covers keypair generation, transaction building, Horizon queries, Soroban smart contracts, smart accounts (OpenZeppelin) with passkey / WebAuthn authentication, XDR encoding, and SEP integrations. Use when the developer is working with Kotlin, KMP, or Android and mentions Stellar, blockchain, cryptocurrency, passkey, or smart wallet operations. license: Apache-2.0 compatibility: Requires Kotlin 2.2+ and com.soneso.stellar:stellar-sdk 1.8.0. Supports JVM (Java 17+), Android (API 24+), iOS, macOS, and JavaScript (Browser/Node.js). metadata: author: soneso version: "1.1.6" sdk_repo: https://github.com/Soneso/kmp-stellar-sdk

Stellar SDK for Kotlin Multiplatform

Overview

The KMP Stellar SDK (com.soneso.stellar:stellar-sdk) is a Kotlin Multiplatform library for building Stellar blockchain applications on JVM/Android, iOS/macOS, and JavaScript (Browser/Node.js). It provides full Horizon API coverage, full Soroban RPC coverage, 27 Stellar operations, and 14 SEP implementations. Crypto operations (KeyPair, signing) are suspend functions. Uses Ktor for networking and kotlinx.coroutines for async.

Installation

// build.gradle.kts
dependencies {
    implementation("com.soneso.stellar:stellar-sdk:1.8.0")
}

All code examples below assume import com.soneso.stellar.sdk.* and run inside a suspend context (coroutine).

If you can't find a constructor or method in this file or the topic references, grep references/api_reference.md.

1. Stellar Basics

Keys and KeyPairs

// IMPORTANT: KeyPair.random() and KeyPair.fromSecretSeed() are suspend functions
val keyPair = KeyPair.random()
val accountId: String = keyPair.getAccountId()   // G... public address
val secretSeed: CharArray? = keyPair.getSecretSeed() // S... secret seed (CharArray for security)

// From existing seed (also suspend). Load the seed from platform-secure storage
// (Keychain / Android Keystore / OS keyring) — NEVER commit a real seed to source.
val restored = KeyPair.fromSecretSeed("S_YOUR_SECRET_SEED_HERE")   // 56-char S... strkey
val publicOnly = KeyPair.fromAccountId(accountId) // public-key-only, cannot sign

// WRONG: KeyPair.random() without suspend — it IS a suspend function
// WRONG: keyPair.accountId — property access does NOT exist
// CORRECT: keyPair.getAccountId() — use the getter method
// WRONG: keyPair.secretSeed — property access does NOT exist
// CORRECT: keyPair.getSecretSeed() — returns CharArray? (null if public-only)

Accounts

import com.soneso.stellar.sdk.horizon.HorizonServer
import com.soneso.stellar.sdk.horizon.responses.AccountResponse

val server = HorizonServer("https://horizon-testnet.stellar.org")
val keyPair = KeyPair.random()
val accountId = keyPair.getAccountId()

// Fund on testnet using FriendBot (10,000 test XLM)
// WRONG: FriendBot.fundTestAccount(...) — method does NOT exist
// CORRECT: FriendBot.fundTestnetAccount(accountId) — returns Boolean
FriendBot.fundTestnetAccount(accountId)

// Query account
// WRONG: HorizonServer.TESTNET or StellarSDK.TESTNET — no static factory
// CORRECT: HorizonServer("https://horizon-testnet.stellar.org")
val account: AccountResponse = server.accounts().account(accountId)
println("Sequence: ${account.sequenceNumber}")

for (balance in account.balances) {
    if (balance.assetType == "native") println("XLM: ${balance.balance}")
    else println("${balance.assetCode}: ${balance.balance}")
}

for (signer in account.signers) {
    println("${signer.key} weight=${signer.weight} type=${signer.type}")
}

Assets

// Native asset (XLM)
val xlm: Asset = AssetTypeNative // Singleton object, not a function call

// 1-4 char code -> AssetTypeCreditAlphaNum4; 5-12 char -> AssetTypeCreditAlphaNum12
val usdc = Asset.createNonNativeAsset("USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")

// From canonical form ("native" or "CODE:ISSUER")
val parsed = Asset.create("USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")

// WRONG: Asset.NATIVE or AssetTypeNative() — it's a data object, not a constructor
// CORRECT: AssetTypeNative (no parentheses)

// Access code and issuer (cast to AssetTypeCreditAlphaNum)
if (usdc is AssetTypeCreditAlphaNum) {
    val code: String = usdc.code
    val issuer: String = usdc.issuer
}
// toString() returns canonical form: "native" for XLM, "USDC:GISSUER..." for custom

// Network passphrases: Network.TESTNET, Network.PUBLIC, Network.FUTURENET
// Horizon servers: construct with URL string
val server = HorizonServer("https://horizon-testnet.stellar.org")
val publicServer = HorizonServer("https://horizon.stellar.org")

2. Horizon API - Fetching Data

Query patterns for retrieving blockchain data. All request builders support .cursor(), .limit(), .order() for pagination.

Query Accounts

import com.soneso.stellar.sdk.horizon.requests.RequestBuilder.Order

val server = HorizonServer("https://horizon-testnet.stellar.org")

// Single account
val account = server.accounts().account(accountId)

// Builder pattern: forSigner(), forAsset(), limit(), order(), cursor()
val bySigner = server.accounts()
    .forSigner(accountId)
    .limit(10)
    .order(Order.DESC)
    .execute()
for (acct in bySigner.records) {
    println("Account: ${acct.accountId}")
}

Query Transactions

val txPage = server.transactions()
    .forAccount(accountId)
    .order(Order.DESC)
    .limit(5)
    .execute()

for (tx in txPage.records) {
    println("${tx.hash} ledger=${tx.ledger}")
}

// Pagination: cursor from last result
val nextPage = server.transactions()
    .forAccount(accountId)
    .cursor(txPage.records.last().pagingToken)
    .limit(5).order(Order.DESC).execute()

// Single transaction by hash
val single = server.transactions().transaction("abc123...")

For all Horizon endpoints, advanced queries, and pagination patterns: Horizon API Reference

3. Horizon API - Streaming

Real-time update patterns using Server-Sent Events (SSE). Set cursor to "now" for real-time events. Store the SSEStream to close later.

Stream Payments

import com.soneso.stellar.sdk.horizon.requests.EventListener
import com.soneso.stellar.sdk.horizon.requests.SSEStream
import com.soneso.stellar.sdk.horizon.responses.operations.OperationResponse
import com.soneso.stellar.sdk.horizon.responses.operations.PaymentOperationResponse

// SSE stream pattern — callback-based with EventListener interface
val stream = server.payments().forAccount(accountId).cursor("now")
    .stream(
        serializer = OperationResponse.serializer(),
        listener = object : EventListener<OperationResponse> {
            override fun onEvent(event: OperationResponse) {
                if (event is PaymentOperationResponse) {
                    println("${event.amount} from ${event.from}")
                }
            }
            override fun onFailure(error: Throwable?, responseCode: Int?) {
                println("Stream error: ${error?.message}")
            }
        }
    )
// stream.close() — always close to prevent resource leaks

Streams reconnect automatically. For all streaming endpoints: Horizon Streaming Guide

4. Transactions & Operations

Complete transaction lifecycle: Build -> Sign -> Submit.

Transaction Lifecycle

val server = HorizonServer("https://horizon-testnet.stellar.org")
val network = Network.TESTNET

// 1. Load sender keypair (suspend function)
val senderKeyPair = KeyPair.fromSecretSeed(senderSecret)
val senderAccountId = senderKeyPair.getAccountId()

// 2. Load source account (provides sequence number)
// WRONG: server.accounts().account(id) then wrap manually
// CORRECT: server.loadAccount(id) — returns Account ready for TransactionBuilder
val senderAccount = server.loadAccount(senderAccountId)

// 3. Build transaction — requires Network as second parameter
val transaction = TransactionBuilder(senderAccount, network)
    .addOperation(
        PaymentOperation(
            destination = destinationAccountId,
            asset = AssetTypeNative,
            amount = "100.50"
        )
    )
    .addMemo(MemoText("payment"))
    .setBaseFee(200) // stroops per operation (minimum: 100)
    .setTimeout(300)  // seconds from now
    .build()

// 4. Sign transaction (suspend function)
transaction.sign(senderKeyPair)

// 5. Submit to Horizon
val response = server.submitTransaction(transaction.toEnvelopeXdrBase64())
if (response.successful) {
    println("Success! Hash: ${response.hash}")
} else {
    println("Failed! Check result XDR: ${response.resultXdr}")
}

// WRONG: TransactionBuilder(account) — missing Network parameter
// CORRECT: TransactionBuilder(account, network) — Network is required

// WRONG: transaction.sign(keyPair, network) — Network is NOT a parameter of sign()
// CORRECT: transaction.sign(keyPair) — Network is set in TransactionBuilder

// WRONG: server.submitTransaction(transaction) — does NOT accept Transaction object
// CORRECT: server.submitTransaction(transaction.toEnvelopeXdrBase64()) — accepts String

Common Operations

Change Trust (Establish Trustline):

val usdc = Asset.createNonNativeAsset("USDC", issuerAccountId)
val trustline = ChangeTrustOperation(asset = usdc) // default limit = max
// With custom limit: ChangeTrustOperation(asset = usdc, limit = "1000.00")

Manage Sell Offer (DEX):

val selling = AssetTypeNative
val buying = Asset.createNonNativeAsset("USDC", issuerAccountId)

// Create new offer (offerId = 0 means new offer)
val newOffer = ManageSellOfferOperation(
    selling = selling,
    buying = buying,
    amount = "100.0",
    price = Price.fromString("0.5") // 0.5 USDC per XLM
)

For all 27 operations with parameters and examples: Operations Reference

5. Soroban RPC API

RPC endpoint patterns for Soroban smart contract queries.

import com.soneso.stellar.sdk.rpc.SorobanServer

val rpcServer = SorobanServer("https://soroban-testnet.stellar.org:443")
val health = rpcServer.getHealth() // .status, .latestLedger

For all RPC methods including event queries and transaction simulation: RPC Reference

6. Smart Contracts

Contract deployment and invocation using the high-level ContractClient.

Deploy Contract

import com.soneso.stellar.sdk.contract.ContractClient

val keyPair = KeyPair.fromSecretSeed(secretSeed)
val rpcUrl = "https://soroban-testnet.stellar.org:443"
val source = keyPair.getAccountId()

// One-step deploy: upload WASM + create contract instance
val client = ContractClient.deploy(
    wasmBytes = wasmBytes, source = source, signer = keyPair,
    network = Network.TESTNET, rpcUrl = rpcUrl
)
println("Contract ID: ${client.contractId}")

// Two-step: install() returns wasmId, then deployFromWasmId() (reuse WASM)
val wasmId = ContractClient.install(
    wasmBytes = wasmBytes, source = source, signer = keyPair,
    network = Network.TESTNET, rpcUrl = rpcUrl
)
val client2 = ContractClient.deployFromWasmId(
    wasmId = wasmId, source = source, signer = keyPair,
    network = Network.TESTNET, rpcUrl = rpcUrl
)

Invoke Contract Function

// Create client for an existing contract (loads spec from network)
val client = ContractClient.forContract(
    contractId = "CABC...",
    rpcUrl = rpcUrl,
    network = Network.TESTNET
)

// Invoke with automatic type conversion (recommended)
// Read calls return immediately; write calls auto-sign and submit
val result = client.invoke(
    functionName = "get_count",
    arguments = emptyMap(),
    source = keyPair.getAccountId(),
    signer = null  // null for read-only calls
)

// Write call with arguments
val writeResult = client.invoke(
    functionName = "increment",
    arguments = mapOf("value" to 5),
    source = keyPair.getAccountId(),
    signer = keyPair  // required for write calls
)

// Advanced: get AssembledTransaction for manual control (multi-sig workflows)
val tx = client.buildInvoke(
    functionName = "transfer",
    arguments = mapOf("from" to fromAddr, "to" to toAddr, "amount" to 1000),
    source = keyPair.getAccountId(),
    signer = keyPair
)
tx.signAndSubmit(keyPair)

For contract authorization, multi-auth workflows, and low-level deploy/invoke: Smart Contracts Guide

7. Smart Accounts (OpenZeppelin)

Passkey-authenticated Soroban smart accounts: biometric auth, multiple signers (passkey/delegated/Ed25519), context rules, policies, and optional fee sponsoring via a relayer. Entry point: OZSmartAccountKit.create(OZSmartAccountConfig(...)) — requires rpcUrl, networkPassphrase, accountWasmHash (hex), webauthnVerifierAddress (C-address), plus platform-specific webauthnProvider and storage.

  • Smart Accounts Guide — kit config, wallet create/connect, signers, transactions, credentials, events, submit / fundWallet, external signer manager, indexer
  • Context Rules & Policies — context rules, policies, multi-signer, common scenarios (recovery, rotation, __check_auth debugging), contract error codes
  • WebAuthn Platform Setup — Android, iOS, macOS, Web adapters + rpId/DAL/AASA

8. XDR Encoding & Decoding

XDR (External Data Representation) is Stellar's binary serialization format.

Transaction XDR Roundtrip

// Encode: Transaction -> base64 XDR
val xdrBase64: String = transaction.toEnvelopeXdrBase64()

// Decode: base64 XDR -> AbstractTransaction
val decoded = AbstractTransaction.fromEnvelopeXdr(xdrBase64, Network.TESTNET)
if (decoded is Transaction) {
    println("Source: ${decoded.sourceAccount}, Fee: ${decoded.fee}")
    println("Operations: ${decoded.operations.size}")
}

Working with Soroban XDR Values (Scv)

import com.soneso.stellar.sdk.scval.Scv
import com.soneso.stellar.sdk.xdr.SCValXdr

// Factory methods: toBoolean(), toUint32(), toInt64(), toString(), toSymbol(), toVoid()
val symVal  = Scv.toSymbol("transfer")
val addrVal = Scv.toAddress(Address("GABC...").toSCAddress())
val vecVal  = Scv.toVec(listOf(Scv.toUint32(1u), Scv.toUint32(2u)))
val mapVal  = Scv.toMap(linkedMapOf(symVal to Scv.toUint32(42u)))

// WRONG: Scv.toU32() / Scv.fromU32() — those method names do NOT exist
// CORRECT: Scv.toUint32() / Scv.fromUint32() — full type name, not abbreviated
// WRONG: XdrSCVal.forSymbol("transfer") — that is the Flutter SDK API
// CORRECT: Scv.toSymbol("transfer") — KMP uses the Scv utility object

// Read back: fromBoolean(), fromUint32(), fromInt64(), fromString(), fromSymbol()
val intValue: UInt = Scv.fromUint32(Scv.toUint32(42u))

To submit a pre-signed XDR envelope: server.submitTransaction(signedXdrBase64).

For all Scv factory methods and type mapping: XDR Reference | Contract Arguments

9. Error Handling & Troubleshooting

Horizon Errors

import com.soneso.stellar.sdk.horizon.exceptions.*

try {
    val account = server.accounts().account(accountId)
} catch (e: BadRequestException) {
    // HTTP 4xx error: e.code (404, 400, etc.), e.body
    println("Horizon error ${e.code}: ${e.body}")
} catch (e: TooManyRequestsException) {
    // Rate limiting (429)
    println("Rate limited: ${e.code}")
} catch (e: ConnectionErrorException) {
    // Network/connectivity error
    println("Connection error: ${e.message}")
}

Transaction Submission Errors

// submitTransaction returns TransactionResponse — check .successful
val response = server.submitTransaction(transaction.toEnvelopeXdrBase64())
if (!response.successful) {
    // Inspect resultXdr for error details
    println("Result XDR: ${response.resultXdr}")
    // Common: tx_failed (check operation results), tx_bad_seq (reload account)
}

// WRONG: response.success — property does NOT exist
// CORRECT: response.successful — Boolean property

Soroban RPC Errors

import com.soneso.stellar.sdk.rpc.responses.GetHealthResponse

val health = rpcServer.getHealth()
if (health.status != "healthy") { /* server unhealthy */ }

// Contract exceptions (10 types in com.soneso.stellar.sdk.contract.exception)
// SimulationFailedException, SendTransactionFailedException,
// TransactionFailedException, ExpiredStateException, etc.

For the full error catalog and solutions: Troubleshooting Guide

10. Security Best Practices

Covers secret key management (getSecretSeed() returns CharArray?), transaction verification, StrKey validation, and amount precision. See Security Guide.

11. SEP Implementations

The KMP SDK implements 14 SEPs: 01, 02, 05, 06, 08, 09, 10, 12, 24, 30, 31, 38, 45, 53. See SEP Implementations Guide.

Reference Documentation

Common Pitfalls

Suspend functions everywhere: KeyPair.random(), KeyPair.fromSecretSeed(), transaction.sign(), and all network calls are suspend functions. Must be called from a coroutine context.

// WRONG: calling from non-suspend context — compile error
// CORRECT: use suspend fun, or launch coroutine:
suspend fun createAccount() { val kp = KeyPair.random() }
// Android: lifecycleScope.launch { val kp = KeyPair.random() }
// Tests: @Test fun test() = runTest { val kp = KeyPair.random() }

Amounts are always Strings: All payment amounts, balances, and prices are String types (7 decimal places max).

Sequence number management: build() increments the source account's sequence number. Reload the account before building a new transaction. Don't increment manually or you get tx_bad_seq.

setBaseFee is required: TransactionBuilder requires setBaseFee() before build(). Omitting it throws IllegalStateException. Fee is per operation: N ops at setBaseFee(200) = N * 200 stroops total.

submitTransaction accepts String: server.submitTransaction(transaction.toEnvelopeXdrBase64()) -- does NOT accept a Transaction object directly.

SecretSeed is CharArray: keyPair.getSecretSeed() returns CharArray? for secure memory handling. Convert with String(charArray) when needed.

Install via CLI
npx skills add https://github.com/Soneso/kmp-stellar-sdk --skill kmp-stellar-sdk
Repository Details
star Stars 9
call_split Forks 3
navigation Branch main
article Path SKILL.md
More from Creator