Android Integration
Cartridge Controller provides native Android support through UniFFI-generated Kotlin bindings. This enables seamless integration with Android applications while maintaining full access to Controller functionality.
Prerequisites
- Android Studio with NDK installed
- Rust toolchain with Android targets:
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android - cargo-ndk for cross-compilation:
cargo install cargo-ndk
Installation
Add the UniFFI-generated bindings to your Android project. The bindings are generated from the Controller Rust library and include all necessary Kotlin classes and interfaces.
Place the generated controller_uniffi.kt file in your source directory and ensure the native libraries are included in jniLibs/.
Core Types
The binding exposes several key types for interacting with Controller.
FieldElement
Blockchain field elements are represented as strings:
typealias FieldElement = StringCall
Represents a contract call:
data class Call(
var contractAddress: FieldElement,
var entrypoint: String,
var calldata: List<FieldElement>
)Session Policies
Define permissions for session accounts:
data class SessionPolicy(
var contractAddress: FieldElement,
var entrypoint: String
)
data class SessionPolicies(
var policies: List<SessionPolicy>,
var maxFee: FieldElement
)SignerType
Supported signer types:
enum class SignerType {
WEBAUTHN,
STARKNET
}Creating a Controller
Initialize a headless Controller with your own signing key:
import com.cartridge.controller.*
val owner = Owner(privateKey = "0x...")
// Class hash for Controller contract (use appropriate version)
val classHash = "0x05f..."
val controller = controllerNewHeadless(
appId = "my_app",
username = "player123",
classHash = classHash,
rpcUrl = "https://api.cartridge.gg/x/starknet/sepolia",
owner = owner,
chainId = "0x534e5f5345504f4c4941" // SEPOLIA
)
// Access controller properties
val address: FieldElement = controller.address()
val appId: String = controller.appId()
val chainId: FieldElement = controller.chainId()
val username: String = controller.username()User Authentication
Handle user signup and chain switching:
// Sign up with signer type
try {
controller.signup(
signerType = SignerType.STARKNET,
sessionExpiration = null,
cartridgeApiUrl = null
)
} catch (e: ControllerException) {
// Handle signup error
}
// Switch to a different chain
controller.switchChain(rpcUrl = "https://api.cartridge.gg/x/starknet/mainnet")
// Disconnect the controller
controller.disconnect()Creating a SessionAccount
Create a session account for executing transactions without repeated signatures:
val sessionAccount = SessionAccount(
rpcUrl = "https://api.cartridge.gg/x/starknet/sepolia",
privateKey = "0x...",
address = "0x...",
ownerGuid = "0x...",
chainId = "0x534e5f5345504f4c4941",
policies = SessionPolicies(
policies = listOf(
SessionPolicy(
contractAddress = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
entrypoint = "transfer"
)
),
maxFee = "0x2386f26fc10000"
),
sessionExpiration = 3600UL // 1 hour
)Alternatively, create from subscription (waits for browser authorization):
val sessionAccount = sessionAccountCreateFromSubscribe(
privateKey = "0x...",
policies = policies,
rpcUrl = "https://api.cartridge.gg/x/starknet/sepolia",
cartridgeApiUrl = "https://api.cartridge.gg"
)Executing Transactions
Execute transactions through the Controller or SessionAccount:
val calls = listOf(
Call(
contractAddress = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
entrypoint = "transfer",
calldata = listOf(
"0x1234...", // recipient
"0x100", // amount low
"0x0" // amount high
)
)
)
// Execute via Controller
try {
val txHash = controller.execute(calls = calls)
println("Transaction submitted: $txHash")
} catch (e: ControllerException) {
// Handle execution error
}
// Execute via SessionAccount
try {
val txHash = sessionAccount.execute(calls = calls)
println("Session transaction: $txHash")
} catch (e: ControllerException) {
// Handle error
}
// Execute from outside (for meta-transactions)
try {
val txHash = sessionAccount.executeFromOutside(calls = calls)
println("External transaction: $txHash")
} catch (e: ControllerException) {
// Handle error
}Transfer Helper
Transfer tokens directly:
try {
val txHash = controller.transfer(
recipient = "0x1234...",
amount = "0x100"
)
} catch (e: ControllerException) {
// Handle transfer error
}Error Handling
The bindings define ControllerException for error handling:
try {
controller.execute(calls = calls)
} catch (e: ControllerException.InitializationException) {
println("Initialization failed: ${e.message}")
} catch (e: ControllerException.SignupException) {
println("Signup failed: ${e.message}")
} catch (e: ControllerException.ExecutionException) {
println("Execution failed: ${e.message}")
} catch (e: ControllerException.NetworkException) {
println("Network error: ${e.message}")
} catch (e: ControllerException.StorageException) {
println("Storage error: ${e.message}")
} catch (e: ControllerException.InvalidInput) {
println("Invalid input: ${e.message}")
} catch (e: ControllerException.DisconnectException) {
println("Disconnect failed: ${e.message}")
}Building Native Libraries
Build the native libraries for each Android ABI using cargo-ndk:
# Build for all supported architectures
cargo ndk -t armeabi-v7a -t arm64-v8a -t x86 -t x86_64 -o ./jniLibs build --release
# Or build for specific targets
cargo ndk -t arm64-v8a -o ./jniLibs build --releasePlace the resulting .so files in your Android project:
app/
src/
main/
jniLibs/
arm64-v8a/
libcontroller_uniffi.so
armeabi-v7a/
libcontroller_uniffi.so
x86/
libcontroller_uniffi.so
x86_64/
libcontroller_uniffi.soLoad the library in your application:
companion object {
init {
System.loadLibrary("controller_uniffi")
}
}