From 4824f3964a866415496923e320aec0e2297714d9 Mon Sep 17 00:00:00 2001 From: John Osezele Date: Tue, 18 Nov 2025 14:33:50 +0100 Subject: [PATCH 1/8] feat: add mobile platform build support --- scripts/build-android.sh | 35 ++++++ scripts/build-ios-xcframework.sh | 48 ++++++++ scripts/generate_bindings.sh | 183 ++++++++++++++++++++++++++----- 3 files changed, 239 insertions(+), 27 deletions(-) create mode 100644 scripts/build-android.sh create mode 100755 scripts/build-ios-xcframework.sh diff --git a/scripts/build-android.sh b/scripts/build-android.sh new file mode 100644 index 0000000..770f770 --- /dev/null +++ b/scripts/build-android.sh @@ -0,0 +1,35 @@ +#!/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="$PROJECT_ROOT/android/libs" + +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..fb4bee0 --- /dev/null +++ b/scripts/build-ios-xcframework.sh @@ -0,0 +1,48 @@ +#!/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="$PROJECT_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" + +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!" From b4886ddca1d49782101017dda3c112b967d1207d Mon Sep 17 00:00:00 2001 From: John Osezele Date: Tue, 18 Nov 2025 14:35:15 +0100 Subject: [PATCH 2/8] docs: add mobile build artifacts documentation --- lib/README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/lib/README.md b/lib/README.md index f52b57c..501f64a 100644 --- a/lib/README.md +++ b/lib/README.md @@ -6,3 +6,64 @@ 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/`. + +### 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: + ```bash + ./scripts/build-android.sh + ``` + Output libraries are copied to `android/libs//libbdkffi.so`. + +### 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" + ``` From 3f44c2f2eb52e12f07eb382c183cc23b3080d22b Mon Sep 17 00:00:00 2001 From: John Osezele Date: Tue, 18 Nov 2025 14:36:15 +0100 Subject: [PATCH 3/8] chore: ignore mobile build artifacts --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index cde80cf..ced98cc 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ Thumbs.db # Native libs built locally libbdkffi.* +android/libs +ios/Release/ +bdk_demo/ios/ios/Release/ test_output.txt test output.txt lib/bdk.dart From a32ba7403b35136a3630f2360cef142895650d81 Mon Sep 17 00:00:00 2001 From: John Osezele Date: Tue, 25 Nov 2025 09:04:25 +0100 Subject: [PATCH 4/8] docs: add execution instructions for Android build script --- lib/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/README.md b/lib/README.md index 501f64a..fab6260 100644 --- a/lib/README.md +++ b/lib/README.md @@ -40,9 +40,12 @@ scripts in `scripts/`. ```bash ./scripts/generate_bindings.sh --target android ``` -3. Stage the artifacts for inclusion in a Flutter project: +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 ``` Output libraries are copied to `android/libs//libbdkffi.so`. From 72a85bbb8b42a8e7fa5d5ddc2a16fe20f16b2a46 Mon Sep 17 00:00:00 2001 From: John Osezele Date: Tue, 25 Nov 2025 11:32:55 +0100 Subject: [PATCH 5/8] build: add android host project to demo --- bdk_demo/.metadata | 25 ++++++++-- bdk_demo/android/.gitignore | 14 ++++++ bdk_demo/android/app/build.gradle.kts | 44 +++++++++++++++++ .../android/app/src/debug/AndroidManifest.xml | 7 +++ .../android/app/src/main/AndroidManifest.xml | 45 ++++++++++++++++++ .../com/example/bdk_demo/MainActivity.kt | 5 ++ .../res/drawable-v21/launch_background.xml | 12 +++++ .../main/res/drawable/launch_background.xml | 12 +++++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 +++++++ .../app/src/main/res/values/styles.xml | 18 +++++++ .../app/src/profile/AndroidManifest.xml | 7 +++ bdk_demo/android/build.gradle.kts | 24 ++++++++++ bdk_demo/android/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.properties | 5 ++ bdk_demo/android/settings.gradle.kts | 26 ++++++++++ 20 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 bdk_demo/android/.gitignore create mode 100644 bdk_demo/android/app/build.gradle.kts create mode 100644 bdk_demo/android/app/src/debug/AndroidManifest.xml create mode 100644 bdk_demo/android/app/src/main/AndroidManifest.xml create mode 100644 bdk_demo/android/app/src/main/kotlin/com/example/bdk_demo/MainActivity.kt create mode 100644 bdk_demo/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 bdk_demo/android/app/src/main/res/drawable/launch_background.xml create mode 100644 bdk_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 bdk_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 bdk_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 bdk_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 bdk_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 bdk_demo/android/app/src/main/res/values-night/styles.xml create mode 100644 bdk_demo/android/app/src/main/res/values/styles.xml create mode 100644 bdk_demo/android/app/src/profile/AndroidManifest.xml create mode 100644 bdk_demo/android/build.gradle.kts create mode 100644 bdk_demo/android/gradle.properties create mode 100644 bdk_demo/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 bdk_demo/android/settings.gradle.kts diff --git a/bdk_demo/.metadata b/bdk_demo/.metadata index bad0587..829937b 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,26 @@ 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 + - platform: linux + create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b + base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b + - platform: macos + create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b + base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b + - platform: web + create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b + base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b + - platform: windows + 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..8e2db43 --- /dev/null +++ b/bdk_demo/android/app/src/main/kotlin/com/example/bdk_demo/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.bdk_demo + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() 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 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 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") From b8c6ba8f682cb2396b4e2c6e6fe667ede2006ba1 Mon Sep 17 00:00:00 2001 From: John Osezele Date: Tue, 25 Nov 2025 11:37:20 +0100 Subject: [PATCH 6/8] chore: remove unsupported platform from metadata --- bdk_demo/.metadata | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/bdk_demo/.metadata b/bdk_demo/.metadata index 829937b..faf2eb0 100644 --- a/bdk_demo/.metadata +++ b/bdk_demo/.metadata @@ -21,18 +21,6 @@ migration: - platform: ios create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b - - platform: linux - create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b - base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b - - platform: macos - create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b - base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b - - platform: web - create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b - base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b - - platform: windows - create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b - base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b # User provided section From 23144068071490db28150e562afd1b3d4891430c Mon Sep 17 00:00:00 2001 From: John Osezele Date: Tue, 25 Nov 2025 17:03:50 +0100 Subject: [PATCH 7/8] Add optional --output flag to both iOS and Android build scripts --- .gitignore | 3 ++- lib/README.md | 23 +++++++++++++++---- scripts/build-android.sh | 37 ++++++++++++++++++++++++++++++- scripts/build-ios-xcframework.sh | 38 +++++++++++++++++++++++++++++++- 4 files changed, 94 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index ced98cc..f668078 100644 --- a/.gitignore +++ b/.gitignore @@ -27,8 +27,9 @@ Thumbs.db # Native libs built locally libbdkffi.* android/libs +bdk_demo/android/app/src/main/jniLibs ios/Release/ -bdk_demo/ios/ios/Release/ +bdk_demo/ios/ios/ test_output.txt test output.txt lib/bdk.dart diff --git a/lib/README.md b/lib/README.md index fab6260..b0e695f 100644 --- a/lib/README.md +++ b/lib/README.md @@ -22,7 +22,13 @@ scripts in `scripts/`. ```bash ./scripts/build-ios-xcframework.sh ``` - The framework is written to `ios/Release/bdkffi.xcframework/`. + 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) @@ -42,12 +48,21 @@ scripts in `scripts/`. ``` 3. Stage the artifacts for inclusion in a Flutter project (make script executable or run with `bash`): ```bash - chmod +x scripts/build-android.sh + chmod +x scripts/build-android.sh ./scripts/build-android.sh - # or + # or bash ./scripts/build-android.sh ``` - Output libraries are copied to `android/libs//libbdkffi.so`. + + 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) diff --git a/scripts/build-android.sh b/scripts/build-android.sh index 770f770..a359415 100644 --- a/scripts/build-android.sh +++ b/scripts/build-android.sh @@ -4,7 +4,42 @@ 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="$PROJECT_ROOT/android/libs" +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 diff --git a/scripts/build-ios-xcframework.sh b/scripts/build-ios-xcframework.sh index fb4bee0..9ba4c23 100755 --- a/scripts/build-ios-xcframework.sh +++ b/scripts/build-ios-xcframework.sh @@ -4,7 +4,7 @@ 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="$PROJECT_ROOT/ios/Release" +OUTPUT_ROOT="ios/Release" XCFRAMEWORK_NAME="bdkffi.xcframework" DEVICE_LIB="$IOS_BUILD_ROOT/aarch64-apple-ios/libbdkffi.a" @@ -13,6 +13,42 @@ 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 From 2ea0aae0eb41c07a7c2cf1902b32e0871b5307a1 Mon Sep 17 00:00:00 2001 From: John Osezele Date: Tue, 25 Nov 2025 22:18:24 +0100 Subject: [PATCH 8/8] feat: integrate dart bindings into Flutter demo with native library loading --- .gitignore | 6 + .../com/example/bdk_demo/MainActivity.kt | 134 ++++++++++++++++- bdk_demo/lib/main.dart | 142 ++++++++++++------ bdk_demo/pubspec.yaml | 3 + lib/README.md | 39 +++++ 5 files changed, 275 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index f668078..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/ 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 index 8e2db43..7f7cb8b 100644 --- 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 @@ -1,5 +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 -class MainActivity : FlutterActivity() +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/lib/main.dart b/bdk_demo/lib/main.dart index b93e32c..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(); - void _showSignetNetwork() { - setState(() { - // This simulates what the real Dart bindings would return - // when properly linked to the Rust library - _networkName = 'Signet'; - _success = true; - }); + final network = bdk.Network.testnet; + final descriptor = bdk.Descriptor( + 'wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/' + '84h/1h/0h/0/*)', + network, + ); + + setState(() { + _networkName = network.name; + _descriptorSnippet = descriptor.toString().substring(0, 32); + _error = null; + }); + } catch (e) { + setState(() { + _error = e.toString(); + _networkName = null; + _descriptorSnippet = null; + }); + } } @override @@ -53,56 +98,57 @@ 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'), ), ); } -} \ No newline at end of file +} 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 b0e695f..9df1efd 100644 --- a/lib/README.md +++ b/lib/README.md @@ -85,3 +85,42 @@ scripts in `scripts/`. ```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.