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:
- Set
debug.ssl.bypass=trueinlocal.properties - Uses
handleChallengein Ktor Darwin engine (seeiosMain/.../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):
- Moved
PlatformContextfrominternalpackage toplatformpackage (still undercom.cramium.blockchain.kit) - Moved
BlockchainSdkServicefromblockchainsubpackage to maincom.cramium.blockchain.kitpackage - Modified KSP generator to have submodule iOS wrappers take parent wrapper and delegate to
parentModule.wrappedModule.propertyName - 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 runor Xcode - App crashes immediately on second launch from device home screen
- Crash occurs in
FlutterViewController viewDidLoad→createTouchRateCorrectionVSyncClientIfNeeded - 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+):
- 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>
- 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)
}
}
- SceneDelegate.swift (optional, for custom scene logic):
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
// Custom scene lifecycle methods here
}
- Remove
UIMainStoryboardFilefrom root Info.plist (only useUISceneStoryboardFilein scene config)
References:
- Flutter UISceneDelegate Migration Guide
- GitHub Issue #179422 - Xcode 26/iOS 26 beta crashes
Flutter Integration Tests Setup
Requirements for local iOS development:
- Framework module name: Must be
BlockchainKitSdk(PascalCase) - configured inbuild.gradle.kts - Podspec:
blockchain-kit-sdk/BlockchainKitSdk.podspecwith matching name - Static linkage: Flutter example Podfile must use
use_frameworks! :linkage => :static - Local pod path: Podfile declares
pod 'BlockchainKitSdk', :path => '../../../blockchain-kit-sdk' - Submodule registration: Swift plugin must register
BlockchainKitSdkIOS,MpcServiceIOS, andBlockchainSdkServiceIOS - Resources:
spec.resources = ['src/commonMain/resources/*.json']in podspec for bundled JSON files
Key files:
blockchain-kit-sdk/BlockchainKitSdk.podspec- CocoaPods spec for iOSblockchain_kit_sdk_flutter_plugin/ios/Classes/BlockchainKitSdkFlutterPlugin.swift- Swift plugin bridgeblockchain_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:
-
Wrong Thread (Dispatchers.IO instead of Dispatchers.Main)
- Android Credential Manager requires operations to run on the main/UI thread
- Using
Dispatchers.IOcaused unpredictable behavior and credential operations to fail - Affected:
register(),authenticate(), andauthenticateWithPrf()methods
-
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
-
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 declarationsPasskeyManager.ios.kt- Removed obsolete credential storage actual implementationsBlockchainKitSdk.kt- RemovedclearCredentialId()call fromlogout()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 correctlyiosTest/- 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- TestsderiveAddress()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 vectorstests/chains/Ethereum/AddressTests.cpp- Ethereum address test vectors
KSP / Code Generation Issues
KSP Not Generating Classes
Checklist:
- Class has
@Serializableannotation - Class is referenced in a
@FlutterMethodparameter or return type - Re-run both
kspCommonMainKotlinMetadataandcopyGeneratedDartFiles
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:coretocom.juul.kable:kable-coresince version 0.33.0 - Peripheral builder: Use
Peripheral(advertisement) { ... }(top-level function), NOTscope.peripheral(advertisement)(deprecated) - MTU negotiation:
requestMtu()is Android-only. Useexpect/actualpattern — iOS negotiates MTU automatically Channel.isEmpty: Requires@OptIn(ExperimentalCoroutinesApi::class)- KMP compatibility: Replace
synchronizedwithMutex+withLock,@Volatilewithkotlin.concurrent.Volatile,String.formatwith 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:
- 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" }
- Apply plugin and add dependency in
build.gradle.kts:
plugins {
alias(libs.plugins.kotlinxResources)
}
kotlin {
sourceSets {
commonTest.dependencies {
implementation(libs.kotlinx.resources.test)
}
}
}
-
Place test fixtures in:
src/commonTest/resources/ -
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:
deviceormac→ 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
| Setting | Android | iOS | Kotlin Compiler |
|---|---|---|---|
buildMode=debug | minifyEnabled=false | NativeBuildType.DEBUG, -Xadd-light-debug=enable | Full assertions |
buildMode=release | Uses extension config | NativeBuildType.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