Skip to main content

Troubleshooting Guide

Platform-specific issues, error solutions, and workarounds for the Blockchain Kit SDK.

iOS Issues

SSL Certificate Errors

Error: NSURLErrorDomain Code=-1202 (certificate validation failed on iOS Simulator)

Solution:

  1. Set debug.ssl.bypass=true in local.properties
  2. Uses handleChallenge in Ktor Darwin engine (see iosMain/.../http/PlatformHttpClientFactory.kt)

Swift Export Package Visibility

Error: Swift compiler error 'init()' is unavailable when instantiating Kotlin classes from Swift

Problem: Swift Export's flattenPackage was configured to only export com.cramium.blockchain.kit, but classes like PlatformContext and BlockchainSdkService were in subpackages (internal and blockchain respectively) that weren't exported to Swift.

Solution (Phase 5.3):

  1. Moved PlatformContext from internal package to platform package (still under com.cramium.blockchain.kit)
  2. Moved BlockchainSdkService from blockchain subpackage to main com.cramium.blockchain.kit package
  3. Modified KSP generator to have submodule iOS wrappers take parent wrapper and delegate to parentModule.wrappedModule.propertyName
  4. Updated Swift plugin to pass parent module to submodule constructor

Key insight: Classes must be directly in the package specified by flattenPackage, not in subpackages

Kotlin/Native Test Linker - Undefined Symbols (Firebase)

Error: ld: symbol(s) not found for architecture arm64 with nanopb/GoogleAppMeasurement symbols

Problem: The Kotlin/Native CocoaPods plugin doesn't automatically resolve transitive framework dependencies for test binaries. Firebase's GoogleAppMeasurement depends on nanopb, GoogleUtilities, etc.

Solution: Add explicit linkerOpts for all transitive frameworks in the iOS target configuration:

listOf(iosArm64(), iosX64(), iosSimulatorArm64()).forEach { target ->
target.binaries.all {
linkerOpts(
"-framework", "nanopb",
"-framework", "GoogleUtilities",
"-framework", "GoogleAppMeasurement",
"-framework", "FirebaseInstallations",
"-framework", "FirebaseCoreInternal",
// ... other transitive frameworks
)
}
}

CommonCrypto PBKDF2 Type Mismatch

Error: Type mismatch: inferred type is CPointer<ByteVarOf<Byte>> but CValuesRef<UByteVarOf<UByte>>? was expected

Problem: CCKeyDerivationPBKDF expects UByte pointers, but ByteArray.usePinned provides signed Byte pointers.

Solution: Convert to UByteArray before pinning:

val saltUBytes = salt.toUByteArray()
val derivedKeyUBytes = UByteArray(keyLengthBytes)
saltUBytes.usePinned { saltPinned ->
derivedKeyUBytes.usePinned { keyPinned ->
CCKeyDerivationPBKDF(
password = password,
salt = saltPinned.addressOf(0),
derivedKey = keyPinned.addressOf(0),
// ...
)
}
}
return derivedKeyUBytes.toByteArray()

iOS UISceneDelegate Debug Mode Crash (Flutter 3.38+)

Error: EXC_BAD_ACCESS (SIGSEGV) crash in -[VSyncClient initWithTaskRunner:callback:] when launching the app manually (not from Xcode/debugger)

Symptoms:

  • App works on first launch via flutter run or Xcode
  • App crashes immediately on second launch from device home screen
  • Crash occurs in FlutterViewController viewDidLoadcreateTouchRateCorrectionVSyncClientIfNeeded
  • Only happens with debug builds, release builds work fine

Problem: Flutter 3.38+ requires UISceneDelegate migration for iOS 26+. In debug mode, the Flutter engine's task runner initialization depends on the debug service connection. When launching without debugger attached, the task runner is null, causing a crash.

Solution: Use release builds for testing on device without debugger:

# Build release version
fvm flutter build ios --release --no-codesign

# Or run in release mode
fvm flutter run --release

UISceneDelegate Migration Requirements (Flutter 3.38.0+):

  1. Info.plist - Add UIApplicationSceneManifest:
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
  1. AppDelegate.swift - Use FlutterImplicitEngineDelegate:
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
  1. SceneDelegate.swift (optional, for custom scene logic):
import Flutter
import UIKit

class SceneDelegate: FlutterSceneDelegate {
// Custom scene lifecycle methods here
}
  1. Remove UIMainStoryboardFile from root Info.plist (only use UISceneStoryboardFile in scene config)

References:

Flutter Integration Tests Setup

Requirements for local iOS development:

  1. Framework module name: Must be BlockchainKitSdk (PascalCase) - configured in build.gradle.kts
  2. Podspec: blockchain-kit-sdk/BlockchainKitSdk.podspec with matching name
  3. Static linkage: Flutter example Podfile must use use_frameworks! :linkage => :static
  4. Local pod path: Podfile declares pod 'BlockchainKitSdk', :path => '../../../blockchain-kit-sdk'
  5. Submodule registration: Swift plugin must register BlockchainKitSdkIOS, MpcServiceIOS, and BlockchainSdkServiceIOS
  6. Resources: spec.resources = ['src/commonMain/resources/*.json'] in podspec for bundled JSON files

Key files:

  • blockchain-kit-sdk/BlockchainKitSdk.podspec - CocoaPods spec for iOS
  • blockchain_kit_sdk_flutter_plugin/ios/Classes/BlockchainKitSdkFlutterPlugin.swift - Swift plugin bridge
  • blockchain_kit_sdk_flutter_plugin/example/ios/Podfile - Example app pod configuration

Build sequence for iOS tests:

./gradlew :blockchain-kit-sdk:generateDummyFramework  # Generate initial framework
cd blockchain_kit_sdk_flutter_plugin/example/ios && pod install # Install pods
fvm flutter test integration_test/ -d "iPhone 15 Pro" # Run tests (Xcode builds real framework)

Android Issues

Passkey Authentication Fails Immediately After Registration

Error: androidx.credentials.exceptions.NoCredentialException: No credentials available when attempting to authenticate with PRF immediately after registering a passkey

Problem: Three critical bugs in the Android passkey implementation:

  1. Wrong Thread (Dispatchers.IO instead of Dispatchers.Main)

    • Android Credential Manager requires operations to run on the main/UI thread
    • Using Dispatchers.IO caused unpredictable behavior and credential operations to fail
    • Affected: register(), authenticate(), and authenticateWithPrf() methods
  2. Wrong Encoding (Base64 instead of Base64URL)

    • WebAuthn PRF extension requires Base64URL encoding (RFC 4648 Section 5)
    • Used standard Base64 (android.util.Base64) instead of Base64URL
    • Caused PRF extension to be ignored by authenticator
    • Affected: authenticateWithPrf() salt parameter encoding
  3. Complex Hybrid Logic Masking Core Issues

    • Previous implementation had 200+ lines of hybrid authentication (fast path + retry + fallback)
    • Credential ID storage and retrieval mechanisms
    • Multiple authentication attempts with exponential backoff
    • Complex error handling that masked the real bugs

Solution: Complete rewrite based on working POC pattern from docs/simple_passkeys.md:

// Fix #1: Use Dispatchers.Main (not IO)
internal actual suspend fun authenticateWithPrf(salt: ByteArray): PasskeyResult<PrfAuthResult> {
return withContext(Dispatchers.Main) { // ✅ CORRECT THREAD
try {
val credentialManager = CredentialManager.create(platformContext.context)

// Fix #2: Use Base64URL encoding (not standard Base64)
val saltB64 = Base64Url.encode(salt) // ✅ CORRECT ENCODING

val requestWithPrf = GetPasskeyRequestWithPrf(
extensions = PrfExtension(
prf = PrfEval(eval = PrfEvalInput(first = saltB64))
)
)

// Fix #3: Simple, clean flow (no retry/hybrid logic)
val response = credentialManager.getCredential(activity, getRequest)
// ... extract PRF output and verify
} catch (e: NoCredentialException) {
PasskeyResult.Error(PasskeyException("No passkey found", e))
}
}
}

Files Modified:

  • PasskeyManager.android.kt - All three methods rewritten (register, authenticate, authenticateWithPrf)
  • PasskeyManager.kt (commonMain) - Removed obsolete credential storage expect declarations
  • PasskeyManager.ios.kt - Removed obsolete credential storage actual implementations
  • BlockchainKitSdk.kt - Removed clearCredentialId() call from logout() method

Result:

  • Authentication now works immediately after registration (< 1 second)
  • Uses correct thread (Dispatchers.Main)
  • Uses correct encoding (Base64URL unpadded)
  • Simple, maintainable code following proven POC pattern
  • Works with all passkey providers (Google Password Manager, platform authenticator, third-party)

Key Insight: The "discoverable mode propagation delay" theory was wrong. The POC uses discoverable mode and works immediately because it runs on the correct thread with correct encoding.

DEX Backtick Test Names

Error: Space characters in SimpleName 'encrypt generates random DEK and wraps with KEK' are not allowed prior to DEX version 040

Problem: Kotlin backtick test function names with spaces (e.g., fun `my test name`()) are not compatible with Android DEX format prior to version 40. This affects instrumented tests.

Solution: Rename backtick test functions to camelCase format:

// Before (fails on Android instrumented tests)
@Test
fun `encrypt generates random DEK and wraps with KEK`() { ... }

// After (works)
@Test
fun encryptGeneratesRandomDekAndWrapsWithKek() { ... }

Note: This only affects androidInstrumentedTest/ tests. commonTest/ tests running via Robolectric don't have this issue because they don't go through DEX compilation.

Libsodium Tests in Robolectric (NullPointerException)

Error: NullPointerException when running BackupEncryptionTest, SymmetricEncryptionTest, WalletBackupServiceTest in Android unit tests (Robolectric).

Problem: Libsodium native libraries (libsodium.so) cannot be loaded in Robolectric's JVM environment. The library bindings return null pointers.

Solution: Move encryption tests from commonTest/ to platform-specific directories:

  • androidInstrumentedTest/ - Runs on real device/emulator where native libs load correctly
  • iosTest/ - Runs on iOS where native libs work

Files moved:

  • BackupEncryptionTest.kt (14 tests)
  • SymmetricEncryptionTest.kt (9 tests)
  • WalletBackupServiceTest.kt (9 tests)
  • FakeKeyValueStorage.kt (copied to androidInstrumentedTest/testutil/)

Commands:

# Run Android instrumented tests (requires device/emulator)
./gradlew :blockchain-kit-sdk:connectedDebugAndroidTest

# Run iOS tests
./gradlew :blockchain-kit-sdk:iosSimulatorArm64Test

WalletCore Tests - Native Library Not Available in Robolectric

Error: java.lang.UnsatisfiedLinkError when running WalletCore-dependent tests in Android unit tests (Robolectric)

Problem: WalletCore native libraries (libTrustWalletCore.so) cannot be loaded in Robolectric's JVM environment. Tests that use AnyAddress, PublicKey, or other WalletCore classes fail with UnsatisfiedLinkError.

Solution: Move WalletCore-dependent tests to iosTest/ directory where native libraries are available:

  • EvmDeriveAddressTest.kt - Tests deriveAddress() with WalletCore test vectors

Commands:

# Run iOS tests (includes WalletCore tests)
./gradlew :blockchain-kit-sdk:iosSimulatorArm64Test

Test vectors: Use known test vectors from TrustWallet WalletCore repository:

  • tests/interface/TWPublicKeyTests.cpp - Public key test vectors
  • tests/chains/Ethereum/AddressTests.cpp - Ethereum address test vectors

KSP / Code Generation Issues

KSP Not Generating Classes

Checklist:

  1. Class has @Serializable annotation
  2. Class is referenced in a @FlutterMethod parameter or return type
  3. Re-run both kspCommonMainKotlinMetadata and copyGeneratedDartFiles

KSP Android/iOS Platform Wrappers Missing (Stale Cache)

Error: Unresolved reference 'MpcServiceAndroid' (or similar *Android/*IOS wrapper class) when building the Flutter plugin.

Problem: kspCommonMainKotlinMetadata only generates Dart files (into build/generated/ksp/metadata/). The Android Kotlin wrappers (*Android.kt) are generated by the Android-specific KSP task (kspDebugKotlinAndroid). When the KSP cache is stale, the task reports UP-TO-DATE but missing files aren't regenerated.

Solution: Force re-run the Android KSP task:

./gradlew :blockchain-kit-sdk:kspDebugKotlinAndroid --rerun-tasks

Generated file locations:

  • Dart models: build/generated/ksp/metadata/commonMain/resources/flutterkmp/
  • Android wrappers: build/generated/ksp/android/androidDebug/kotlin/com/cramium/blockchain/kit/
  • iOS wrappers: Generated via kspIosArm64KotlinMetadata (or similar iOS target task)

Full rebuild sequence (when platform wrappers are missing):

./scripts/publish-local.sh

Note: The publish-local.sh script now automatically forces KSP re-generation (--rerun-tasks) before publishing to prevent stale cache issues. Do NOT use ./gradlew :blockchain-kit-sdk:clean before publish — it wipes UniFFI bindings which won't regenerate properly.

Sealed Class Property Ordering for Dart Super Parameters

Error: Too few positional arguments or incorrect parameter order in generated Dart code for sealed class subclasses

Problem: Dart 3.x requires super parameters in child classes to match the exact order of parent class constructor parameters. KSP code generator was emitting override properties in declaration order rather than parent constructor order.

Solution: Added reorderOverridePropertiesToMatchParent() function in flutter-to-kmp-ksp/Utils.kt that reorders override properties to match parent constructor order before Dart code generation.

Example:

// Parent class (Fee.kt)
sealed class Fee(
val maxFeeWei: BigInteger,
val l1FeeWei: BigInteger?,
val gasLimit: BigInteger,
)

// Child class - properties MUST be in same order for Dart super()
data class EvmLegacyFee(
override val maxFeeWei: BigInteger,
override val l1FeeWei: BigInteger?,
override val gasLimit: BigInteger,
val gasPrice: BigInteger,
) : Fee(maxFeeWei, l1FeeWei, gasLimit)

Cross-Platform Issues

Named Arguments in Function Types

Error: Named arguments are prohibited for function types

Solution: Use positional arguments in lambda callbacks:

// Wrong
completionHandler(value1, credential = null)

// Correct
completionHandler(value1, null)

NSURLSessionAuthChallengeDisposition Type Mismatch

Error: Argument type mismatch: actual type is 'Long', but 'Int' was expected (varies by target)

Problem: NSURLSessionAuthChallengeDisposition is a typealias for NSInteger which maps to different sizes on different iOS compilation targets (32-bit vs 64-bit).

Solution: Use kotlinx.cinterop.convert() to convert the value:

import kotlinx.cinterop.convert

// Wrong - fails on some targets
completionHandler(NSURLSessionAuthChallengeUseCredential, credential)

// Correct - works on all targets
completionHandler(NSURLSessionAuthChallengeUseCredential.convert(), credential)

Cross-Platform Symmetric Encryption (Backup System)

Problem: Need authenticated encryption for wallet backups that works in both Android unit tests (Robolectric) and iOS.

Attempted solution: Use libsodium (com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings) in commonMain for both platforms.

Issue: Libsodium native library not available in Robolectric environment → NullPointerException when running Android unit tests.

Solution: Use expect/actual pattern with platform-specific crypto:

  • Android: libsodium ChaCha20-Poly1305 (requires instrumented tests, not Robolectric)
  • iOS: libsodium ChaCha20-Poly1305 (works in iOS tests)

Note: Android encryption tests must run as instrumented tests (not Robolectric) because libsodium native libraries don't load in the JVM environment.

Kable BLE Library - API Migration Notes

  • Artifact name: Changed from com.juul.kable:core to com.juul.kable:kable-core since version 0.33.0
  • Peripheral builder: Use Peripheral(advertisement) { ... } (top-level function), NOT scope.peripheral(advertisement) (deprecated)
  • MTU negotiation: requestMtu() is Android-only. Use expect/actual pattern — iOS negotiates MTU automatically
  • Channel.isEmpty: Requires @OptIn(ExperimentalCoroutinesApi::class)
  • KMP compatibility: Replace synchronized with Mutex+withLock, @Volatile with kotlin.concurrent.Volatile, String.format with manual hex conversion

KMP Common Test Issues

Kotlin/Native - MutableList.replaceAll Requires OptIn

Error: This declaration needs opt-in. Its usage must be marked with '@kotlin.experimental.ExperimentalNativeApi'

Problem: MutableList.replaceAll extension function is experimental on Kotlin/Native and requires explicit opt-in annotation.

Solution: Add @OptIn(kotlin.experimental.ExperimentalNativeApi::class) to the class or function:

@OptIn(kotlin.experimental.ExperimentalNativeApi::class)
class FakeCredentialRegistry : CredentialRegistry {
override suspend fun setDefault(credentialId: String) {
credentials.replaceAll { cred -> // ✅ Now works on Native
// ...
}
}
}

System.currentTimeMillis() Not Available in Common Code

Error: Unresolved reference: System in commonTest or commonMain code

Problem: System.currentTimeMillis() is a JVM-only API. KMP common code cannot use it directly.

Solution: Use the KMP-compatible currentTimeMillis() function from the util package:

import com.cramium.blockchain.kit.util.currentTimeMillis

// Wrong (JVM only)
val now = System.currentTimeMillis()

// Correct (KMP multiplatform)
val now = currentTimeMillis()

Note: The currentTimeMillis() function is defined as expect fun in commonMain and has platform-specific actual implementations.

Loading Test Resources in KMP (Cross-Platform)

Problem: Loading test fixture files (JSON, etc.) from commonTest/resources requires platform-specific code (ClassLoader on JVM, NSBundle on iOS).

Solution: Use the goncalossilva/kotlinx-resources plugin which provides a cross-platform API:

  1. Add plugin and library to libs.versions.toml:
[versions]
kotlinx-resources = "0.14.4"

[libraries]
kotlinx-resources-test = { module = "com.goncalossilva:resources", version.ref = "kotlinx-resources" }

[plugins]
kotlinxResources = { id = "com.goncalossilva.resources", version.ref = "kotlinx-resources" }
  1. Apply plugin and add dependency in build.gradle.kts:
plugins {
alias(libs.plugins.kotlinxResources)
}

kotlin {
sourceSets {
commonTest.dependencies {
implementation(libs.kotlinx.resources.test)
}
}
}
  1. Place test fixtures in: src/commonTest/resources/

  2. Load in tests:

import com.goncalossilva.resources.Resource

fun loadTestResource(resourceName: String): String {
return Resource(resourceName).readText()
}

// Usage:
val json = loadTestResource("wallet_backup.json")

Note: The plugin automatically handles resource bundling for all platforms (Android, iOS, JVM). No need for expect/actual or manual resources.srcDirs configuration.


Flutter Testing Tips

Minimal Test Output (Reducing Noise)

Flutter test output can be verbose. Here are reporter options from most minimal to more informative:

1. Only print failures (recommended for most use cases):

fvm flutter test --reporter=failures-only

2. Compact output (very quiet, relies on exit code):

fvm flutter test --reporter=compact

3. Machine-readable JSON (for CI parsing):

fvm flutter test --reporter=json

Additional noise reduction options:

# Reduce stack trace noise
fvm flutter test --no-chain-stack-traces

# Single file with minimal output
fvm flutter test test/my_test.dart --reporter=failures-only

# Combine options for CI
fvm flutter test --reporter=failures-only --no-chain-stack-traces

Available reporters: compact, expanded (default), failures-only, github, json


Build System Issues

Missing development.config File

Error: GradleException: BuildType was not configured or GradleException: TargetPlatform was not configured

Problem: The build system requires a development.config file in the project root to determine which platforms to build and which build mode to use.

Solution: Create development.config in the project root:

# Target platform: device|mac|arm64Simulator|intelX64Simulator
iTestLocallyOnAndroidAnd=arm64Simulator

# Build mode: debug|release
buildMode=debug

Platform options:

  • device or mac → Android + iosArm64 (for physical iOS device)
  • arm64Simulator → Android + iosSimulatorArm64 (Apple Silicon Mac)
  • intelX64Simulator → Android + iosX64 (Intel Mac)

Invalid Platform/Build Mode Configuration

Error: GradleException: Invalid buildMode value in development.config: 'xxx'. Expected 'debug' or 'release'.

Problem: Typo or invalid value in development.config.

Solution: Use exact lowercase values:

buildMode=debug    # NOT "Debug" or "DEBUG"
iTestLocallyOnAndroidAnd=arm64Simulator # NOT "arm64" or "simulator"

KSP Not Running for Specific Platform

Error: Generated Kotlin wrappers missing for iOS or Android.

Problem: When development.config sets a single-platform target (e.g., Android-only), the KSP task for other platforms won't run.

Solution: Ensure your configuration includes both Android and at least one iOS target:

iTestLocallyOnAndroidAnd=arm64Simulator  # Builds Android + iOS simulator

For CI builds that need all platforms, use environment variable:

TARGET_PLATFORM=All ./gradlew build

CocoaPods Framework Build Mode Mismatch

Error: Xcode build fails with "framework not found" when switching between Debug/Release.

Problem: The convention plugin disables the opposite build mode's linkPodFramework task to optimize build times. If Xcode expects Release but you're in Debug mode, the framework won't exist.

Solution: Ensure development.config matches your Xcode scheme:

# For Debug builds (default for development)
buildMode=debug

# For Release builds (CI/production)
buildMode=release

Or override via environment variable:

BUILD_MODE=Release ./gradlew :blockchain-kit-sdk:linkPodReleaseFrameworkIosArm64

Build Mode Effects Summary

SettingAndroidiOSKotlin Compiler
buildMode=debugminifyEnabled=falseNativeBuildType.DEBUG, -Xadd-light-debug=enableFull assertions
buildMode=releaseUses extension configNativeBuildType.RELEASE-Xno-param-assertions, -Xno-call-assertions, -Xno-receiver-assertions

Native MPC SDK Build Dependency

Error: UnsatisfiedLinkError when running unit tests that use MPC functions.

Problem: Unit tests require the native MPC SDK to be built first to provide JNI bindings.

Solution: The build is configured to depend on native:mpc-sdk:wrapper:build, but if running tests in isolation:

# Build native SDK first
./gradlew :native:mpc-sdk:wrapper:build

# Then run tests
./gradlew :blockchain-kit-sdk:allTests

The JNI library path is automatically configured based on build mode:

  • Debug: native/mpc-sdk/mpc-sdk-rs/target/aarch64-apple-darwin/debug
  • Release: native/mpc-sdk/mpc-sdk-rs/target/aarch64-apple-darwin/release