Skip to content

Kuira SDK — Integration Guide

Alpha — 0.1.0-alpha03

Build a Midnight zero-knowledge dApp on Android by adding one dependency, declaring your own passkey domain, and hosting one tiny JSON file on it. That's the whole on-ramp.

This guide walks the recipe end-to-end. Two public reference apps consume the SDK exactly as described below — clone either and read along:

  • Kuira Starter — the minimal counter dApp. Sigil identity + wallet + a one-circuit Compact contract. Clone it, set your applicationId + rpId, run. (Also a GitHub template.)
  • BBoard — a shared on-chain bulletin board (post / take-down) showing the deploy → call → read flow.

The snippets below are extracted from those apps; when in doubt, the linked source is the ground truth.


What the SDK gives you

Adding the one line below brings:

  • Sigil identity — a passkey-derived DID (did:key + Ed25519). One biometric, no recovery phrase, no maintainer dependency. Backed up to Google Block Store, encrypted with a key you can't read.
  • Wallet — Midnight HD wallet (unshielded + shielded NIGHT, Dust gas), with live balance, send, receive, and a drop-in Compose wallet panel if you want it.
  • Contract surface — deploy / call / read state on any .compact contract; ZK proof generation runs on-device (no proof-server hop required).
  • Indexer + chain client — block / state / event subscription, backed by Midnight's official indexer.
  • App-state cloud backup — your dApp's per-user data rides the sigil's Block Store backup automatically. The backup blob is PRF-encrypted client-side before Google's Block Store touches it.

Drop in the prebuilt UI, build your own on the same contracts, or go fully headless — see Choose your level below.


1. Prerequisites

Android Gradle Plugin ≥ 8.13 (matches the SDK build)
Kotlin ≥ 2.3
compileSdk 35+
minSdk 30 — required (the SDK uses keystore APIs added in API 30: biometric ⊕ device-credential)
Hilt + KSP Required — the SDK is Hilt-first; your app applies both
A web domain you control Required for the passkey relying party (GitHub Pages is fine for dev)
Your app's signing-cert SHA-256 Needed for assetlinks.json (step 4)

2. Add the dependency — one line

Choose your level

Kuira meets you at the level you want — and the dependency you pick reflects it:

  • Drop in the prebuilt UIio.github.kuiralabs:dapp-ui. Ready-made, themeable Compose components — the wallet pill, the sigil pill, and the Settings panel (network, recovery phrase, lock, sign-out), all in one PanelBar — wired to SDK state out of the box with safe security defaults (FLAG_SECURE, biometric gates, an auto-clearing clipboard). Restyle with a colors object; you write no wallet logic. The fastest path — see Set up Sigil identity.
  • Build your own experience — also dapp-ui (its ViewModels) or midnight-sdk. Render your own screens on the same public contracts the pills are built on — WalletRecovery, WalletPanelViewModel, SigilPanelViewModel / SigilSession, MidnightContract, MidnightSdk. The SDK keeps the hard parts (crypto, secure vault, on-device proving, sync, session-lock); the screens are yours. Every bundled component is just the first consumer of a contract you can call directly — e.g. Reveal & restore the recovery phrase.
  • Go headlessio.github.kuiralabs:midnight-sdk. The full wallet, identity, and contract surface with no Compose pulled in; you build the UI layer top-to-bottom. See § 7 Going headless.

Theming today, more tuning coming

The prebuilt components expose theming now plus a small set of behavioral knobs; richer per-component tuning hooks are on the roadmap. Until a knob exists, the build-your-own path gives full control of that surface — the contracts carry no UI policy.

Maven Central is already in every Android project's defaults, so there's no repo config to add. In app/build.gradle.kts:

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")          // only if your own UI is Compose
    id("com.google.devtools.ksp")
    id("com.google.dagger.hilt.android")
}

android {
    namespace = "com.example.mydapp"
    compileSdk = 35
    defaultConfig {
        applicationId = "com.example.mydapp"
        minSdk = 30
        targetSdk = 35
    }
}

dependencies {
    // ── Pick ONE Kuira entry (see "Choose your level" above) ──
    //
    // Prebuilt pills + the ViewModels behind them (Compose). Drop the wallet
    // and sigil panels in as-is, OR build your own UI on the same contracts.
    implementation("io.github.kuiralabs:dapp-ui:0.1.0-alpha03")
    //
    // OR — headless core, no Compose pulled in. For dApps building the whole
    // UI layer top-to-bottom.
    // implementation("io.github.kuiralabs:midnight-sdk:0.1.0-alpha03")

    // Hilt processor — required, the SDK is Hilt-first
    ksp("com.google.dagger:hilt-android-compiler:2.58")

    // your own app code, your own Compose deps, etc.
}

That single Kuira line brings the whole consumer surface — passkey, sigil, wallet, ZK proving, contract deploy/call, indexer, panel UI (if you picked dapp-ui) — onto your compile classpath transitively. No per-module redeclaration.


3. Declare YOUR passkey domain

The SDK provides no default PasskeyConfig — that's deliberate. A passkey is bound to a domain, and that domain must be yours. If the SDK baked in a default, every consumer would silently route through the maintainer's domain and PRF would only work after the maintainer added them to a maintainer-hosted assetlinks.json — i.e. the "open" SDK would actually be permissioned.

Add a tiny Hilt module (anywhere in your di/):

package com.example.mydapp.di

import com.midnight.kuira.core.identity.passkey.PasskeyConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object IdentityConfigModule {

    @Provides
    @Singleton
    fun providePasskeyConfig() = PasskeyConfig(
        rpId   = "mydapp.example.com",   // YOUR domain (matches assetlinks.json below)
        rpName = "My dApp",              // shown in the biometric prompt
    )
}

If you forget this, you'll get a clear Dagger missing-binding error at build time. That's intentional — it forces you to declare your domain before shipping.


4. Host assetlinks.json on YOUR domain

Android's CredentialManager checks Digital Asset Links to verify your app may use passkeys for your domain. Place this file at:

https://<your-domain>/.well-known/assetlinks.json
[{
  "relation": [
    "delegate_permission/common.get_login_creds",
    "delegate_permission/common.handle_all_urls"
  ],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.mydapp",
    "sha256_cert_fingerprints": ["AB:CD:EF:01:23:45:67:89:..."]
  }
}]

Get your cert SHA-256:

keytool -list -v -keystore <your-keystore> -alias <your-alias> | grep SHA256
For debug builds, the default keystore is ~/.android/debug.keystore (alias androiddebugkey, password android).

Without this file (or with a wrong package name / SHA-256), the biometric prompt fails and PRF can't derive your sigil. This is a one-time host; updates are needed only when you change signing certs.


5. Develop against a localnet

You don't need testnet tokens or a deployed contract to build. The UNDEPLOYED network is a full Midnight stack — node + indexer + proof server — running locally in Docker: instant, free, and ephemeral (reset it any time). It's the recommended loop for day-to-day development; switch to PREPROD only when you want the hosted chain.

The recommended way to run and fund a localnet is the Midnight Wallet CLI (mn), installed from npm as midnight-wallet-cli. It wraps the Docker stack and wallet funding behind a few commands:

# one-time — needs Docker running + Node 20+
npm install -g midnight-wallet-cli          # installs `midnight` (alias `mn`)

mn localnet up                              # start node + indexer + proof server (Docker)
mn localnet status                          # check it's healthy
# … develop …
mn localnet down                            # tear it down

Fund the wallet your app shows (copy its address from the wallet panel's receive screen):

mn airdrop 10000 --wallet mn_addr_undeployed1…   # NIGHT for fees
mn dust register --wallet mn_addr_undeployed1…   # enable Dust (gas) generation

Localnet is ephemeral — mn localnet down (or a Docker restart) wipes all state, including funded balances. Re-airdrop after each fresh up.

5b. Let the app reach the localnet

The SDK already maps UNDEPLOYED to the right host per device type (NetworkConfig):

  • Emulator10.0.2.2 (the emulator's alias for your machine's localhost). Nothing to do.
  • Physical device127.0.0.1, so the three localnet ports must be tunnelled over the debug bridge. The SDK ships a Gradle plugin that does this automatically on every installDebug — apply it once, no configuration:
// app/build.gradle.kts
plugins {
    id("io.github.kuiralabs.localnet") version "<sdk-version>"
}

It registers an adbReverseLocalnet task wired ahead of installDebug, so deploying to a connected physical device forwards ports 9944 (node RPC), 8088 (indexer), and 6300 (proof server) for you. Emulators use 10.0.2.2 and are skipped automatically.

The example apps apply it exactly this way — see them for a working reference: Kicks, BBoard, and the starter.

Prefer a one-off without the plugin? Run it by hand:

adb reverse tcp:9944 tcp:9944   # node RPC
adb reverse tcp:8088 tcp:8088   # indexer
adb reverse tcp:6300 tcp:6300   # proof server

5c. Allow cleartext to the localnet (debug only)

Production hits HTTPS. Localnet is plain HTTP, so declare cleartext in a debug-only manifest — release builds stay HTTPS-clean:

<!-- app/src/debug/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application android:usesCleartextTraffic="true" />
</manifest>

Why debug-only: the SDK's release variants are HTTPS-clean and don't grant any cleartext allowance, so each app declares its own in a debug-only manifest — release stays cleartext-free.


6. Minimal "Hello World" — deploy + call

Your Application is @HiltAndroidApp, and your activity is a Hilt'd FragmentActivityAppCompatActivity is the usual choice, since SigilStatusPanel hosts a biometric prompt that needs a FragmentActivity host:

@HiltAndroidApp
class MyApp : Application()

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Column {
                // Drop-in SDK panels — each owns its own state. The wallet
                // panel builds the SDK (one biometric) the first time it shows.
                SigilStatusPanel()   // forge / restore identity (DID)
                WalletStatusPanel()  // balance, receive, dust register, network
                MyDappScreen()       // your UI
            }
        }
    }
}

Your dApp logic gets the SDK from the injected MidnightSdkProvider — observe sdkProvider.sdk (a StateFlow<MidnightSdk?>, non-null once the wallet panel has bootstrapped) or suspend on awaitSdk():

@HiltViewModel
class MyDappViewModel @Inject constructor(
    @ApplicationContext private val context: Context,
    private val sdkProvider: MidnightSdkProvider,
) : ViewModel() {

    fun deployAndCall() = viewModelScope.launch {
        val sdk = sdkProvider.awaitSdk()   // waits for the panel to bootstrap it

        // 1. Install your contract's proving keys — required before the first
        //    deploy/call (see §6.1), or proving fails.
        ProvingKeyManager(context).installCircuitKeysFromAssets()

        // 2. Build a write-capable handle. create() takes the SDK's config;
        //    contractJs is an InputStream from assets; deploy/call need the
        //    coin public key + each circuit's verifier-key bytes.
        val verifier = context.assets
            .open("keys/myCircuit.verifier").use { it.readBytes() }
        val contract = MidnightContract.create(sdk.config) {
            name = "mycontract"
            contractJs = context.assets.open("runtime/mycontract-contract.js")
            coinPublicKey = sdk.coinPublicKey
            circuitVerifierKeys = mapOf("myCircuit" to verifier)
        }

        // 3. Deploy, then call a circuit.
        val address = contract.deploy().contractAddress
        contract.call("myCircuit")

        // 4. Read typed ledger state (lossless — no cell-hex parsing).
        val readOnly = MidnightContract.create(sdk.config) {
            contractJs = context.assets.open("runtime/mycontract-contract.js")
            this.address = address
        }
        val count = readOnly.ledger().getUint64("count")
    }
}

6.1 Install your contract's proving keys (required)

ZK proofs run on the device, so every circuit your contract calls needs its proving keys + a BLS parameter set present before the first deploy/call — otherwise the call fails at the proving step. Ship them in assets/ — sync them in with the io.github.kuiralabs.contract Gradle plugin (0.1.0-alpha03), the recommended path, which stages the contract runtime and circuit keys into assets/ for you. Without the plugin, use the hand-rolled Copy task shown in the deploy-and-call recipe. Then install once at runtime — it's idempotent, so call it before each action:

// Discovers the circuit keys bundled in assets and stages them
// where the on-device prover looks.
ProvingKeyManager(context).installCircuitKeysFromAssets()

The shared wallet proving keys ship the full BLS parameter set (bls_midnight_2p52p15), so a small contract circuit (e.g. a counter that needs 2p5) finds the size it requires from the wallet keys — you do not bundle BLS per contract. Just make sure the wallet keys are provisioned: they are, during SDK bootstrap, and you can ship them in the APK to skip the first-run download with kuiraContract { bundleWalletKeys = true } (see On-device proving). The Kuira Starter's CounterContract shows the full pattern end-to-end.

For a larger, multi-step contract (commit / reveal, witness packing, indexer-state polling, retry, force-resync), the BBoard reference app shows the deploy → call → read flow end-to-end.


7. Going headless (no panel)

If you don't want the Compose wallet panel, swap the dep:

implementation("io.github.kuiralabs:midnight-sdk:0.1.0-alpha03")  // no dapp-ui

You still own bootstrap + sigil forging — MidnightSdkProvider.ensureSdk(...) throws SigilRequiredException until a sigil exists. Forge it through PasskeyManager directly, or use the SigilSession helper.

midnight-sdk does not pull Compose — the headless entry stays small.


Common pitfalls

Symptom Fix
Dagger: "PasskeyConfig cannot be provided without an @Provides-annotated method" Step 3 — declare your own PasskeyConfig module.
Runtime: "CLEARTEXT communication to 10.0.2.2 not permitted by network security policy" Step 5 — add the debug manifest.
Biometric prompt fails / PRF returns null Step 4 — assetlinks.json missing, wrong package_name, or wrong cert SHA-256.
Contract call fails at the proving step / "circuit keys not found" / "BLS params" §6.1 — install your contract's proving keys before the first deploy/call (and bundle the right BLS set for small circuits).
App balance stays at 0 after an airdrop The wallet's background subscription is live; check adb logcat for indexer connectivity. On localnet, state is ephemeral — restarting the localnet wipes funds.
IllegalArgumentException: Could not find io.github.kuiralabs:… Check the alpha version is current; the SDK is 0.1.0-alpha03 at the time of writing.

Known limitations (alpha)

The alpha ships with one known dependency you should be aware of. It isn't a blocker for building, but it shapes what "alpha" means in practice and will evolve as Midnight matures.

Proving infrastructure downloads from a Midnight dev URL

On first launch the SDK fetches Midnight's protocol-level proving assets — the universal BLS parameters and the wallet's shielded-spend / Dust circuit keys — from https://midnight-s3-fileshare-dev-eu-west-1.s3.eu-west-1.amazonaws.com. This is the same bucket Midnight's own tooling uses; it's Midnight's bucket, not the SDK maintainer's, and the URL is labeled -dev-. There is no production SLA on it.

What this means in practice:

  • If Midnight retires, renames, or restricts that bucket, every alpha dApp briefly can't generate proofs until the SDK is updated.
  • The right party to publish a production URL is Midnight (the asset owner), not the SDK maintainer. So the alpha documents this dependency rather than self-hosting.

Your own .compact contract's proving keys are NOT affected by this. Those download from a URL you supply when you call ProvingKeyManager.downloadCircuitKeys(baseUrl = …, …) — e.g. BBoard hosts BBoard's keys. You always own your contract's keys. This limitation is only about the protocol-wide keys that every Midnight dApp shares.

How it will evolve. When Midnight publishes a production URL — or, if that's not in their immediate roadmap, when the SDK mirrors these files under a kuiralabs-controlled URL with a version pin (ProvingKeyManager.CURRENT_VERSION tracks the protocol version, currently 9) — the SDK swaps the constant and re-publishes. The expected migration cost for a consumer is a single SDK version bump; no app-side code change.


See also

  • Kuira Starter — clone-and-run minimal dApp (also a GitHub template).
  • BBoard — on-chain bulletin board; the deploy → call → read flow end-to-end.
  • Home — install instructions and the full module list.
  • Security — threat model, vulnerability reporting, signature verification.
  • Maven Central — io.github.kuiralabs — every published artifact (binary AAR, sources jar, javadoc jar, POM, PGP signature).