Set up Sigil identity¶
Outcome: your app forges a sigil — a PRF-derived DID + wallet seed tied to a single passkey — and can later sign the user in with one biometric prompt that yields both identity and funding authority.
What "sigil" means here¶
A sigil is the unit of identity in Kuira. Concretely it's:
- A DID (Decentralized Identifier) derived from a PRF over a
passkey assertion —
did:key:z6Mk…. - A wallet seed derived from the same passkey via a different PRF salt — produces a BIP-39-compatible 24-word entropy you never have to surface to the user.
- A Block Store backup of both, PRF-encrypted client-side before Google's Block Store touches the blob.
One passkey, one biometric, one tap — and the user has identity AND the funding key. No seed phrases shown at onboarding; the optional recovery-phrase export (BIP-39) is its own flow.
Prerequisites¶
- The previous recipe (Add Kuira to an Android project)
completed: dependency,
PasskeyConfig,assetlinks.json. - A Compose-based UI host. The SDK ships
SigilStatusPanelas a ready-made Compose component you can drop in, or you can render a custom UI on top ofSigilPanelViewModel.
Step 1 — Add the SDK provider to your Application class¶
The SDK needs to be reachable across the app. Wire it as an injected singleton via Hilt:
package com.example.myapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApp : Application()
And declare it in your manifest:
<application
android:name=".MyApp"
android:allowBackup="false"
android:label="Your App">
<!-- … -->
</application>
@HiltAndroidApp lights up the dependency graph the SDK's modules
plug into. Nothing further required — the SDK's IdentityModule,
AuthModule, WalletRuntimeModule, SdkModule are auto-installed
once Hilt is on.
Verify: ./gradlew :app:assembleDebug succeeds and Hilt's
MyApp_HiltComponents is generated under
app/build/generated/hilt/.
Step 2 — Drop the SigilStatusPanel Composable into your UI¶
The panel composable takes a Hilt-provided SigilPanelViewModel
(obtained via hiltViewModel() from androidx.hilt.navigation.compose)
plus optional Modifier, SigilPanelColors, and an onStatusChange
callback. The hosting Activity must subclass FragmentActivity so the
panel's internal biometric prompts can run — AppCompatActivity
satisfies this; ComponentActivity alone does not.
package com.example.myapp
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import com.example.myapp.ui.MainScreen
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { // (1)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme { // (2)
Surface(modifier = Modifier.fillMaxSize()) {
MainScreen()
}
}
}
}
}
@AndroidEntryPointis required forhiltViewModel()to resolve.- The panel uses Material 3 primitives, so a
MaterialThemeancestor must be present.
package com.example.myapp.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.midnight.kuira.dapp.sigil.SigilStatusPanel
@Composable
fun MainScreen() {
Column(modifier = Modifier.padding(16.dp)) {
Text("My Midnight dApp")
// Zero-arg call is the canonical drop-in: the SDK defaults
// `viewModel = hiltViewModel()`, `colors = SigilPanelColors.Default`,
// `modifier = Modifier`, and `onStatusChange = { }`.
SigilStatusPanel()
}
}
If you want to react to sigil state transitions, pass an
onStatusChange callback:
SigilStatusPanel(
onStatusChange = { status ->
// status is com.midnight.kuira.dapp.sigil.SigilStatus,
// one of None / BackupAvailable / Creating / Forged / Error.
},
)
If you want to restyle the panel, pass a custom SigilPanelColors
or a non-default Modifier. Both have sensible defaults; override
only when you need to.
Behaviourally, the panel renders one of four states:
None— no sigil on this device; surfaces a Forge button.BackupAvailable— Block Store knows about a previous sigil; surfaces a Restore button.Forged— sigil exists and is unlocked; renders DID, balance, copy actions.Error— recoverable error; shows the message + a retry path.
Verify: launch your app on an Android 13+ device with a screen lock. You should see the panel render one of the four states based on Block Store presence.
Step 3 — Forge a sigil (first-run flow)¶
In the panel UI: tap Forge sigil. Under the hood:
SigilPanelViewModel.forgeSigil(activity)invokes the passkeycreateceremony with therpIdyou declared.- The user authenticates with their biometric / screen-lock.
- The SDK performs two PRF assertions in the same ceremony
(
SIGIL_SALT+SEED_SALT) — yielding the DID-derivation entropy and the wallet-seed entropy from a single biometric. - Both are persisted to
EncryptedSharedPreferencesand a PRF-encrypted backup blob lands in Google Block Store.
After the forge completes, the panel transitions to Forged and
the wallet is funded-ready.
Verify: on the same device, kill and restart the app. The panel
should now render Forged without a re-prompt (the sigil state
is loaded from local prefs). Tap any value-bearing action — the SDK
will prompt biometric only when the session-cache expires.
Step 4 — Sign in on a fresh device (restore flow)¶
When the user installs the app on a second device with the same Google account:
- The app launches; Block Store reports a backup is available.
- Panel renders
BackupAvailablewith a Restore button. - Tap → biometric prompt → SDK runs the dual-PRF assertion against the saved passkey, derives the same DID + wallet seed, and seeds local state.
Verify: confirm the restored DID matches the original. Funds — if any were on-chain at backup time — should resolve via balance sync after a few seconds.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
Panel stuck on None even though Block Store should have a backup |
Different Google account on this device, or Block Store backup hasn't propagated yet. | Verify GPM signed-in account; wait 1–2 min after first forge for backup to land in Block Store. |
Forge fails with RP_ID_MISMATCH |
assetlinks.json not reachable, or PasskeyConfig.rpId ≠ the domain that hosts it. |
See Recipe 1 Step 3. |
| Two devices on the same Google account resolve to the same sigil | This is by design — passkeys are per-account in GPM. For testing distinct identities on emulators, use distinct Google accounts. | A debug-only per-device identity seam is not supported today. |
Restore prompt fails with BackupException |
Backup blob missing or corrupted, or the user tapped restore when they should have forged. | The Error body offers retry. If still failing, tap "Sign out → Forge" (data-layer guard prevents accidental overwrite). |
What's next¶
- Deploy and call a Compact contract — now that your app has a sigil-bound wallet, use it to deploy a contract and call a circuit.
- Security § Verifying releases — verify the SDK artifacts on your CI before shipping a release.