diff --git a/.gitignore b/.gitignore
index cde80cf..deca7a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,12 @@ Cargo.lock
**/.pub-cache/
**/.pub/
**/pubspec.lock
+**/local.properties
+**/GeneratedPluginRegistrant.*
+**/flutter_export_environment.sh
+**/Flutter-Generated.xcconfig
+**/Generated.xcconfig
+**/Flutter/ephemeral/
# IDE
.vscode/
@@ -26,6 +32,10 @@ Thumbs.db
# Native libs built locally
libbdkffi.*
+android/libs
+bdk_demo/android/app/src/main/jniLibs
+ios/Release/
+bdk_demo/ios/ios/
test_output.txt
test output.txt
lib/bdk.dart
diff --git a/bdk_demo/.metadata b/bdk_demo/.metadata
index bad0587..faf2eb0 100644
--- a/bdk_demo/.metadata
+++ b/bdk_demo/.metadata
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
- revision: "d7b523b356d15fb81e7d340bbe52b47f93937323"
+ revision: "f5a8537f90d143abd5bb2f658fa69c388da9677b"
channel: "stable"
project_type: app
@@ -13,11 +13,14 @@ project_type: app
migration:
platforms:
- platform: root
- create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
- base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
+ create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b
+ base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b
+ - platform: android
+ create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b
+ base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b
- platform: ios
- create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
- base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
+ create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b
+ base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b
# User provided section
diff --git a/bdk_demo/android/.gitignore b/bdk_demo/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/bdk_demo/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/bdk_demo/android/app/build.gradle.kts b/bdk_demo/android/app/build.gradle.kts
new file mode 100644
index 0000000..ca0db07
--- /dev/null
+++ b/bdk_demo/android/app/build.gradle.kts
@@ -0,0 +1,44 @@
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "com.example.bdk_demo"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.example.bdk_demo"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/bdk_demo/android/app/src/debug/AndroidManifest.xml b/bdk_demo/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/bdk_demo/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/bdk_demo/android/app/src/main/AndroidManifest.xml b/bdk_demo/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..6ce2eb8
--- /dev/null
+++ b/bdk_demo/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bdk_demo/android/app/src/main/kotlin/com/example/bdk_demo/MainActivity.kt b/bdk_demo/android/app/src/main/kotlin/com/example/bdk_demo/MainActivity.kt
new file mode 100644
index 0000000..7f7cb8b
--- /dev/null
+++ b/bdk_demo/android/app/src/main/kotlin/com/example/bdk_demo/MainActivity.kt
@@ -0,0 +1,137 @@
+package com.example.bdk_demo
+
+import android.os.Build
+import android.util.Log
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodChannel
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.util.zip.ZipFile
+
+private const val CHANNEL = "bdk_demo/native_lib_dir"
+
+class MainActivity : FlutterActivity() {
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+
+ MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
+ .setMethodCallHandler { call, result ->
+ when (call.method) {
+ "getNativeLibDir" -> {
+ val path = findLibDirectory()
+ if (path != null) {
+ result.success(path)
+ } else {
+ result.error(
+ "LIB_NOT_FOUND",
+ "Could not locate libbdkffi.so in nativeLibraryDir",
+ null
+ )
+ }
+ }
+ else -> result.notImplemented()
+ }
+ }
+ }
+
+ private fun findLibDirectory(): String? {
+ val info = applicationContext.applicationInfo
+ val nativeDirPath = info.nativeLibraryDir ?: return null
+ val baseDir = File(nativeDirPath)
+ if (!baseDir.exists()) {
+ Log.w("BDKDemo", "nativeLibraryDir does not exist: $nativeDirPath")
+ return null
+ }
+
+ val direct = File(baseDir, "libbdkffi.so")
+ if (direct.exists()) {
+ return baseDir.absolutePath
+ }
+
+ baseDir.listFiles()?.forEach { candidateDir ->
+ if (!candidateDir.isDirectory) return@forEach
+
+ val candidate = File(candidateDir, "libbdkffi.so")
+ if (candidate.exists()) {
+ return candidateDir.absolutePath
+ }
+
+ candidateDir.listFiles()?.forEach { nestedDir ->
+ if (!nestedDir.isDirectory) return@forEach
+ val nestedCandidate = File(nestedDir, "libbdkffi.so")
+ if (nestedCandidate.exists()) {
+ return nestedDir.absolutePath
+ }
+
+ nestedDir.listFiles()?.forEach { innerDir ->
+ if (!innerDir.isDirectory) return@forEach
+ val innerCandidate = File(innerDir, "libbdkffi.so")
+ if (innerCandidate.exists()) {
+ return innerDir.absolutePath
+ }
+ }
+ }
+ }
+
+ Build.SUPPORTED_ABIS?.forEach { abi ->
+ val candidateDir = File(baseDir, abi)
+ val candidate = File(candidateDir, "libbdkffi.so")
+ if (candidate.exists()) {
+ return candidateDir.absolutePath
+ }
+ }
+
+ val extracted = extractLibraryFromApk()
+ if (extracted != null) {
+ return extracted
+ }
+
+ return null
+ }
+
+ private fun extractLibraryFromApk(): String? {
+ val info = applicationContext.applicationInfo
+ val apkPath = info.sourceDir ?: return null
+ val supportedAbis = Build.SUPPORTED_ABIS ?: return null
+ val destRoot = File(applicationContext.filesDir, "bdk_native_libs")
+
+ if (!destRoot.exists() && !destRoot.mkdirs()) {
+ Log.w("BDKDemo", "Failed to create destination dir: ${destRoot.absolutePath}")
+ return null
+ }
+
+ try {
+ ZipFile(apkPath).use { zip ->
+ for (abi in supportedAbis) {
+ val entryName = "lib/$abi/libbdkffi.so"
+ val entry = zip.getEntry(entryName) ?: continue
+
+ val abiDir = File(destRoot, abi)
+ if (!abiDir.exists() && !abiDir.mkdirs()) {
+ Log.w("BDKDemo", "Failed to create ABI dir: ${abiDir.absolutePath}")
+ continue
+ }
+
+ val outputFile = File(abiDir, "libbdkffi.so")
+ if (outputFile.exists()) {
+ return abiDir.absolutePath
+ }
+
+ zip.getInputStream(entry).use { input ->
+ FileOutputStream(outputFile).use { output ->
+ input.copyTo(output)
+ }
+ }
+ outputFile.setReadable(true, false)
+ return abiDir.absolutePath
+ }
+ }
+ } catch (e: IOException) {
+ Log.e("BDKDemo", "Failed to extract libbdkffi.so", e)
+ }
+
+ return null
+ }
+}
diff --git a/bdk_demo/android/app/src/main/res/drawable-v21/launch_background.xml b/bdk_demo/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/bdk_demo/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/bdk_demo/android/app/src/main/res/drawable/launch_background.xml b/bdk_demo/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/bdk_demo/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/bdk_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/bdk_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/bdk_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/bdk_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/bdk_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/bdk_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/bdk_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/bdk_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/bdk_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/bdk_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/bdk_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/bdk_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/bdk_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/bdk_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/bdk_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/bdk_demo/android/app/src/main/res/values-night/styles.xml b/bdk_demo/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/bdk_demo/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/bdk_demo/android/app/src/main/res/values/styles.xml b/bdk_demo/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/bdk_demo/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/bdk_demo/android/app/src/profile/AndroidManifest.xml b/bdk_demo/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/bdk_demo/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/bdk_demo/android/build.gradle.kts b/bdk_demo/android/build.gradle.kts
new file mode 100644
index 0000000..dbee657
--- /dev/null
+++ b/bdk_demo/android/build.gradle.kts
@@ -0,0 +1,24 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/bdk_demo/android/gradle.properties b/bdk_demo/android/gradle.properties
new file mode 100644
index 0000000..fbee1d8
--- /dev/null
+++ b/bdk_demo/android/gradle.properties
@@ -0,0 +1,2 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
diff --git a/bdk_demo/android/gradle/wrapper/gradle-wrapper.properties b/bdk_demo/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e4ef43f
--- /dev/null
+++ b/bdk_demo/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
diff --git a/bdk_demo/android/settings.gradle.kts b/bdk_demo/android/settings.gradle.kts
new file mode 100644
index 0000000..ca7fe06
--- /dev/null
+++ b/bdk_demo/android/settings.gradle.kts
@@ -0,0 +1,26 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "8.11.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.2.20" apply false
+}
+
+include(":app")
diff --git a/bdk_demo/lib/main.dart b/bdk_demo/lib/main.dart
index 54cf84c..bac91e6 100644
--- a/bdk_demo/lib/main.dart
+++ b/bdk_demo/lib/main.dart
@@ -1,4 +1,32 @@
+import 'dart:io' as io;
+
+import 'package:bdk_dart/bdk.dart' as bdk;
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+const _nativeLibChannel = MethodChannel('bdk_demo/native_lib_dir');
+String? _cachedNativeLibDir;
+
+Future _ensureNativeLibraryDir() async {
+ if (_cachedNativeLibDir != null) {
+ io.Directory.current = io.Directory(_cachedNativeLibDir!);
+ return;
+ }
+
+ if (io.Platform.isAndroid) {
+ final dir = await _nativeLibChannel.invokeMethod('getNativeLibDir');
+ if (dir == null || dir.isEmpty) {
+ throw StateError('Native library directory channel returned empty path');
+ }
+ _cachedNativeLibDir = dir;
+ } else {
+ _cachedNativeLibDir = io.File(io.Platform.resolvedExecutable).parent.path;
+ }
+
+ if (_cachedNativeLibDir != null) {
+ io.Directory.current = io.Directory(_cachedNativeLibDir!);
+ }
+}
void main() {
runApp(const MyApp());
@@ -29,16 +57,33 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State {
- String _networkName = 'Press button to show network';
- bool _success = false;
+ String? _networkName;
+ String? _descriptorSnippet;
+ String? _error;
+
+ Future _loadBindingData() async {
+ try {
+ await _ensureNativeLibraryDir();
+
+ final network = bdk.Network.testnet;
+ final descriptor = bdk.Descriptor(
+ 'wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/'
+ '84h/1h/0h/0/*)',
+ network,
+ );
- void _showSignetNetwork() {
- setState(() {
- // This simulates what the real Dart bindings would return
- // when properly linked to the Rust library
- _networkName = 'Signet';
- _success = true;
- });
+ setState(() {
+ _networkName = network.name;
+ _descriptorSnippet = descriptor.toString().substring(0, 32);
+ _error = null;
+ });
+ } catch (e) {
+ setState(() {
+ _error = e.toString();
+ _networkName = null;
+ _descriptorSnippet = null;
+ });
+ }
}
@override
@@ -53,52 +98,56 @@ class _MyHomePageState extends State {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
- _success ? Icons.check_circle : Icons.network_check,
+ _error != null
+ ? Icons.error_outline
+ : _networkName != null
+ ? Icons.check_circle
+ : Icons.network_check,
size: 80,
- color: _success ? Colors.green : Colors.grey,
+ color: _error != null
+ ? Colors.red
+ : _networkName != null
+ ? Colors.green
+ : Colors.grey,
),
const SizedBox(height: 20),
- const Text('BDK Network Type:', style: TextStyle(fontSize: 20)),
- Text(
- _networkName,
- style: Theme.of(context).textTheme.headlineMedium?.copyWith(
- color: _success ? Colors.orange : null,
- fontWeight: FontWeight.bold,
+ const Text('BDK bindings status', style: TextStyle(fontSize: 20)),
+ if (_networkName != null) ...[
+ Text(
+ 'Network: $_networkName',
+ style: Theme.of(context).textTheme.headlineMedium?.copyWith(
+ color: Colors.orange,
+ fontWeight: FontWeight.bold,
+ ),
),
- ),
- const SizedBox(height: 40),
- Container(
- padding: const EdgeInsets.all(16),
- margin: const EdgeInsets.symmetric(horizontal: 20),
- decoration: BoxDecoration(
- color: Colors.grey[100],
- borderRadius: BorderRadius.circular(8),
- ),
- child: Column(
- children: [
- Text('✅ Dart bindings generated with uniffi-dart'),
- Text('✅ Network enum includes SIGNET'),
- Text('✅ Flutter app ready to use BDK'),
- const SizedBox(height: 8),
- Text(
- 'Generated from: bdk.udl → bdk.dart',
- style: TextStyle(
- fontSize: 12,
- color: Colors.grey[600],
- fontFamily: 'monospace',
- ),
+ if (_descriptorSnippet != null)
+ Padding(
+ padding: const EdgeInsets.only(top: 12),
+ child: Text(
+ 'Descriptor sample: $_descriptorSnippet…',
+ style: const TextStyle(fontFamily: 'monospace'),
),
- ],
+ ),
+ ] else if (_error != null) ...[
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: Text(
+ _error!,
+ style: const TextStyle(color: Colors.red),
+ textAlign: TextAlign.center,
+ ),
),
- ),
+ ] else ...[
+ const Text('Press the button to load bindings'),
+ ],
],
),
),
floatingActionButton: FloatingActionButton.extended(
- onPressed: _showSignetNetwork,
+ onPressed: _loadBindingData,
backgroundColor: Colors.orange,
- icon: const Icon(Icons.network_check),
- label: const Text('Get Signet Network'),
+ icon: const Icon(Icons.play_circle_fill),
+ label: const Text('Load Dart binding'),
),
);
}
diff --git a/bdk_demo/pubspec.yaml b/bdk_demo/pubspec.yaml
index 476ccec..ad381e6 100644
--- a/bdk_demo/pubspec.yaml
+++ b/bdk_demo/pubspec.yaml
@@ -31,6 +31,9 @@ dependencies:
flutter:
sdk: flutter
+ bdk_dart:
+ path: ../
+
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
diff --git a/lib/README.md b/lib/README.md
index f52b57c..9df1efd 100644
--- a/lib/README.md
+++ b/lib/README.md
@@ -6,3 +6,121 @@ source control. Run `scripts/generate_bindings.sh` to regenerate
This placeholder file keeps the `lib/` directory present in the
repository so that `dart` tooling can resolve the package structure.
+
+## Mobile build artifacts
+
+Mobile consumers (Flutter/Dart) can build platform-specific binaries using the
+scripts in `scripts/`.
+
+### iOS (XCFramework)
+
+1. Generate the required static libraries:
+ ```bash
+ ./scripts/generate_bindings.sh --target ios
+ ```
+2. Package them into an XCFramework:
+ ```bash
+ ./scripts/build-ios-xcframework.sh
+ ```
+ The framework is written to `ios/Release/bdkffi.xcframework/`. Keep it there to
+ reuse across multiple apps, or direct the output into a Flutter project with the
+ optional flag:
+
+ ```bash
+ ./scripts/build-ios-xcframework.sh --output bdk_demo/ios/ios
+ ```
+
+### Android (.so libraries)
+
+1. Ensure `ANDROID_NDK_ROOT` points to an Android NDK r26c (or compatible) installation. For example:
+ - macOS/Linux (adjust the NDK directory as needed):
+ ```bash
+ export ANDROID_NDK_ROOT="$HOME/Library/Android/sdk/ndk/29.0.14206865"
+ ```
+ - Windows PowerShell:
+ ```powershell
+ $Env:ANDROID_NDK_ROOT = "C:\\Users\\\\AppData\\Local\\Android\\Sdk\\ndk\\29.0.14206865"
+ ```
+ (Use `setx ANDROID_NDK_ROOT ` if you want the variable persisted for future shells.)
+2. Build the shared objects:
+ ```bash
+ ./scripts/generate_bindings.sh --target android
+ ```
+3. Stage the artifacts for inclusion in a Flutter project (make script executable or run with `bash`):
+ ```bash
+ chmod +x scripts/build-android.sh
+ ./scripts/build-android.sh
+ # or
+ bash ./scripts/build-android.sh
+ ```
+
+ By default the script stages artifacts in `android/libs//libbdkffi.so` so the same
+ slices can be reused across multiple apps; direct them into the demo’s `jniLibs`
+ by passing `--output` as shown below.
+
+ ```bash
+ chmod +x scripts/build-android.sh
+ ./scripts/build-android.sh --output bdk_demo/android/app/src/main/jniLibs
+ # or, without changing permissions
+ bash ./scripts/build-android.sh --output bdk_demo/android/app/src/main/jniLibs
+
+### Desktop tests (macOS/Linux)
+
+- Run the base script without a target flag before executing `dart test`:
+ ```bash
+ ./scripts/generate_bindings.sh
+ ```
+ This regenerates the Dart bindings and drops the host dynamic library
+ (`libbdkffi.dylib` on macOS, `libbdkffi.so` on Linux) in the project root.
+ The generated loader expects those files when running in the VM, so tests fail if you only invoke the platform-specific targets.
+
+### Verification
+
+- On iOS, confirm the framework slices:
+ ```bash
+ lipo -info ios/Release/bdkffi.xcframework/ios-arm64/libbdkffi.a
+ lipo -info ios/Release/bdkffi.xcframework/ios-arm64_x86_64-simulator/libbdkffi.a
+ ```
+- On Android, verify the shared objects:
+ ```bash
+ find android/libs -name "libbdkffi.so"
+ ```
+
+## Flutter demo (`bdk_demo/`)
+
+Once the native artifacts are staged you can run the sample Flutter app to confirm
+the bindings load end-to-end.
+
+### Prerequisites
+
+1. Generate bindings and host libraries:
+ ```bash
+ ./scripts/generate_bindings.sh
+ ./scripts/generate_bindings.sh --target android
+ ```
+2. Stage Android shared objects into the app’s `jniLibs` (repeat per update):
+ ```bash
+ bash ./scripts/build-android.sh --output bdk_demo/android/app/src/main/jniLibs
+ ```
+
+### Run the app
+
+```bash
+cd bdk_demo
+flutter pub get
+flutter run
+```
+
+The Android variant uses a small MethodChannel to discover where the system
+actually deploys `libbdkffi.so` (some devices nest ABI folders under
+`nativeLibraryDir`). That path is fed back into Dart so the generated loader in
+`lib/bdk.dart` can `dlopen` the correct slice.
+
+### What the demo verifies
+
+- The native dynamic library loads on-device via the generated FFI loader.
+- A BIP84 descriptor string is constructed through the Dart bindings and displayed to the UI.
+- The UI badge switches between success and error states, so you immediately see
+ if the bindings failed to load or threw during descriptor creation (delete the android artifacts and rerun the app to simulate a failure).
+
+Use this screen as a smoke test after rebuilding bindings or regenerating artifacts; if it turns green and shows `Network: testnet` the demo is exercising the FFI surface successfully.
diff --git a/scripts/build-android.sh b/scripts/build-android.sh
new file mode 100644
index 0000000..a359415
--- /dev/null
+++ b/scripts/build-android.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
+ANDROID_BUILD_ROOT="$PROJECT_ROOT/build/android"
+OUTPUT_ROOT="android/libs"
+
+usage() {
+ cat <]
+
+Copy the built Android shared libraries into . When is relative,
+it is resolved from the repository root. Defaults to android/libs.
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --output|-o)
+ OUTPUT_ROOT="${2:-}"
+ if [[ -z "$OUTPUT_ROOT" ]]; then
+ echo "Error: --output requires a value" >&2
+ exit 1
+ fi
+ shift 2
+ ;;
+ --help|-h)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "Unknown option: $1" >&2
+ usage >&2
+ exit 1
+ ;;
+ esac
+done
+
+if [[ "$OUTPUT_ROOT" != /* ]]; then
+ OUTPUT_ROOT="$PROJECT_ROOT/$OUTPUT_ROOT"
+fi
+
+if [[ -z "${ANDROID_NDK_ROOT:-}" ]]; then
+ echo "ANDROID_NDK_ROOT must be set" >&2
+ exit 1
+fi
+
+if [[ ! -d "$ANDROID_BUILD_ROOT" ]]; then
+ echo "Android build artifacts not found in $ANDROID_BUILD_ROOT" >&2
+ echo "Run scripts/generate_bindings.sh --target android first." >&2
+ exit 1
+fi
+
+mkdir -p "$OUTPUT_ROOT"
+
+for ABI in arm64-v8a armeabi-v7a x86_64; do
+ ARTIFACT="$ANDROID_BUILD_ROOT/${ABI}/libbdkffi.so"
+ if [[ ! -f "$ARTIFACT" ]]; then
+ echo "Missing artifact for $ABI at $ARTIFACT" >&2
+ exit 1
+ fi
+
+ DEST_DIR="$OUTPUT_ROOT/$ABI"
+ mkdir -p "$DEST_DIR"
+ cp "$ARTIFACT" "$DEST_DIR/"
+ echo "Copied $ABI artifact to $DEST_DIR"
+done
+
+echo "Android libraries staged under $OUTPUT_ROOT"
diff --git a/scripts/build-ios-xcframework.sh b/scripts/build-ios-xcframework.sh
new file mode 100755
index 0000000..9ba4c23
--- /dev/null
+++ b/scripts/build-ios-xcframework.sh
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
+IOS_BUILD_ROOT="$PROJECT_ROOT/build/ios"
+OUTPUT_ROOT="ios/Release"
+XCFRAMEWORK_NAME="bdkffi.xcframework"
+
+DEVICE_LIB="$IOS_BUILD_ROOT/aarch64-apple-ios/libbdkffi.a"
+SIM_ARM64_LIB="$IOS_BUILD_ROOT/aarch64-apple-ios-sim/libbdkffi.a"
+SIM_X86_LIB="$IOS_BUILD_ROOT/x86_64-apple-ios/libbdkffi.a"
+SIM_UNIVERSAL_DIR="$IOS_BUILD_ROOT/simulator-universal"
+SIM_UNIVERSAL_LIB="$SIM_UNIVERSAL_DIR/libbdkffi.a"
+
+usage() {
+ cat <]
+
+Create an XCFramework from previously built static libraries and write it to
+. When is relative, it is resolved from the repository root.
+Defaults to ios/Release.
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --output|-o)
+ OUTPUT_ROOT="${2:-}"
+ if [[ -z "$OUTPUT_ROOT" ]]; then
+ echo "Error: --output requires a value" >&2
+ exit 1
+ fi
+ shift 2
+ ;;
+ --help|-h)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "Unknown option: $1" >&2
+ usage >&2
+ exit 1
+ ;;
+ esac
+done
+
+if [[ "$OUTPUT_ROOT" != /* ]]; then
+ OUTPUT_ROOT="$PROJECT_ROOT/$OUTPUT_ROOT"
+fi
+
+if [[ ! -f "$DEVICE_LIB" ]]; then
+ echo "Missing device library: $DEVICE_LIB" >&2
+ echo "Run scripts/generate_bindings.sh --target ios first." >&2
+ exit 1
+fi
+
+if [[ ! -f "$SIM_ARM64_LIB" || ! -f "$SIM_X86_LIB" ]]; then
+ echo "Missing simulator libraries: $SIM_ARM64_LIB or $SIM_X86_LIB" >&2
+ echo "Run scripts/generate_bindings.sh --target ios first." >&2
+ exit 1
+fi
+
+if ! command -v xcodebuild >/dev/null 2>&1; then
+ echo "xcodebuild is required to create an XCFramework" >&2
+ exit 1
+fi
+
+mkdir -p "$SIM_UNIVERSAL_DIR"
+
+echo "Combining simulator slices with lipo..."
+lipo -create "$SIM_ARM64_LIB" "$SIM_X86_LIB" -output "$SIM_UNIVERSAL_LIB"
+
+mkdir -p "$OUTPUT_ROOT"
+OUTPUT_PATH="$OUTPUT_ROOT/$XCFRAMEWORK_NAME"
+rm -rf "$OUTPUT_PATH"
+
+echo "Creating XCFramework at $OUTPUT_PATH..."
+xcodebuild -create-xcframework \
+ -library "$DEVICE_LIB" \
+ -library "$SIM_UNIVERSAL_LIB" \
+ -output "$OUTPUT_PATH"
+
+echo "XCFramework created: $OUTPUT_PATH"
diff --git a/scripts/generate_bindings.sh b/scripts/generate_bindings.sh
index 13e94f5..a338234 100755
--- a/scripts/generate_bindings.sh
+++ b/scripts/generate_bindings.sh
@@ -1,8 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
+TARGET="desktop"
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --target)
+ TARGET="${2:-desktop}"
+ shift 2
+ ;;
+ --help|-h)
+ echo "Usage: $0 [--target desktop|ios|android]"
+ exit 0
+ ;;
+ *)
+ echo "Unknown option: $1" >&2
+ exit 1
+ ;;
+ esac
+done
+
OS=$(uname -s)
-echo "Running on $OS"
+ARCH=$(uname -m)
+echo "Running on $OS ($ARCH)"
dart --version
dart pub get
@@ -10,44 +29,154 @@ dart pub get
mkdir -p lib
rm -f lib/bdk.dart
-# Install Rust targets if on macOS
if [[ "$OS" == "Darwin" ]]; then
LIBNAME=libbdkffi.dylib
elif [[ "$OS" == "Linux" ]]; then
LIBNAME=libbdkffi.so
else
- echo "Unsupported os: $OS"
+ echo "Unsupported os: $OS" >&2
exit 1
fi
# Run from the specific crate inside the embedded submodule
cd ./bdk-ffi/bdk-ffi/
-echo "Building bdk-ffi crate and generating Dart bindings..."
-cargo build --profile dev -p bdk-ffi
-# Generate Dart bindings using local uniffi-bindgen wrapper
-(cd ../../ && cargo run --profile dev --bin uniffi-bindgen -- --language dart --library bdk-ffi/bdk-ffi/target/debug/$LIBNAME --out-dir lib/)
+generate_bindings() {
+ echo "Building bdk-ffi crate and generating Dart bindings..."
+ cargo build --profile dev -p bdk-ffi
+ (cd ../../ && cargo run --profile dev --bin uniffi-bindgen -- --language dart --library bdk-ffi/bdk-ffi/target/debug/$LIBNAME --out-dir lib/)
+}
-if [[ "$OS" == "Darwin" ]]; then
- echo "Generating native binaries..."
- rustup target add aarch64-apple-darwin x86_64-apple-darwin
- # This is a test script the actual release should not include the test utils feature
- cargo build --profile dev -p bdk-ffi --target aarch64-apple-darwin &
- cargo build --profile dev -p bdk-ffi --target x86_64-apple-darwin &
- wait
-
- echo "Building macOS fat library"
- lipo -create -output ../../$LIBNAME \
- target/aarch64-apple-darwin/debug/$LIBNAME \
- target/x86_64-apple-darwin/debug/$LIBNAME
-else
- echo "Generating native binaries..."
- rustup target add x86_64-unknown-linux-gnu
- # This is a test script the actual release should not include the test utils feature
- cargo build --profile dev -p bdk-ffi --target x86_64-unknown-linux-gnu
+build_ios() {
+ echo "Building iOS static libraries..."
+ rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim >/dev/null
- echo "Copying bdk-ffi binary"
- cp target/x86_64-unknown-linux-gnu/debug/$LIBNAME ../../$LIBNAME
-fi
+ PROFILE="release-smaller"
+ OUT_ROOT="../../build/ios"
+ mkdir -p "$OUT_ROOT"
+
+ for TARGET_TRIPLE in aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim; do
+ echo " -> $TARGET_TRIPLE"
+ cargo build --profile "$PROFILE" -p bdk-ffi --target "$TARGET_TRIPLE"
+ ARTIFACT="target/${TARGET_TRIPLE}/${PROFILE}/libbdkffi.a"
+ DEST_DIR="$OUT_ROOT/${TARGET_TRIPLE}"
+ mkdir -p "$DEST_DIR"
+ cp "$ARTIFACT" "$DEST_DIR/"
+ done
+}
+
+build_android() {
+ if [[ -z "${ANDROID_NDK_ROOT:-}" ]]; then
+ echo "ANDROID_NDK_ROOT must be set to build Android artifacts" >&2
+ exit 1
+ fi
+
+ echo "Building Android shared libraries..."
+ rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android >/dev/null
+
+ API_LEVEL=24
+ case "$OS" in
+ Darwin)
+ HOST_OS=darwin
+ ;;
+ Linux)
+ HOST_OS=linux
+ ;;
+ *)
+ echo "Unsupported host for Android builds: $OS" >&2
+ exit 1
+ ;;
+ esac
+
+ case "$ARCH" in
+ x86_64)
+ HOST_ARCH=x86_64
+ ;;
+ arm64|aarch64)
+ HOST_ARCH=arm64
+ ;;
+ *)
+ echo "Unsupported architecture for Android builds: $ARCH" >&2
+ exit 1
+ ;;
+ esac
+
+ TOOLCHAIN="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/${HOST_OS}-${HOST_ARCH}/bin"
+ if [[ ! -d "$TOOLCHAIN" ]]; then
+ echo "Unable to locate NDK toolchain at $TOOLCHAIN" >&2
+ exit 1
+ fi
+
+ OUT_ROOT="../../build/android"
+ mkdir -p "$OUT_ROOT"
+
+ for TARGET_TRIPLE in aarch64-linux-android armv7-linux-androideabi x86_64-linux-android; do
+ case "$TARGET_TRIPLE" in
+ aarch64-linux-android)
+ ABI="arm64-v8a"
+ CLANG="${TOOLCHAIN}/aarch64-linux-android${API_LEVEL}-clang"
+ TARGET_ENV_LOWER="aarch64_linux_android"
+ ;;
+ armv7-linux-androideabi)
+ ABI="armeabi-v7a"
+ CLANG="${TOOLCHAIN}/armv7a-linux-androideabi${API_LEVEL}-clang"
+ TARGET_ENV_LOWER="armv7_linux_androideabi"
+ ;;
+ x86_64-linux-android)
+ ABI="x86_64"
+ CLANG="${TOOLCHAIN}/x86_64-linux-android${API_LEVEL}-clang"
+ TARGET_ENV_LOWER="x86_64_linux_android"
+ ;;
+ esac
+
+ TARGET_ENV=$(echo "$TARGET_TRIPLE" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
+
+ export CARGO_TARGET_${TARGET_ENV}_LINKER="$CLANG"
+ export CARGO_TARGET_${TARGET_ENV}_AR="${TOOLCHAIN}/llvm-ar"
+
+ export CC_${TARGET_ENV_LOWER}="$CLANG"
+ export AR_${TARGET_ENV_LOWER}="${TOOLCHAIN}/llvm-ar"
+
+ echo " -> $TARGET_TRIPLE ($ABI)"
+ cargo build --profile release-smaller -p bdk-ffi --target "$TARGET_TRIPLE"
+ ARTIFACT="target/${TARGET_TRIPLE}/release-smaller/libbdkffi.so"
+ DEST_DIR="$OUT_ROOT/$ABI"
+ mkdir -p "$DEST_DIR"
+ cp "$ARTIFACT" "$DEST_DIR/"
+ done
+}
+
+case "$TARGET" in
+ ios)
+ generate_bindings
+ build_ios
+ ;;
+ android)
+ generate_bindings
+ build_android
+ ;;
+ desktop|*)
+ generate_bindings
+ if [[ "$OS" == "Darwin" ]]; then
+ echo "Generating native macOS binaries..."
+ rustup target add aarch64-apple-darwin x86_64-apple-darwin >/dev/null
+ cargo build --profile dev -p bdk-ffi --target aarch64-apple-darwin &
+ cargo build --profile dev -p bdk-ffi --target x86_64-apple-darwin &
+ wait
+
+ echo "Building macOS fat library"
+ lipo -create -output ../../$LIBNAME \
+ target/aarch64-apple-darwin/debug/$LIBNAME \
+ target/x86_64-apple-darwin/debug/$LIBNAME
+ else
+ echo "Generating native Linux binaries..."
+ rustup target add x86_64-unknown-linux-gnu >/dev/null
+ cargo build --profile dev -p bdk-ffi --target x86_64-unknown-linux-gnu
+
+ echo "Copying bdk-ffi binary"
+ cp target/x86_64-unknown-linux-gnu/debug/$LIBNAME ../../$LIBNAME
+ fi
+ ;;
+esac
echo "All done!"