diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..aae098ff --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,15 @@ +# Android cross-compilation configuration +# The actual toolchain paths are set by build-android.sh via environment variables + +[target.aarch64-linux-android] +# Environment variables will set the linker and ar + +# Pass Android-specific flags to RocksDB compilation +[env] +# Ensure RocksDB doesn't detect unavailable Android features +ROCKSDB_DISABLE_FALLOCATE = "1" +ROCKSDB_DISABLE_SYNC_FILE_RANGE = "1" +ROCKSDB_DISABLE_PTHREAD_MUTEX_ADAPTIVE_NP = "1" +ROCKSDB_DISABLE_SCHED_GETCPU = "1" +ROCKSDB_DISABLE_AUXV = "1" +ROCKSDB_DISABLE_MALLOC_USABLE_SIZE = "1" diff --git a/.gitignore b/.gitignore index 1687a072..8f658c84 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,15 @@ /package-lock.json /.vscode /data + +# Android/Gradle +android-app/.gradle/ +android-app/app/build/ +android-app/local.properties +*.apk +*.aab + +# Build artifacts +*.o +*.a +*.so diff --git a/Cargo.lock b/Cargo.lock index 5c9af3b7..15eb7cf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,24 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f" +dependencies = [ + "android_log-sys", + "env_logger 0.10.2", + "log", + "once_cell", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -369,6 +387,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -787,12 +811,13 @@ dependencies = [ "ckb-types", "clap", "ctrlc", - "env_logger", + "env_logger 0.11.6", "jsonrpc-core", "jsonrpc-derive", "jsonrpc-http-server", "jsonrpc-server-utils", "log", + "openssl", "rand 0.8.5", "serde_json", "tempfile", @@ -804,8 +829,10 @@ dependencies = [ name = "ckb-light-client-lib" version = "0.5.4" dependencies = [ + "android_logger", "anyhow", "ckb-app-config", + "ckb-async-runtime", "ckb-chain", "ckb-chain-spec", "ckb-constant", @@ -822,6 +849,7 @@ dependencies = [ "ckb-rocksdb", "ckb-script", "ckb-shared", + "ckb-stop-handler", "ckb-store", "ckb-systemtime", "ckb-traits", @@ -830,10 +858,11 @@ dependencies = [ "ckb-verification", "console_error_panic_hook", "dashmap 5.5.3", - "env_logger", + "env_logger 0.11.6", "golomb-coded-set", "governor 0.6.3", "idb", + "jni", "lazy_static", "light-client-db-common", "linked-hash-map", @@ -1450,6 +1479,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "console" version = "0.15.10" @@ -1864,6 +1903,16 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.11.6" @@ -2783,6 +2832,28 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if 1.0.0", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.32" @@ -5126,6 +5197,15 @@ dependencies = [ "windows-targets 0.53.0", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5144,6 +5224,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5176,6 +5271,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5188,6 +5289,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5200,6 +5307,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5224,6 +5337,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5236,6 +5355,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5248,6 +5373,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5260,6 +5391,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/android-app/BUILD_INSTRUCTIONS.md b/android-app/BUILD_INSTRUCTIONS.md new file mode 100644 index 00000000..098441ac --- /dev/null +++ b/android-app/BUILD_INSTRUCTIONS.md @@ -0,0 +1,148 @@ +# Building the CKB Light Client Android APK + +The Rust binary has been successfully built for Android! Now you need to build the Android APK. + +## Prerequisites + +You need the Android SDK with build tools to compile the APK. Here are your options: + +### Option 1: Using Android Studio (Recommended - Easiest) + +1. **Install Android Studio** from https://developer.android.com/studio + +2. **Open the project:** + - Launch Android Studio + - Select "Open an existing project" + - Navigate to: `/home/exec/Projects/github.com/nervosnetwork/ckb-light-client/android-app` + +3. **Let Android Studio sync:** + - It will automatically download required SDK components + - Wait for Gradle sync to complete + +4. **Build the APK:** + - Menu: Build → Build Bundle(s) / APK(s) → Build APK(s) + - Or click the "Build" button in the toolbar + +5. **Find your APK:** + - Location: `app/build/outputs/apk/debug/app-debug.apk` (debug build) + - Location: `app/build/outputs/apk/release/app-release.apk` (release build) + +### Option 2: Command Line (Requires Android SDK) + +If you prefer command-line building, you need to install the Android SDK first: + +#### Step 1: Install Android Command Line Tools + +```bash +# Download Android command line tools +cd ~ +mkdir -p Android/Sdk/cmdline-tools +cd Android/Sdk/cmdline-tools + +# Download latest command line tools from: +# https://developer.android.com/studio#command-line-tools-only +# For Linux: +wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip +unzip commandlinetools-linux-9477386_latest.zip +mv cmdline-tools latest +``` + +#### Step 2: Install Required SDK Components + +```bash +export ANDROID_HOME=$HOME/Android/Sdk +export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$PATH + +# Accept licenses +yes | sdkmanager --licenses + +# Install required components +sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0" +``` + +#### Step 3: Build with Gradle + +```bash +cd /home/exec/Projects/github.com/nervosnetwork/ckb-light-client/android-app + +# Build debug APK +./gradlew assembleDebug + +# Or build release APK (unsigned) +./gradlew assembleRelease +``` + +### Option 3: Use a Build Server / CI + +If you have access to a CI system with Android build capabilities (GitHub Actions, GitLab CI, etc.), you can build there. + +## After Building + +### Install the APK on your device: + +```bash +# Make sure your Android device is connected and USB debugging is enabled +adb devices + +# Install the APK +adb install app/build/outputs/apk/debug/app-debug.apk +# or +adb install app/build/outputs/apk/release/app-release.apk +``` + +### Run and monitor: + +```bash +# View logs from the terminal +adb logcat | grep CKBLightClient + +# Or just open the app on your device and press "Start" +``` + +## What's Already Done + +✅ Rust binary compiled for Android (arm64-v8a) +✅ Android app code complete with real-time log viewing +✅ Build configuration ready (Gradle files) +✅ Config file bundled in assets + +**You just need to build the APK using one of the methods above!** + +## Quick Test (Without Building APK) + +If you want to test the binary directly on an Android device without building the full APK: + +```bash +# Push the binary to your device +adb push target/aarch64-linux-android/release/ckb-light-client /data/local/tmp/ + +# Push the config +adb push config/mainnet.toml /data/local/tmp/ + +# Make it executable +adb shell chmod +x /data/local/tmp/ckb-light-client + +# Run it (in adb shell) +adb shell +cd /data/local/tmp +./ckb-light-client run --config-file mainnet.toml +``` + +This will run the light client directly in adb shell, though you won't get the nice Android UI with log viewing. + +## Troubleshooting + +**"SDK location not found"** +- Set ANDROID_HOME: `export ANDROID_HOME=$HOME/Android/Sdk` + +**"Gradle sync failed"** +- Make sure you have Java JDK installed: `java -version` +- Gradle requires JDK 17 or newer + +**"Build tools not found"** +- Install via SDK Manager in Android Studio +- Or via command line: `sdkmanager "build-tools;34.0.0"` + +## Need Help? + +The Android app is fully implemented and ready to build. The only requirement is the Android SDK with build tools, which is most easily obtained through Android Studio. diff --git a/android-app/README.md b/android-app/README.md new file mode 100644 index 00000000..910cd328 --- /dev/null +++ b/android-app/README.md @@ -0,0 +1,220 @@ +# CKB Light Client - Android App + +Minimal Android wrapper app that runs ckb-light-client binary on Android devices and displays logs in real-time. + +## Features + +- Runs ckb-light-client natively on Android (arm64-v8a) +- Real-time log viewing with scrollable TextView +- Start/Stop controls +- Automatic binary extraction and config setup +- Uses app private storage for data +- RPC on localhost only (127.0.0.1:9000) + +## Prerequisites + +### For Building the Rust Binary + +1. **Android NDK r26+** + - Install via Android Studio: Tools > SDK Manager > SDK Tools > NDK (Side by side) + - Or download from: https://developer.android.com/ndk/downloads + - Set environment: `export ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk/` + +2. **Rust Android Target** + - Already installed: `aarch64-linux-android` + +### For Building the Android App + +1. **Android Studio** (recommended) or **Command-line tools** + - Android SDK with API level 34 + - Gradle 8.5+ (will be downloaded automatically by Gradle wrapper) + - JDK 8 or higher + +## Build Instructions + +### Step 1: Build Rust Binary for Android + +```bash +cd /home/exec/Projects/github.com/nervosnetwork/ckb-light-client + +# Run the build script (it will check for NDK and guide you) +./build-android.sh +``` + +The script will: +- Check for Android NDK installation +- Set up cross-compilation environment +- Build the binary for arm64-v8a +- Output: `target/aarch64-linux-android/release/ckb-light-client` (~15-25 MB) + +### Step 2: Build Android APK + +#### Option A: Using Android Studio (Recommended) + +1. Open Android Studio +2. Select "Open an existing project" +3. Navigate to `/home/exec/Projects/github.com/nervosnetwork/ckb-light-client/android-app` +4. Wait for Gradle sync to complete +5. Build > Build Bundle(s) / APK(s) > Build APK(s) +6. APK will be at: `app/build/outputs/apk/release/app-release.apk` + +#### Option B: Using Command Line + +```bash +cd /home/exec/Projects/github.com/nervosnetwork/ckb-light-client/android-app + +# Build release APK +./gradlew assembleRelease + +# Or build debug APK +./gradlew assembleDebug +``` + +**Note:** The Gradle build will automatically copy the Rust binary from `target/aarch64-linux-android/release/ckb-light-client` to `app/src/main/jniLibs/arm64-v8a/libckb_light_client.so`. + +### Step 3: Install on Device + +```bash +# Install via adb +adb install app/build/outputs/apk/release/app-release.apk + +# Or install debug version +adb install app/build/outputs/apk/debug/app-debug.apk +``` + +## Usage + +1. **Launch the app** on your Android device +2. The app will automatically: + - Extract the ckb-light-client binary to app storage + - Create a modified mainnet.toml config with Android paths + - Display "Ready to start" when setup is complete +3. **Press "Start"** to launch ckb-light-client +4. **View logs** in real-time in the scrollable log view showing: + - Version and build information + - Network connection status + - Block synchronization progress + - RPC server startup +5. **Press "Stop"** to terminate the process + +## Log Viewing + +The app displays all stdout/stderr output from the ckb-light-client binary in a scrollable TextView with: +- Monospace font for easy reading +- Auto-scroll to show latest logs +- Real-time streaming as logs are generated + +You can scroll through the logs at any time. The app captures: +- Startup messages (version, git hash, build time) +- Network events (peer connections, bootnodes) +- Sync progress (header downloads, verification) +- RPC server status +- Any errors or warnings + +## Storage + +The app uses private storage for all data: + +``` +/data/data/com.nervosnetwork.ckblightclient/files/ +├── ckb-light-client # Executable binary +├── mainnet.toml # Config with Android paths +└── data/ + ├── store/ # RocksDB database (grows to 500MB-2GB) + └── network/ # Peer information +``` + +All data is automatically deleted when the app is uninstalled. + +## Network & RPC + +- **P2P Network:** Connects to CKB mainnet bootnodes (configured in mainnet.toml) +- **RPC Server:** Runs on `127.0.0.1:9000` (localhost only, not accessible from outside the app) +- **Permissions:** Requires `INTERNET` and `ACCESS_NETWORK_STATE` + +## Troubleshooting + +### Binary won't build + +```bash +# Check NDK installation +ls $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/*/bin/aarch64-linux-android21-clang + +# Check Rust target +rustup target list --installed | grep aarch64-linux-android +``` + +### Gradle build fails - binary not found + +```bash +# Make sure you built the Rust binary first +ls -lh target/aarch64-linux-android/release/ckb-light-client + +# If missing, run: +./build-android.sh +``` + +### App crashes on startup + +```bash +# Check logs +adb logcat | grep -E "(CKBLightClient|AndroidRuntime)" + +# Check binary permissions +adb shell ls -l /data/data/com.nervosnetwork.ckblightclient/files/ckb-light-client +# Should show: -rwx------ +``` + +### No logs appear in the app + +- The binary may not have started - check `adb logcat` for errors +- Ensure the binary has execute permissions +- Check that the config file was created correctly + +### Network connection fails + +- Ensure device has WiFi or mobile data connection +- Check INTERNET permission in AndroidManifest.xml +- Verify bootnodes are reachable + +## Development + +### Project Structure + +``` +android-app/ +├── app/ +│ ├── build.gradle.kts # App module build config +│ └── src/main/ +│ ├── AndroidManifest.xml # App permissions and components +│ ├── java/com/nervosnetwork/ckblightclient/ +│ │ └── MainActivity.kt # Main app logic +│ ├── res/layout/ +│ │ └── activity_main.xml # UI layout +│ ├── assets/ +│ │ └── mainnet.toml # Config template +│ └── jniLibs/arm64-v8a/ +│ └── libckb_light_client.so # Rust binary (copied at build time) +├── build.gradle.kts # Root build config +├── settings.gradle.kts # Project settings +└── gradle/wrapper/ + └── gradle-wrapper.properties # Gradle version +``` + +### Modifying the App + +- **Change config:** Edit `app/src/main/assets/mainnet.toml` or modify the path replacement logic in `MainActivity.kt` +- **Adjust UI:** Edit `app/src/main/res/layout/activity_main.xml` +- **Change package name:** Update `namespace` in `app/build.gradle.kts` and rename package directory +- **Add features:** Extend `MainActivity.kt` (e.g., add RPC client, display sync status, etc.) + +## Version + +- CKB Light Client: 0.5.3 +- Min SDK: 21 (Android 5.0) +- Target SDK: 34 (Android 14) +- ABI: arm64-v8a only + +## License + +Same as ckb-light-client: MIT diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts new file mode 100644 index 00000000..7017a370 --- /dev/null +++ b/android-app/app/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.nervosnetwork.ckblightclient" + compileSdk = 34 + + defaultConfig { + applicationId = "com.nervosnetwork.ckblightclient" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "0.5.3" + + ndk { + abiFilters += listOf("arm64-v8a") + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.10.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + + // Fragments + implementation("androidx.fragment:fragment-ktx:1.6.2") + + // Lifecycle & Coroutines + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + + // JSON Parsing + implementation("com.google.code.gson:gson:2.10.1") + + // SwipeRefreshLayout + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") +} + +// Copy Rust JNI libraries from target directory to jniLibs +tasks.register("copyNativeLibrary") { + from("../../target/aarch64-linux-android/release/libckb_light_client_lib.so") { + into("arm64-v8a") + } + into("src/main/jniLibs") +} + +tasks.named("preBuild") { + dependsOn("copyNativeLibrary") +} diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..fc397948 --- /dev/null +++ b/android-app/app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/assets/mainnet.toml b/android-app/app/src/main/assets/mainnet.toml new file mode 100644 index 00000000..6a9ce9b8 --- /dev/null +++ b/android-app/app/src/main/assets/mainnet.toml @@ -0,0 +1,78 @@ +# chain = "mainnet" +# chain = "testnet" +# chain = "your_path_to/dev.toml" +chain = "mainnet" + + +[store] +path = "data/store" + +[network] +path = "data/network" + +listen_addresses = ["/ip4/0.0.0.0/tcp/8118"] +### Specify the public and routable network addresses +# public_addresses = [] + +# Node connects to nodes listed here to discovery other peers when there's no local stored peers. +# When chain.spec is changed, this usually should also be changed to the bootnodes in the new chain. +bootnodes = [ + # Hongkong, Asia + "/ip4/16.163.82.218/tcp/8114/p2p/QmaZMemLXSsxKUrYNucjEbPxVX3rBKsGhWW2muWtWxUWyh", + # Tokyo, Asia + "/ip4/35.79.196.111/tcp/8114/p2p/QmYCRVonLfP18LSoz2WCHaXDorUYxuUMfhtcXK1TuZ1iwF", + # Mumbai, Asia + "/ip4/13.234.144.148/tcp/8114/p2p/QmbT7QimcrcD5k2znoJiWpxoESxang6z1Gy9wof1rT1LKR", + # Seoul, Asia + "/ip4/34.64.120.143/tcp/8114/p2p/QmejEJEbDcGGMp4D6WtftMMVLkR1ZuBfMgyLFDMJymkDt6", + # Virginia, North America + "/ip4/3.218.170.86/tcp/8114/p2p/QmShw2vtVt49wJagc1zGQXGS6LkQTcHxnEV3xs6y8MAmQN", + # Los Angeles, North America + "/ip4/35.236.107.161/tcp/8114/p2p/QmSRj57aa9sR2AiTvMyrEea8n1sEM1cDTrfb2VHVJxnGuu", + # Texas, North America + "/ip4/23.101.191.12/tcp/8114/p2p/QmexvXVDiRt2FBGptgK4gBJusWyyTEEaHeuCAa35EPNkZS", + # Toronto, North America + "/ip4/20.151.143.237/tcp/8114/p2p/QmNsGNQjYA6iP472bNnNE2GR31kCYBifhY1XcaUxRjZ1py", + # Frankfurt, Europe + "/ip4/52.59.155.249/tcp/8114/p2p/QmRHqhSGMGm5FtnkW8D6T83X7YwaiMAZXCXJJaKzQEo3rb", + # London, Europe + "/ip4/3.10.216.39/tcp/8114/p2p/QmagxSv7GNwKXQE7mi1iDjFHghjUpbqjBgqSot7PmMJqHA", + # Paris, Europe + "/ip4/13.37.172.80/tcp/8114/p2p/QmXJg4iKbQzMpLhX75RyDn89Mv7N2H8vLePBR7kgZf6hYk", + # Warsaw, Europe + "/ip4/34.118.49.255/tcp/8114/p2p/QmeCzzVmSAU5LNYAeXhdJj8TCq335aJMqUxcvZXERBWdgS", + # Victoria, Oceania + "/ip4/40.115.75.216/tcp/8114/p2p/QmW3P1WYtuz9hitqctKnRZua2deHXhNePNjvtc9Qjnwp4q", + # Santiago, South America + "/ip4/34.176.239.95/tcp/8114/p2p/QmQoWrmuFauCn3zZ2mYYKAciG9opTbjzC2wVEfWveZNDt8", + # Capetown, Africa + "/ip4/13.245.217.98/tcp/8114/p2p/Qmf4t1SzFhRWuGcFcgs7r4pXvkACsz3FcaBMcmMKQMMpn7" +] + +### Whitelist-only mode +# whitelist_only = false +### Whitelist peers connecting from the given IP addresses +# whitelist_peers = [] + +### Enable `SO_REUSEPORT` feature to reuse port on Linux, not supported on other OS yet +# reuse_port_on_linux = true + +max_peers = 125 +max_outbound_peers = 8 +# 2 minutes +ping_interval_secs = 120 +# 20 minutes +ping_timeout_secs = 1200 +connect_outbound_interval_secs = 15 +# If set to true, try to register upnp +upnp = false +# If set to true, network service will add discovered local address to peer store, it's helpful for private net development +discovery_local_address = false +# If set to true, random cleanup when there are too many inbound nodes +# Ensure that itself can continue to serve as a bootnode node +bootnode_mode = false + +[rpc] +# Light client rpc is designed for self hosting, exposing to public network is not recommended and may cause security issues. +# By default RPC only binds to localhost, thus it only allows accessing from the same machine. +listen_address = "127.0.0.1:9000" diff --git a/android-app/app/src/main/java/com/nervosnetwork/ckblightclient/LightClientNative.kt b/android-app/app/src/main/java/com/nervosnetwork/ckblightclient/LightClientNative.kt new file mode 100644 index 00000000..75c2dd0e --- /dev/null +++ b/android-app/app/src/main/java/com/nervosnetwork/ckblightclient/LightClientNative.kt @@ -0,0 +1,250 @@ +package com.nervosnetwork.ckblightclient + +/** + * JNI bridge to CKB Light Client native library + * + * This object provides access to the native Rust implementation of CKB Light Client. + * The library is loaded from libckb_light_client_lib.so at initialization. + */ +object LightClientNative { + init { + System.loadLibrary("ckb_light_client_lib") + } + + // ======================================== + // Lifecycle Management + // ======================================== + + /** + * Initialize the light client + * + * @param configPath Absolute path to TOML config file + * @param logCallback Callback for log messages (batched every 100ms) + * @param statusCallback Callback for status changes + * @return true if initialization succeeded, false otherwise + */ + external fun nativeInit( + configPath: String, + logCallback: LogCallback, + statusCallback: StatusCallback + ): Boolean + + /** + * Start the light client + * + * Transitions from INIT to RUNNING state. + * Must call nativeInit() first. + * + * @return true if start succeeded, false otherwise + */ + external fun nativeStart(): Boolean + + /** + * Stop the light client + * + * Gracefully shuts down the light client. + * Broadcasts exit signals and waits for services to stop. + * + * @return true if stop succeeded, false otherwise + */ + external fun nativeStop(): Boolean + + /** + * Get current status + * + * @return Status code: STATUS_INIT (0), STATUS_RUNNING (1), or STATUS_STOPPED (2) + */ + external fun nativeGetStatus(): Int + + /** + * Set log filter using RUST_LOG format + * + * @param filter RUST_LOG-style filter string + * Examples: + * - "info" - all modules at info level + * - "debug" - all modules at debug level + * - "info,ckb_network=debug" - most at info, ckb_network at debug + * - "info,ckb_network=debug,ckb_sync=trace" - per-module levels + * @return true if successful, false otherwise + */ + external fun nativeSetLogFilter(filter: String): Boolean + + // ======================================== + // Query APIs (17 functions) + // ======================================== + + /** + * Get tip header + * @return JSON string of HeaderView, or null on error + */ + external fun nativeGetTipHeader(): String? + + /** + * Get genesis block + * @return JSON string of BlockView, or null on error + */ + external fun nativeGetGenesisBlock(): String? + + /** + * Get header by hash + * @param hash Block hash (hex string with 0x prefix) + * @return JSON string of HeaderView, or null if not found + */ + external fun nativeGetHeader(hash: String): String? + + /** + * Fetch header (with fetch status) + * @param hash Block hash (hex string with 0x prefix) + * @return JSON string of FetchStatus, or null on error + */ + external fun nativeFetchHeader(hash: String): String? + + /** + * Set filter scripts + * @param scriptsJson JSON array of ScriptStatus + * @param command Command type: 0=All, 1=Partial, 2=Delete + * @return true if succeeded, false otherwise + */ + external fun nativeSetScripts(scriptsJson: String, command: Int): Boolean + + /** + * Get filter scripts + * @return JSON array of ScriptStatus, or null on error + */ + external fun nativeGetScripts(): String? + + /** + * Get cells + * @param searchKeyJson JSON of SearchKey + * @param order "asc" or "desc" + * @param limit Maximum number of results + * @param cursor Pagination cursor (JSON), or null for first page + * @return JSON of Pagination, or null on error + */ + external fun nativeGetCells( + searchKeyJson: String, + order: String, + limit: Int, + cursor: String? + ): String? + + /** + * Get transactions + * @param searchKeyJson JSON of SearchKey + * @param order "asc" or "desc" + * @param limit Maximum number of results + * @param cursor Pagination cursor (JSON), or null for first page + * @return JSON of Pagination, or null on error + */ + external fun nativeGetTransactions( + searchKeyJson: String, + order: String, + limit: Int, + cursor: String? + ): String? + + /** + * Get cells capacity + * @param searchKeyJson JSON of SearchKey + * @return JSON of CellsCapacity, or null on error + */ + external fun nativeGetCellsCapacity(searchKeyJson: String): String? + + /** + * Send transaction + * @param txJson JSON of Transaction + * @return Transaction hash (hex string), or null on error + */ + external fun nativeSendTransaction(txJson: String): String? + + /** + * Get transaction + * @param hash Transaction hash (hex string with 0x prefix) + * @return JSON of TransactionWithStatus, or null if not found + */ + external fun nativeGetTransaction(hash: String): String? + + /** + * Fetch transaction (with fetch status) + * @param hash Transaction hash (hex string with 0x prefix) + * @return JSON of FetchStatus, or null on error + */ + external fun nativeFetchTransaction(hash: String): String? + + /** + * Get local node info + * @return JSON of LocalNode, or null on error + */ + external fun nativeLocalNodeInfo(): String? + + /** + * Get peers + * @return JSON array of RemoteNode, or null on error + */ + external fun nativeGetPeers(): String? + + /** + * Estimate transaction cycles + * @param txJson JSON of Transaction + * @return JSON of EstimateCycles, or null on error + */ + external fun nativeEstimateCycles(txJson: String): String? + + // ======================================== + // Callback Interfaces + // ======================================== + + /** + * Log callback interface + * + * Receives batched log messages every 100ms. + * Called from native thread, so must be thread-safe. + */ + interface LogCallback { + /** + * Called when log messages are available + * + * @param level Log level ("ERROR", "WARN", "INFO", "DEBUG", "TRACE") + * @param messages Array of log messages (batched) + */ + fun onLog(level: String, messages: Array) + } + + /** + * Status callback interface + * + * Receives status change notifications. + * Called from native thread, so must be thread-safe. + */ + interface StatusCallback { + /** + * Called when status changes + * + * @param status Status name ("initialized", "running", "stopped", etc.) + * @param data Additional data (usually empty) + */ + fun onStatusChange(status: String, data: String) + } + + // ======================================== + // Constants + // ======================================== + + /** Status: Initialized but not started */ + const val STATUS_INIT = 0 + + /** Status: Running */ + const val STATUS_RUNNING = 1 + + /** Status: Stopped */ + const val STATUS_STOPPED = 2 + + /** SetScripts command: Replace all scripts */ + const val CMD_SET_SCRIPTS_ALL = 0 + + /** SetScripts command: Partial update */ + const val CMD_SET_SCRIPTS_PARTIAL = 1 + + /** SetScripts command: Delete scripts */ + const val CMD_SET_SCRIPTS_DELETE = 2 +} diff --git a/android-app/app/src/main/java/com/nervosnetwork/ckblightclient/LightClientService.kt b/android-app/app/src/main/java/com/nervosnetwork/ckblightclient/LightClientService.kt new file mode 100644 index 00000000..2905fd81 --- /dev/null +++ b/android-app/app/src/main/java/com/nervosnetwork/ckblightclient/LightClientService.kt @@ -0,0 +1,273 @@ +package com.nervosnetwork.ckblightclient + +import android.app.* +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import java.io.* + +class LightClientService : Service() { + private var isRunning = false + private var isStarting = false + private val TAG = "LightClientService" + + companion object { + const val CHANNEL_ID = "ckb_light_client_channel" + const val NOTIFICATION_ID = 1 + const val ACTION_START = "com.nervosnetwork.ckblightclient.START" + const val ACTION_STOP = "com.nervosnetwork.ckblightclient.STOP" + const val MAX_LOG_LINES = 5000 + + var logCallback: ((String) -> Unit)? = null + + // Persistent log buffer that survives fragment lifecycle + private val logBuffer = ArrayList() + + fun getAllLogs(): List { + synchronized(logBuffer) { + return ArrayList(logBuffer) + } + } + + fun clearLogs() { + synchronized(logBuffer) { + logBuffer.clear() + } + } + + private fun addLogLine(line: String) { + synchronized(logBuffer) { + logBuffer.add(line) + // Keep only last 5000 lines + if (logBuffer.size > MAX_LOG_LINES) { + logBuffer.removeAt(0) + } + } + } + } + + // JNI callbacks + private val nativeLogCallback = object : LightClientNative.LogCallback { + override fun onLog(level: String, messages: Array) { + val timestamp = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()) + .format(java.util.Date()) + messages.forEach { msg -> + val formatted = "[$timestamp] [$level] $msg" + Log.d(TAG, formatted) + addLogLine(formatted) + logCallback?.invoke(formatted) + } + } + } + + private val nativeStatusCallback = object : LightClientNative.StatusCallback { + override fun onStatusChange(status: String, data: String) { + Log.i(TAG, "Status changed: $status") + when (status) { + "initialized" -> updateNotification("Initialized") + "running" -> updateNotification("Running") + "stopped" -> updateNotification("Stopped") + else -> updateNotification("Status: $status") + } + } + } + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + ACTION_START -> { + startForegroundService() + startLightClient() + } + ACTION_STOP -> { + stopLightClient() + stopSelf() + } + } + return START_STICKY + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Log.d(TAG, "Creating notification channel for Android O+") + val channel = NotificationChannel( + CHANNEL_ID, + "CKB Light Client Service", + NotificationManager.IMPORTANCE_LOW + ).apply { + description = "Running CKB Light Client in background" + } + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + Log.d(TAG, "Notification channel created successfully") + } else { + Log.d(TAG, "Android version < O, no notification channel needed") + } + } + + private fun startForegroundService() { + val notification = createNotification("CKB Light Client is starting...") + + Log.d(TAG, "Starting foreground service with notification") + appendLog("Starting foreground service...") + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } else { + startForeground(NOTIFICATION_ID, notification) + } + Log.d(TAG, "Foreground service started successfully") + appendLog("Foreground service started - notification should be visible") + } catch (e: Exception) { + Log.e(TAG, "Failed to start foreground service", e) + appendLog("ERROR starting foreground service: ${e.message}") + } + } + + private fun createNotification(contentText: String): Notification { + val notificationIntent = Intent(this, MainActivity::class.java) + val pendingIntent = PendingIntent.getActivity( + this, 0, notificationIntent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("CKB Light Client") + .setContentText(contentText) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentIntent(pendingIntent) + .setOngoing(true) + .build() + } + + private fun updateNotification(text: String) { + val notification = createNotification(text) + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.notify(NOTIFICATION_ID, notification) + } + + private fun startLightClient() { + if (isRunning || isStarting) { + appendLog("Already running or starting, please wait...") + return + } + + isStarting = true + updateNotification("CKB Light Client is starting...") + + Thread { + try { + val configPath = File(filesDir, MainActivity.CONFIG_NAME).absolutePath + + appendLog("---") + appendLog("Starting CKB Light Client via JNI (background thread)...") + appendLog("Config: $configPath") + appendLog("---") + + // Initialize native light client + val initSuccess = LightClientNative.nativeInit( + configPath, + nativeLogCallback, + nativeStatusCallback + ) + + if (!initSuccess) { + appendLog("ERROR: Native initialization failed") + stopSelf() + return@Thread + } + + appendLog("Native initialization succeeded") + + // Apply saved RUST_LOG filter + val prefs = getSharedPreferences("CKBLightClientPrefs", Context.MODE_PRIVATE) + val savedFilter = prefs.getString("rust_log_filter", "TRACE") ?: "TRACE" + val filterValue = savedFilter.lowercase() + LightClientNative.nativeSetLogFilter(filterValue) + appendLog("Applied log filter: $savedFilter") + + // Start the light client + val startSuccess = LightClientNative.nativeStart() + + if (startSuccess) { + isRunning = true + appendLog("Light Client started successfully!") + updateNotification("CKB Light Client is running") + } else { + appendLog("ERROR: Native start failed") + stopSelf() + } + + } catch (e: Exception) { + appendLog("ERROR: ${e.message}") + Log.e(TAG, "Start error", e) + isRunning = false + stopSelf() + } finally { + isStarting = false + } + }.start() + } + + private fun stopLightClient() { + if (isStarting) { + appendLog("Still starting, please wait before stopping.") + return + } + + if (!isRunning) { + appendLog("Not running!") + return + } + + appendLog("---") + appendLog("Stopping CKB Light Client...") + updateNotification("CKB Light Client is stopping...") + + // Run stop on background thread to avoid ANR (wait_all_ckb_services_exit blocks) + Thread { + try { + val stopSuccess = LightClientNative.nativeStop() + if (stopSuccess) { + appendLog("Light Client stopped successfully") + } else { + appendLog("WARNING: Native stop returned false") + } + appendLog("---") + } catch (e: Exception) { + appendLog("ERROR: ${e.message}") + Log.e(TAG, "Stop error", e) + } finally { + isRunning = false + } + }.start() + } + + private fun appendLog(message: String) { + val timestamp = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()) + .format(java.util.Date()) + val logLine = "[$timestamp] $message" + + Log.d(TAG, message) + addLogLine(logLine) + logCallback?.invoke(logLine) + } + + override fun onDestroy() { + super.onDestroy() + stopLightClient() + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } +} diff --git a/android-app/app/src/main/java/com/nervosnetwork/ckblightclient/MainActivity.kt b/android-app/app/src/main/java/com/nervosnetwork/ckblightclient/MainActivity.kt new file mode 100644 index 00000000..f888301e --- /dev/null +++ b/android-app/app/src/main/java/com/nervosnetwork/ckblightclient/MainActivity.kt @@ -0,0 +1,148 @@ +package com.nervosnetwork.ckblightclient + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.nervosnetwork.ckblightclient.fragments.LogsFragment +import com.nervosnetwork.ckblightclient.fragments.StatusFragment +import java.io.File + +class MainActivity : AppCompatActivity() { + private lateinit var bottomNavigation: BottomNavigationView + private val TAG = "CKBLightClient" + + companion object { + const val CONFIG_NAME = "mainnet.toml" + private const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1001 + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + bottomNavigation = findViewById(R.id.bottom_navigation) + + // Request notification permission for Android 13+ + requestNotificationPermission() + + // Setup binary and config + setupBinaryAndConfig() + + // Load default fragment + if (savedInstanceState == null) { + loadFragment(LogsFragment()) + } + + // Setup bottom navigation + bottomNavigation.setOnItemSelectedListener { item -> + when (item.itemId) { + R.id.nav_logs -> { + loadFragment(LogsFragment()) + true + } + R.id.nav_status -> { + loadFragment(StatusFragment()) + true + } + else -> false + } + } + } + + private fun requestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + NOTIFICATION_PERMISSION_REQUEST_CODE + ) + } + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when (requestCode) { + NOTIFICATION_PERMISSION_REQUEST_CODE -> { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Notification permission granted") + } else { + Log.w(TAG, "Notification permission denied - foreground service notification will not show") + } + } + } + } + + private fun loadFragment(fragment: Fragment) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, fragment) + .commit() + } + + private fun setupBinaryAndConfig() { + try { + // JNI mode: no need to extract binary, only setup config + setupConfig() + copySharedLibrary("libc++_shared.so") + } catch (e: Exception) { + Log.e(TAG, "Setup error", e) + } + } + + private fun copySharedLibrary(libName: String): File { + val nativeLibDir = applicationInfo.nativeLibraryDir + val sourceLib = File(nativeLibDir, libName) + val destLib = File(filesDir, libName) + + if (!destLib.exists() || sourceLib.lastModified() > destLib.lastModified()) { + sourceLib.copyTo(destLib, overwrite = true) + destLib.setReadable(true, false) + } + + return destLib + } + + private fun setupConfig(): File { + val configFile = File(filesDir, CONFIG_NAME) + + if (configFile.exists()) { + val existing = runCatching { configFile.readText() }.getOrNull() + if (existing != null && existing.contains("logger", ignoreCase = true)) { + runCatching { + configFile.copyTo(File(configFile.parentFile, "$CONFIG_NAME.bak"), overwrite = true) + } + configFile.delete() + } else { + return configFile + } + } + + val configTemplate = assets.open(CONFIG_NAME).bufferedReader().use { it.readText() } + val dataDir = File(filesDir, "data") + dataDir.mkdirs() + + val modifiedConfig = configTemplate + .replace("path = \"data/store\"", "path = \"${File(dataDir, "store").absolutePath}\"") + .replace("path = \"data/network\"", "path = \"${File(dataDir, "network").absolutePath}\"") + + configFile.writeText(modifiedConfig) + + return configFile + } +} diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..d82d1809 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/android-app/app/src/main/res/values/colors.xml b/android-app/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..824d13bb --- /dev/null +++ b/android-app/app/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ + + + #2D9CDB + #1B74B8 + #27AE60 + #FFFFFF + #FFFFFF + #FFFFFF + diff --git a/android-app/app/src/main/res/values/styles.xml b/android-app/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..d518fa4c --- /dev/null +++ b/android-app/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + diff --git a/android-app/build.gradle.kts b/android-app/build.gradle.kts new file mode 100644 index 00000000..3a6ec8d6 --- /dev/null +++ b/android-app/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("com.android.application") version "8.2.0" apply false + id("org.jetbrains.kotlin.android") version "1.9.20" apply false +} diff --git a/android-app/gradle.properties b/android-app/gradle.properties new file mode 100644 index 00000000..646c51b9 --- /dev/null +++ b/android-app/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +android.enableJetifier=true diff --git a/android-app/gradle/wrapper/gradle-wrapper.jar b/android-app/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..61285a65 Binary files /dev/null and b/android-app/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android-app/gradle/wrapper/gradle-wrapper.properties b/android-app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a5952066 --- /dev/null +++ b/android-app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android-app/gradlew b/android-app/gradlew new file mode 100755 index 00000000..d9596441 --- /dev/null +++ b/android-app/gradlew @@ -0,0 +1,189 @@ +#!/bin/sh + +############################################################################## +# Gradle start up script for UN*X +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname -s )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android-app/settings.gradle.kts b/android-app/settings.gradle.kts new file mode 100644 index 00000000..eac68e27 --- /dev/null +++ b/android-app/settings.gradle.kts @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "CKB Light Client" +include(":app") diff --git a/android-stubs.c b/android-stubs.c new file mode 100644 index 00000000..32b3c666 --- /dev/null +++ b/android-stubs.c @@ -0,0 +1,21 @@ +/* Android stubs for missing libc functions */ +/* These functions are not available in Android's Bionic libc */ +/* UPnP functionality will be disabled on Android */ + +#include +#include +#include + +/* Stub implementation of getifaddrs for Android */ +/* Returns ENOSYS (function not implemented) */ +int getifaddrs(struct ifaddrs **ifap) { + (void)ifap; + errno = ENOSYS; + return -1; +} + +/* Stub implementation of freeifaddrs for Android */ +void freeifaddrs(struct ifaddrs *ifa) { + (void)ifa; + /* No-op since getifaddrs always fails */ +} diff --git a/build-android-jni.sh b/build-android-jni.sh new file mode 100755 index 00000000..1bb53ad7 --- /dev/null +++ b/build-android-jni.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# Build script for CKB Light Client JNI (Android) +# +# Builds for multiple Android architectures: arm64-v8a, armeabi-v7a, x86_64 + +set -e + +echo "======================================" +echo "CKB Light Client - Android JNI Build" +echo "======================================" +echo "" + +# Try to find Android NDK if not already set +if [ -z "$ANDROID_NDK_HOME" ] || [ ! -d "$ANDROID_NDK_HOME" ]; then + echo "ANDROID_NDK_HOME not set or invalid, searching for NDK..." + # Try common locations + for ndk_path in \ + "$ANDROID_HOME/ndk/"* \ + "$HOME/Android/Sdk/ndk/"* \ + "/usr/local/android-ndk"* \ + "$HOME/soft/ndk/"* \ + "$HOME/android-ndk"*; do + if [ -d "$ndk_path/toolchains/llvm/prebuilt" ]; then + export ANDROID_NDK_HOME="$ndk_path" + echo "Found NDK at: $ANDROID_NDK_HOME" + break + fi + done +fi + +# Check if NDK is available +if [ -z "$ANDROID_NDK_HOME" ] || [ ! -d "$ANDROID_NDK_HOME" ]; then + echo "ERROR: Android NDK not found!" + echo "" + echo "Please install Android NDK and set ANDROID_NDK_HOME environment variable." + echo "" + echo "To install NDK:" + echo " 1. Install Android Studio from https://developer.android.com/studio" + echo " 2. Open SDK Manager: Tools > SDK Manager" + echo " 3. Go to SDK Tools tab" + echo " 4. Check 'NDK (Side by side)' and click Apply" + echo "" + echo "Then set ANDROID_NDK_HOME:" + echo " export ANDROID_NDK_HOME=\$HOME/Android/Sdk/ndk/" + echo "" + echo "Or download standalone NDK from:" + echo " https://developer.android.com/ndk/downloads" + echo "" + exit 1 +fi + +# Detect host platform +if [ -d "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64" ]; then + NDK_HOST="linux-x86_64" +elif [ -d "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64" ]; then + NDK_HOST="darwin-x86_64" +else + echo "ERROR: Could not detect NDK host platform" + exit 1 +fi + +export ANDROID_API_LEVEL=21 +NDK_BIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/$NDK_HOST/bin" + +echo "Using NDK: $ANDROID_NDK_HOME" +echo "NDK Host: $NDK_HOST" +echo "API Level: $ANDROID_API_LEVEL" +echo "" + +# Check if the required tools are available +if [ ! -f "$NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang" ]; then + echo "ERROR: NDK toolchain not found!" + echo "Expected: $NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang" + exit 1 +fi + +# Add Rust Android targets +echo "Adding Rust Android targets..." +rustup target add aarch64-linux-android + +# Configure environment for Android cross-compilation +export CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang" +export CXX_aarch64_linux_android="$NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang++" +export AR_aarch64_linux_android="$NDK_BIN/llvm-ar" +export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang" + +# RocksDB and build configuration +SYSROOT="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/$NDK_HOST/sysroot" +ROCKSDB_DISABLE_FLAGS="-DROCKSDB_NO_DYNAMIC_EXTENSION" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DROCKSDB_LITE" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -Dposix_madvise=madvise" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -Dfread_unlocked=fread" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DPOSIX_MADV_NORMAL=MADV_NORMAL" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DPOSIX_MADV_RANDOM=MADV_RANDOM" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DPOSIX_MADV_SEQUENTIAL=MADV_SEQUENTIAL" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DPOSIX_MADV_WILLNEED=MADV_WILLNEED" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DPOSIX_MADV_DONTNEED=MADV_DONTNEED" + +export CXXFLAGS="-fPIC -D__ANDROID_API__=${ANDROID_API_LEVEL} $ROCKSDB_DISABLE_FLAGS" +export CFLAGS="-fPIC -D__ANDROID_API__=${ANDROID_API_LEVEL}" + +export ROCKSDB_DISABLE_FALLOCATE=1 +export ROCKSDB_DISABLE_SYNC_FILE_RANGE=1 +export ROCKSDB_DISABLE_PTHREAD_MUTEX_ADAPTIVE_NP=1 +export ROCKSDB_DISABLE_SCHED_GETCPU=1 +export ROCKSDB_DISABLE_AUXV=1 + +# BINDGEN_EXTRA_CLANG_ARGS is set per-target in the build loop + +# Set LD_LIBRARY_PATH for Nix environment +if [ -n "$NIX_LD_LIBRARY_PATH" ]; then + export LD_LIBRARY_PATH="$NIX_LD_LIBRARY_PATH" +fi + +# Compile Android stubs for missing libc functions +echo "Compiling Android compatibility stubs..." +STUBS_DIR="$(pwd)/target/android-stubs" +mkdir -p "$STUBS_DIR" + +# Check if stubs file exists +if [ -f "android-stubs.c" ]; then + $CC_aarch64_linux_android -c android-stubs.c -o "$STUBS_DIR/android-stubs.o" + $AR_aarch64_linux_android rcs "$STUBS_DIR/libandroid_stubs.a" "$STUBS_DIR/android-stubs.o" + export RUSTFLAGS="-L $STUBS_DIR -l static=android_stubs" +else + echo "Note: android-stubs.c not found, skipping stubs compilation" + export RUSTFLAGS="" +fi + +echo "" +echo "Building for Android arm64-v8a..." +echo "" + +TARGET=aarch64-linux-android +echo "======================================" +echo "Building for $TARGET" +echo "======================================" + +# Set target-specific bindgen args +export BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$SYSROOT --target=$TARGET -D__ANDROID_API__=${ANDROID_API_LEVEL}" + +cargo build --release \ + --target $TARGET \ + --package ckb-light-client-lib \ + --features jni-bridge,portable + +echo "" + +echo "" +echo "======================================" +echo "Build completed successfully!" +echo "======================================" +echo "" +echo "Output file:" +echo " arm64-v8a: target/aarch64-linux-android/release/libckb_light_client_lib.so" +echo "" + +# Show file size +SO_FILE="target/aarch64-linux-android/release/libckb_light_client_lib.so" +if [ -f "$SO_FILE" ]; then + SIZE=$(ls -lh "$SO_FILE" | awk '{print $5}') + echo "File size: $SIZE" +fi +echo "" diff --git a/build-android.sh b/build-android.sh new file mode 100755 index 00000000..e38743ed --- /dev/null +++ b/build-android.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# Build script for CKB Light Client on Android (aarch64-linux-android) + +set -e + +echo "======================================" +echo "CKB Light Client - Android Build" +echo "======================================" +echo "" + +# Set default NDK path if not already set +if [ -z "$ANDROID_NDK_HOME" ]; then + export ANDROID_NDK_HOME=/home/exec/soft/ndk/android-ndk-r27d + echo "Using default NDK path: $ANDROID_NDK_HOME" +fi + +# Try to find Android NDK +if [ -z "$ANDROID_NDK_HOME" ] || [ ! -d "$ANDROID_NDK_HOME" ]; then + # Try common locations + for ndk_path in \ + "$HOME/Android/Sdk/ndk/"* \ + "$ANDROID_HOME/ndk/"* \ + "/usr/local/android-ndk" \ + "$HOME/android-ndk"*; do + if [ -d "$ndk_path/toolchains/llvm/prebuilt" ]; then + ANDROID_NDK_HOME="$ndk_path" + echo "Found NDK at: $ANDROID_NDK_HOME" + break + fi + done +fi + +# Check if NDK is available +if [ -z "$ANDROID_NDK_HOME" ] || [ ! -d "$ANDROID_NDK_HOME" ]; then + echo "ERROR: Android NDK not found!" + echo "" + echo "Please install Android NDK and set ANDROID_NDK_HOME environment variable." + echo "" + echo "To install NDK:" + echo " 1. Install Android Studio from https://developer.android.com/studio" + echo " 2. Open SDK Manager: Tools > SDK Manager" + echo " 3. Go to SDK Tools tab" + echo " 4. Check 'NDK (Side by side)' and click Apply" + echo "" + echo "Then set ANDROID_NDK_HOME:" + echo " export ANDROID_NDK_HOME=\$HOME/Android/Sdk/ndk/" + echo "" + echo "Or download standalone NDK from:" + echo " https://developer.android.com/ndk/downloads" + echo "" + exit 1 +fi + +# Detect host platform +if [ -d "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64" ]; then + NDK_HOST="linux-x86_64" +elif [ -d "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64" ]; then + NDK_HOST="darwin-x86_64" +else + echo "ERROR: Could not detect NDK host platform" + exit 1 +fi + +export ANDROID_API_LEVEL=21 +NDK_BIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/$NDK_HOST/bin" + +echo "Using NDK: $ANDROID_NDK_HOME" +echo "NDK Host: $NDK_HOST" +echo "API Level: $ANDROID_API_LEVEL" +echo "" + +# Check if the required tools are available +if [ ! -f "$NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang" ]; then + echo "ERROR: NDK toolchain not found!" + echo "Expected: $NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang" + exit 1 +fi + +# Set up environment for cross-compilation +# Use full paths to avoid affecting host build scripts +export CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang" +export CXX_aarch64_linux_android="$NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang++" +export AR_aarch64_linux_android="$NDK_BIN/llvm-ar" +export AS_aarch64_linux_android="$NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang" +export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang" +export CARGO_TARGET_AARCH64_LINUX_ANDROID_AR="$NDK_BIN/llvm-ar" + +# Force static linking and vendored dependencies +export OPENSSL_STATIC=1 +export OPENSSL_INCLUDE_DIR="" +export OPENSSL_LIB_DIR="" + +# OpenSSL specific configuration for Android +export OPENSSL_TARGET="android-arm64" +export OPENSSL_COMPILER="$NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang" + +# RocksDB and bindgen configuration for Android +SYSROOT="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/$NDK_HOST/sysroot" + +# Disable Linux-specific features that don't exist on Android +# These defines tell RocksDB to not use Android-incompatible functions +ROCKSDB_DISABLE_FLAGS="-DROCKSDB_NO_DYNAMIC_EXTENSION" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DROCKSDB_LITE" # Use lite version for mobile + +# Stub out missing POSIX functions for Android +# Android's Bionic libc doesn't have these, so we define them as no-ops +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -Dposix_madvise=madvise" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -Dfread_unlocked=fread" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DPOSIX_MADV_NORMAL=MADV_NORMAL" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DPOSIX_MADV_RANDOM=MADV_RANDOM" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DPOSIX_MADV_SEQUENTIAL=MADV_SEQUENTIAL" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DPOSIX_MADV_WILLNEED=MADV_WILLNEED" +ROCKSDB_DISABLE_FLAGS="$ROCKSDB_DISABLE_FLAGS -DPOSIX_MADV_DONTNEED=MADV_DONTNEED" + +export CXXFLAGS="-fPIC -D__ANDROID_API__=${ANDROID_API_LEVEL} $ROCKSDB_DISABLE_FLAGS" +export CFLAGS="-fPIC -D__ANDROID_API__=${ANDROID_API_LEVEL}" + +# Tell RocksDB build script that Android doesn't have these POSIX features +export ROCKSDB_DISABLE_FALLOCATE=1 +export ROCKSDB_DISABLE_SYNC_FILE_RANGE=1 +export ROCKSDB_DISABLE_PTHREAD_MUTEX_ADAPTIVE_NP=1 +export ROCKSDB_DISABLE_SCHED_GETCPU=1 +export ROCKSDB_DISABLE_AUXV=1 + +# Configure bindgen to use Android headers (use system libclang) +export CLANG_PATH="$NDK_BIN/aarch64-linux-android${ANDROID_API_LEVEL}-clang" +export BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$SYSROOT --target=aarch64-linux-android -D__ANDROID_API__=${ANDROID_API_LEVEL}" + +# Set LD_LIBRARY_PATH for Nix environment +if [ -n "$NIX_LD_LIBRARY_PATH" ]; then + export LD_LIBRARY_PATH="$NIX_LD_LIBRARY_PATH" +fi + +echo "Environment configured for Android cross-compilation" +echo "" +echo "Building for target: aarch64-linux-android" +echo "This may take several minutes on first build..." +echo "" + +# Compile Android stubs for missing libc functions +echo "Compiling Android compatibility stubs..." +STUBS_DIR="$(pwd)/target/android-stubs" +mkdir -p "$STUBS_DIR" +$CC_aarch64_linux_android -c android-stubs.c -o "$STUBS_DIR/android-stubs.o" +$AR_aarch64_linux_android rcs "$STUBS_DIR/libandroid_stubs.a" "$STUBS_DIR/android-stubs.o" + +# Set up linker flags to use our stubs +export RUSTFLAGS="-L $STUBS_DIR -l static=android_stubs" + +# Build with portable feature to disable CPU-specific optimizations for ARM64 +cargo build --release --target aarch64-linux-android --features portable + +echo "" +echo "======================================" +echo "Build completed successfully!" +echo "======================================" +echo "" +echo "Binary location:" +echo " target/aarch64-linux-android/release/ckb-light-client" +echo "" +echo "Binary size:" +ls -lh target/aarch64-linux-android/release/ckb-light-client | awk '{print " " $5}' +echo "" +echo "Verify binary:" +echo " file target/aarch64-linux-android/release/ckb-light-client" +echo "" diff --git a/light-client-bin/Cargo.toml b/light-client-bin/Cargo.toml index 6772c556..ab3cb299 100644 --- a/light-client-bin/Cargo.toml +++ b/light-client-bin/Cargo.toml @@ -33,9 +33,12 @@ rocksdb = { package = "ckb-rocksdb", version = "=0.21.1", features = [ env_logger = "0.11" anyhow = "1.0.56" -[target.'cfg(not(target_env = "msvc"))'.dependencies] +[target.'cfg(all(not(target_env = "msvc"), not(target_os = "android")))'.dependencies] tikv-jemallocator = "0.6" +[target.'cfg(target_os = "android")'.dependencies] +openssl = { version = "0.10", features = ["vendored"] } + [build-dependencies] vergen-gitcl = { version = "1", default-features = false } chrono = "0.4" diff --git a/light-client-bin/src/main.rs b/light-client-bin/src/main.rs index 4858383d..cb8c3d7d 100644 --- a/light-client-bin/src/main.rs +++ b/light-client-bin/src/main.rs @@ -9,7 +9,7 @@ mod tests; use cli::AppConfig; use env_logger::{Builder, Env, Target}; -#[cfg(not(target_env = "msvc"))] +#[cfg(all(not(target_env = "msvc"), not(target_os = "android")))] #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; diff --git a/light-client-bin/src/rpc.rs b/light-client-bin/src/rpc.rs index 6522057f..5f2e282a 100644 --- a/light-client-bin/src/rpc.rs +++ b/light-client-bin/src/rpc.rs @@ -968,11 +968,10 @@ impl Service { storage: Storage, peers: Arc, pending_txs: Arc>, - consensus: Consensus, + consensus: Arc, ) -> Server { let mut io_handler = IoHandler::new(); let swc = StorageWithChainData::new(storage, Arc::clone(&peers), Arc::clone(&pending_txs)); - let consensus = Arc::new(consensus); let block_filter_rpc_impl = BlockFilterRpcImpl { swc: swc.clone() }; let chain_rpc_impl = ChainRpcImpl { swc: swc.clone(), diff --git a/light-client-bin/src/subcmds.rs b/light-client-bin/src/subcmds.rs index 2441c9ef..1612e124 100644 --- a/light-client-bin/src/subcmds.rs +++ b/light-client-bin/src/subcmds.rs @@ -1,23 +1,10 @@ -use std::sync::{Arc, RwLock}; - -use ckb_async_runtime::new_global_runtime; -use ckb_chain_spec::ChainSpec; -use ckb_network::{ - network::TransportType, tokio, CKBProtocol, CKBProtocolHandler, Flags, NetworkService, - NetworkState, SupportProtocols, -}; -use ckb_resource::Resource; +use ckb_network::tokio; use ckb_stop_handler::{broadcast_exit_signals, wait_all_ckb_services_exit}; use log::debug; use ckb_light_client_lib::{ error::{Error, Result}, - protocols::{ - FilterProtocol, LightClientProtocol, Peers, PendingTxs, RelayProtocol, SyncProtocol, - BAD_MESSAGE_ALLOWED_EACH_HOUR, CHECK_POINT_INTERVAL, - }, - storage::Storage, - utils, + runtime::StartedLightClient, }; use crate::{cli::RunConfig, rpc::Service}; @@ -26,107 +13,16 @@ impl RunConfig { pub(crate) fn execute(self) -> Result<()> { log::info!("Run ..."); - utils::fs::need_directory(&self.run_env.network.path)?; - - let storage = Storage::new(&self.run_env.store.path); - let chain_spec = ChainSpec::load_from(&match self.run_env.chain.as_str() { - "mainnet" => Resource::bundled("specs/mainnet.toml".to_string()), - "testnet" => Resource::bundled("specs/testnet.toml".to_string()), - path => Resource::file_system(path.into()), - }) - .expect("load spec should be OK"); - let consensus = chain_spec - .build_consensus() - .expect("build consensus should be OK"); - storage.init_genesis_block(consensus.genesis_block().data()); - - // Cleanup any invalid matched blocks from previous runs (e.g., uncle blocks from chain reorgs) - log::info!("Cleaning up invalid matched blocks..."); - storage.cleanup_invalid_matched_blocks(); - - let pending_txs = Arc::new(RwLock::new(PendingTxs::default())); - let max_outbound_peers = self.run_env.network.max_outbound_peers; - let network_state = NetworkState::from_config(self.run_env.network) - .map(|network_state| { - Arc::new(network_state.required_flags( - Flags::DISCOVERY - | Flags::SYNC - | Flags::RELAY - | Flags::LIGHT_CLIENT - | Flags::BLOCK_FILTER, - )) - }) - .map_err(|err| { - let errmsg = format!("failed to initialize network state since {}", err); - Error::runtime(errmsg) - })?; - let required_protocol_ids = vec![ - SupportProtocols::Sync.protocol_id(), - SupportProtocols::LightClient.protocol_id(), - SupportProtocols::Filter.protocol_id(), - ]; - - let peers = Arc::new(Peers::new( - max_outbound_peers, - CHECK_POINT_INTERVAL, - storage.get_last_check_point(), - BAD_MESSAGE_ALLOWED_EACH_HOUR, - )); - let sync_protocol = SyncProtocol::new(storage.clone(), Arc::clone(&peers)); - let relay_protocol = - RelayProtocol::new(pending_txs.clone(), Arc::clone(&peers), storage.clone()); - let light_client: Box = Box::new(LightClientProtocol::new( - storage.clone(), - Arc::clone(&peers), - consensus.clone(), - )); - let filter_protocol = FilterProtocol::new(storage.clone(), Arc::clone(&peers)); - - let protocols = vec![ - CKBProtocol::new_with_support_protocol( - SupportProtocols::Sync, - Box::new(sync_protocol), - Arc::clone(&network_state), - ), - CKBProtocol::new_with_support_protocol( - SupportProtocols::RelayV3, - Box::new(relay_protocol), - Arc::clone(&network_state), - ), - CKBProtocol::new_with_support_protocol( - SupportProtocols::LightClient, - light_client, - Arc::clone(&network_state), - ), - CKBProtocol::new_with_support_protocol( - SupportProtocols::Filter, - Box::new(filter_protocol), - Arc::clone(&network_state), - ), - ]; - - let (mut handle, mut handle_stop_rx, _stop_handler) = new_global_runtime(None); - - let network_controller = NetworkService::new( - Arc::clone(&network_state), - protocols, - required_protocol_ids, - ( - consensus.identify_name(), - clap::crate_version!().to_owned(), - Flags::DISCOVERY, - ), - // Usually native light-client only connects to peers through TCP - TransportType::Tcp, - ) - .start(&handle) - .map_err(|err| { - let errmsg = format!("failed to start network since {}", err); - Error::runtime(errmsg) - })?; + let mut client = StartedLightClient::start(self.run_env.clone())?; let service = Service::new(&self.run_env.rpc.listen_address); - let rpc_server = service.start(network_controller, storage, peers, pending_txs, consensus); + let rpc_server = service.start( + client.network_controller(), + client.storage(), + client.peers(), + client.pending_txs(), + client.consensus(), + ); ctrlc::set_handler(move || { broadcast_exit_signals(); @@ -138,12 +34,12 @@ impl RunConfig { wait_all_ckb_services_exit(); - handle.drop_guard(); + client.runtime_handle().drop_guard(); rpc_server.close(); tokio::task::block_in_place(|| { debug!("Waiting all tokio tasks finished ..."); - handle_stop_rx.blocking_recv(); + client.stop_receiver().blocking_recv(); }); Ok(()) diff --git a/light-client-lib/Cargo.toml b/light-client-lib/Cargo.toml index 9860b851..7eb05cae 100644 --- a/light-client-lib/Cargo.toml +++ b/light-client-lib/Cargo.toml @@ -9,7 +9,11 @@ homepage = "https://github.com/nervosnetwork/ckb-light-client" repository = "https://github.com/nervosnetwork/ckb-light-client" [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib", "rlib", "staticlib"] + +[features] +jni-bridge = [] +portable = ["rocksdb/portable"] [dependencies] ckb-app-config = "1" @@ -47,11 +51,18 @@ anyhow = "1.0.56" thiserror = "1.0.30" toml = "0.5.8" tokio = { version = "1.20" } +serde_json = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rocksdb = { package = "ckb-rocksdb", version = "=0.21.1", features = [ "snappy", ], default-features = false } +ckb-async-runtime = "1" +ckb-stop-handler = "1" + +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.21" +android_logger = "0.13" [target.'cfg(target_arch = "wasm32")'.dependencies] web-time = "1.1.0" @@ -61,7 +72,6 @@ serde-wasm-bindgen = "0.6.5" light-client-db-common = { path = "../wasm/light-client-db-common" } web-sys = "0.3.72" console_error_panic_hook = { version = "0.1.7" } -serde_json = "1.0.134" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.45" diff --git a/light-client-lib/src/jni_bridge/callbacks.rs b/light-client-lib/src/jni_bridge/callbacks.rs new file mode 100644 index 00000000..504a3e45 --- /dev/null +++ b/light-client-lib/src/jni_bridge/callbacks.rs @@ -0,0 +1,247 @@ +//! JVM callback implementations +//! +//! Provides log and status callbacks with 100ms batching for efficiency. + +use super::types::{JAVA_VM, LOG_CALLBACK, STATUS_CALLBACK}; +use jni::objects::{JObject, JValue}; +use jni::sys::jobjectArray; +use jni::JNIEnv; +use log::{Level, LevelFilter, Log, Metadata, Record}; +use std::sync::{Mutex, RwLock}; +use std::time::{Duration, Instant}; + +/// Log buffer for batching +struct LogBuffer { + entries: Vec<(Level, String)>, + last_flush: Instant, +} + +impl LogBuffer { + fn new() -> Self { + Self { + entries: Vec::new(), + last_flush: Instant::now(), + } + } + + fn should_flush(&self) -> bool { + self.last_flush.elapsed() >= Duration::from_millis(100) || self.entries.len() >= 50 + } +} + +static LOG_BUFFER: Mutex> = Mutex::new(None); + +/// Log filter configuration supporting RUST_LOG format +struct LogFilterConfig { + default_level: LevelFilter, + module_filters: Vec<(String, LevelFilter)>, +} + +impl LogFilterConfig { + const fn new() -> Self { + Self { + // Default to debug for normal diagnostics; can be overridden via nativeSetLogFilter() + default_level: LevelFilter::Debug, + module_filters: Vec::new(), + } + } + + fn should_log(&self, metadata: &Metadata) -> bool { + let target = metadata.target(); + let level = metadata.level(); + + // Check module-specific filters first + for (module, module_level) in &self.module_filters { + if target.starts_with(module) { + return level <= *module_level; + } + } + + // Fall back to default level + level <= self.default_level + } +} + +/// Global log filter for RUST_LOG-style filtering +static LOG_FILTER: RwLock = RwLock::new(LogFilterConfig::new()); + +/// Update log filter from RUST_LOG-style string +/// Examples: "info", "debug", "info,ckb_network=debug", "info,ckb_sync=trace,ckb_network=debug" +pub fn set_log_filter(rust_log: &str) { + let mut config = LogFilterConfig::new(); + + // Parse RUST_LOG format + for directive in rust_log.split(',') { + let directive = directive.trim(); + if directive.is_empty() { + continue; + } + + if let Some((module, level_str)) = directive.split_once('=') { + // Module-specific: "ckb_network=debug" + if let Ok(level) = level_str.trim().parse::() { + config + .module_filters + .push((module.trim().to_string(), level)); + } + } else { + // Global level: "debug" + if let Ok(level) = directive.parse::() { + config.default_level = level; + } + } + } + + // Update global filter + if let Ok(mut filter) = LOG_FILTER.write() { + *filter = config; + log::set_max_level(LevelFilter::Trace); // Allow all, filtering in enabled() + } +} + +/// JNI Logger implementation with batching and RUST_LOG-style filtering +pub struct JniLogger; + +impl Log for JniLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + if let Ok(filter) = LOG_FILTER.read() { + filter.should_log(metadata) + } else { + metadata.level() <= LevelFilter::Info + } + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + + let mut buffer_guard = LOG_BUFFER.lock().unwrap(); + let buffer = buffer_guard.get_or_insert_with(LogBuffer::new); + + // Mimic env_logger default: include target/module path in the message + let message = format!("{}: {}", record.target(), record.args()); + buffer.entries.push((record.level(), message)); + + // Flush immediately to avoid losing short bursts (mobile app often emits only a few lines) + flush_log_buffer(buffer); + } + + fn flush(&self) { + let mut buffer_guard = LOG_BUFFER.lock().unwrap(); + if let Some(buffer) = buffer_guard.as_mut() { + flush_log_buffer(buffer); + } + } +} + +/// Flush log buffer and invoke JVM callback +fn flush_log_buffer(buffer: &mut LogBuffer) { + if buffer.entries.is_empty() { + return; + } + + // Group by level for efficiency + let mut grouped: std::collections::HashMap> = + std::collections::HashMap::new(); + for (level, msg) in buffer.entries.drain(..) { + grouped.entry(level).or_insert_with(Vec::new).push(msg); + } + + // Invoke callback for each level + for (level, messages) in grouped { + if let Err(e) = invoke_log_callback_internal(level, &messages) { + eprintln!("Failed to invoke log callback: {:?}", e); + } + } + + buffer.last_flush = Instant::now(); +} + +/// Internal function to invoke log callback +fn invoke_log_callback_internal( + level: Level, + messages: &[String], +) -> Result<(), Box> { + let vm = JAVA_VM.get().ok_or("JavaVM not initialized")?; + let callback = LOG_CALLBACK.get().ok_or("Log callback not set")?; + + // Attach current thread to JVM + let mut env = vm.attach_current_thread()?; + + // Create level string + let level_str = env.new_string(level.to_string())?; + + // Create String array + let messages_array = create_string_array(&mut env, messages)?; + + // Call: void onLog(String level, String[] messages) + env.call_method( + callback.as_obj(), + "onLog", + "(Ljava/lang/String;[Ljava/lang/String;)V", + &[ + JValue::Object(&level_str), + JValue::Object(unsafe { &JObject::from_raw(messages_array) }), + ], + )?; + + // env is automatically detached when dropped + Ok(()) +} + +/// Public function to invoke log callback +pub fn invoke_log_callback(level: Level, messages: &[String]) { + if let Err(e) = invoke_log_callback_internal(level, messages) { + eprintln!("Failed to invoke log callback: {:?}", e); + } +} + +/// Invoke status callback +pub fn invoke_status_callback(status: &str, data: &str) -> Result<(), Box> { + let vm = JAVA_VM.get().ok_or("JavaVM not initialized")?; + let callback = STATUS_CALLBACK.get().ok_or("Status callback not set")?; + + // Attach current thread to JVM + let mut env = vm.attach_current_thread()?; + + // Create strings + let status_str = env.new_string(status)?; + let data_str = env.new_string(data)?; + + // Call: void onStatusChange(String status, String data) + env.call_method( + callback.as_obj(), + "onStatusChange", + "(Ljava/lang/String;Ljava/lang/String;)V", + &[JValue::Object(&status_str), JValue::Object(&data_str)], + )?; + + Ok(()) +} + +/// Helper to create String array from Vec +fn create_string_array( + env: &mut JNIEnv, + strings: &[String], +) -> Result> { + let string_class = env.find_class("java/lang/String")?; + let empty_string = env.new_string("")?; + + let array = env.new_object_array(strings.len() as i32, string_class, empty_string)?; + + for (i, s) in strings.iter().enumerate() { + let jstr = env.new_string(s)?; + env.set_object_array_element(&array, i as i32, jstr)?; + } + + Ok(array.into_raw()) +} + +/// Force flush on demand (called on stop) +pub fn flush_logs() { + let mut buffer_guard = LOG_BUFFER.lock().unwrap(); + if let Some(buffer) = buffer_guard.as_mut() { + flush_log_buffer(buffer); + } +} diff --git a/light-client-lib/src/jni_bridge/lifecycle.rs b/light-client-lib/src/jni_bridge/lifecycle.rs new file mode 100644 index 00000000..1982707a --- /dev/null +++ b/light-client-lib/src/jni_bridge/lifecycle.rs @@ -0,0 +1,297 @@ +//! Lifecycle management for JNI bridge +//! +//! Implements init/start/stop/status functions that mirror light-client-bin/src/subcmds.rs + +use super::callbacks::{flush_logs, invoke_status_callback, JniLogger}; +use super::types::*; +use crate::runtime::StartedLightClient; +use crate::types::RunEnv; +use ckb_async_runtime::tokio; +use ckb_stop_handler::{broadcast_exit_signals, wait_all_ckb_services_exit}; +use jni::objects::{JClass, JObject, JString}; +use jni::sys::{jboolean, jint, JNI_FALSE, JNI_TRUE}; +use jni::JNIEnv; +use log::{error, info, warn}; +use std::fs; + +/// JNI: Initialize the light client +/// +/// This performs all initialization including: +/// - Loading TOML config +/// - Initializing Storage and ChainSpec +/// - Creating protocols (Sync, Relay, LightClient, Filter) +/// - Starting NetworkService +/// - Starting RPC server +/// - Creating tokio runtime in dedicated thread +/// +/// Note: This starts the network service but doesn't change state to RUNNING yet. +/// Call nativeStart() to actually start processing. +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeInit( + mut env: JNIEnv, + _class: JClass, + config_path_jstr: JString, + log_callback: JObject, + status_callback: JObject, +) -> jboolean { + // Check if already initialized + if is_initialized() { + error!("Already initialized!"); + return JNI_FALSE; + } + + // Initialize JavaVM and callbacks only once (first time) + if JAVA_VM.get().is_none() { + info!("First time initialization - setting up JavaVM and callbacks..."); + + // Get JavaVM for callbacks + let vm = match env.get_java_vm() { + Ok(vm) => vm, + Err(e) => { + eprintln!("Failed to get JavaVM: {}", e); + return JNI_FALSE; + } + }; + + // Store JavaVM + if JAVA_VM.set(vm).is_err() { + error!("Failed to store JavaVM"); + return JNI_FALSE; + } + + // Create global refs for callbacks + let log_callback_ref = match env.new_global_ref(log_callback) { + Ok(r) => r, + Err(e) => { + error!("Failed to create log callback GlobalRef: {}", e); + return JNI_FALSE; + } + }; + + let status_callback_ref = match env.new_global_ref(status_callback) { + Ok(r) => r, + Err(e) => { + error!("Failed to create status callback GlobalRef: {}", e); + return JNI_FALSE; + } + }; + + // Store callbacks + if LOG_CALLBACK.set(log_callback_ref).is_err() { + error!("Failed to store log callback"); + return JNI_FALSE; + } + + if STATUS_CALLBACK.set(status_callback_ref).is_err() { + error!("Failed to store status callback"); + return JNI_FALSE; + } + + // Initialize logger with JNI logger + if let Err(e) = log::set_boxed_logger(Box::new(JniLogger)) { + eprintln!("Failed to set logger: {}", e); + return JNI_FALSE; + } + // Default to debug; filters can still be tightened via nativeSetLogFilter() + log::set_max_level(log::LevelFilter::Trace); + } else { + info!("Reinitialization - JavaVM and callbacks already set, skipping...") + } + + info!("Starting CKB Light Client JNI initialization..."); + + // Get config path + let config_path: String = match env.get_string(&config_path_jstr) { + Ok(s) => s.into(), + Err(e) => { + error!("Failed to get config path: {}", e); + return JNI_FALSE; + } + }; + + info!("Loading config from: {}", config_path); + + // Load and parse TOML config + let run_env: RunEnv = match load_config(&config_path) { + Ok(env) => env, + Err(e) => { + error!("Failed to load config: {}", e); + return JNI_FALSE; + } + }; + + info!("Config loaded successfully"); + info!("Chain: {}", run_env.chain); + info!("Store path: {:?}", run_env.store.path); + info!("Network path: {:?}", run_env.network.path); + + let client = match StartedLightClient::start(run_env) { + Ok(client) => client, + Err(e) => { + error!("Failed to start light client: {}", e); + return JNI_FALSE; + } + }; + + // Store global state for queries and shutdown + *STORAGE_WITH_DATA.write().unwrap() = Some(client.storage_with_data()); + *NET_CONTROL.write().unwrap() = Some(client.network_controller()); + *CONSENSUS.write().unwrap() = Some(client.consensus()); + *PEERS.write().unwrap() = Some(client.peers()); + *CLIENT_HANDLE.lock().unwrap() = Some(client); + + // Set state to INIT + set_state(STATE_INIT); + + info!("CKB Light Client initialized successfully!"); + + // Notify status callback + let _ = invoke_status_callback("initialized", ""); + + JNI_TRUE +} + +/// Load config from TOML file +fn load_config(path: &str) -> Result> { + let content = fs::read_to_string(path)?; + let run_env: RunEnv = toml::from_str(&content)?; + Ok(run_env) +} + +/// JNI: Start the light client +/// +/// This transitions from INIT to RUNNING state. +/// The network service is already running (started in init). +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeStart( + _env: JNIEnv, + _class: JClass, +) -> jboolean { + // Check if initialized + if !is_state(STATE_INIT) { + error!("Not in INIT state! Current state: {}", get_state()); + return JNI_FALSE; + } + + info!("Starting CKB Light Client..."); + + // Transition to RUNNING + set_state(STATE_RUNNING); + + info!("CKB Light Client started successfully!"); + + // Notify status callback + let _ = invoke_status_callback("running", ""); + + JNI_TRUE +} + +/// JNI: Stop the light client +/// +/// This fully shuts down the light client: +/// - Broadcast exit signals to all services +/// - Wait for services to stop +/// - Clear all global state +/// - Flush logs +/// - Transition to STOPPED state +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeStop( + _env: JNIEnv, + _class: JClass, +) -> jboolean { + // Check if running + if !is_state(STATE_RUNNING) { + warn!("Not in RUNNING state! Current state: {}", get_state()); + return JNI_FALSE; + } + + info!("Stopping CKB Light Client..."); + + // Take ownership of the running client (releases the mutex before blocking waits) + let client_opt = { + let mut guard = CLIENT_HANDLE.lock().unwrap(); + guard.take() + }; + let mut client: StartedLightClient = match client_opt { + Some(client) => client, + None => { + warn!("Stop requested but no client handle found"); + return JNI_FALSE; + } + }; + + // Broadcast exit signals to all services + broadcast_exit_signals(); + + // Wait for all CKB services to exit + info!("Waiting for services to exit..."); + wait_all_ckb_services_exit(); + + // Release runtime guard and wait for runtime tasks to finish + client.runtime_handle().drop_guard(); + tokio::task::block_in_place(|| { + client.stop_receiver().blocking_recv(); + }); + + // Flush any pending logs + flush_logs(); + + // Clear all global state (RwLock values) + clear_state(); + + // Transition to STOPPED + set_state(STATE_STOPPED); + + info!("CKB Light Client stopped successfully!"); + + // Notify status callback + let _ = invoke_status_callback("stopped", ""); + + JNI_TRUE +} + +/// JNI: Get current status +/// +/// Returns: +/// - 0 (INIT): Initialized but not started +/// - 1 (RUNNING): Running +/// - 2 (STOPPED): Stopped +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeGetStatus( + _env: JNIEnv, + _class: JClass, +) -> jint { + get_state() as jint +} + +/// JNI: Set log filter using RUST_LOG format +/// +/// Parameters: +/// - filter_str: RUST_LOG-style filter string +/// Examples: +/// - "info" - all modules at info level +/// - "debug" - all modules at debug level +/// - "info,ckb_network=debug" - most at info, ckb_network at debug +/// - "info,ckb_network=debug,ckb_sync=trace" - per-module levels +/// +/// Returns true if successful, false otherwise +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeSetLogFilter( + mut env: JNIEnv, + _class: JClass, + filter_jstr: JString, +) -> jboolean { + // Get filter string + let filter_str: String = match env.get_string(&filter_jstr) { + Ok(s) => s.into(), + Err(e) => { + error!("Failed to get filter string: {}", e); + return JNI_FALSE; + } + }; + + info!("Setting log filter to: {}", filter_str); + super::callbacks::set_log_filter(&filter_str); + + JNI_TRUE +} diff --git a/light-client-lib/src/jni_bridge/mod.rs b/light-client-lib/src/jni_bridge/mod.rs new file mode 100644 index 00000000..ed5bcee8 --- /dev/null +++ b/light-client-lib/src/jni_bridge/mod.rs @@ -0,0 +1,38 @@ +//! JNI bridge for Android +//! +//! This module provides JNI bindings to expose CKB Light Client functionality +//! to Android applications. +//! +//! ## Architecture +//! +//! - `types`: Global state management using OnceLock pattern +//! - `lifecycle`: Init/start/stop/status lifecycle management +//! - `callbacks`: Log and status callbacks with batching +//! - `query`: 17 query APIs matching WASM implementation +//! +//! ## State Machine +//! +//! - 0 (INIT): Initialized but not started +//! - 1 (RUNNING): Running +//! - 2 (STOPPED): Stopped +//! +//! ## Thread Model +//! +//! - Main JNI calls run on Android threads +//! - Dedicated native thread runs tokio runtime +//! - Callbacks attach/detach from JVM as needed + +pub mod callbacks; +pub mod lifecycle; +pub mod query; +pub mod types; + +// Re-export main entry points +pub use lifecycle::{ + Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeGetStatus, + Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeInit, + Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeStart, + Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeStop, +}; + +pub use query::*; diff --git a/light-client-lib/src/jni_bridge/query.rs b/light-client-lib/src/jni_bridge/query.rs new file mode 100644 index 00000000..e11cfeca --- /dev/null +++ b/light-client-lib/src/jni_bridge/query.rs @@ -0,0 +1,518 @@ +//! Query APIs for JNI bridge +//! +//! Provides 17 query APIs matching WASM implementation. +//! All functions return JSON strings for complex types, or null on error. + +use super::types::*; +use crate::service::{ + Cell, CellsCapacity, FetchStatus, LocalNode, Order, Pagination, RemoteNode, SearchKey, + SetScriptsCommand, TransactionWithStatus, +}; +use crate::storage::{self, extract_raw_data, Key, KeyPrefix, LAST_STATE_KEY}; +use crate::verify::verify_tx; +use ckb_jsonrpc_types::{BlockView, EstimateCycles, HeaderView, JsonBytes, Transaction}; +use ckb_network::extract_peer_id; +use ckb_systemtime::unix_time_as_millis; +use ckb_traits::HeaderProvider; +use ckb_types::{core, packed, prelude::*, H256}; +use jni::objects::{JClass, JString}; +use jni::sys::jstring; +use jni::JNIEnv; +use log::{debug, error, warn}; +use std::ptr; +use std::str::FromStr; + +/// Helper to check running state and return null if not running +macro_rules! check_running { + ($env:expr) => { + if !is_running() { + warn!("Light client not running, current state: {}", get_state()); + return ptr::null_mut(); + } + }; +} + +/// Helper to create JString from serde result +fn to_jstring(env: &mut JNIEnv, value: &T) -> jstring { + match serde_json::to_string(value) { + Ok(json) => match env.new_string(json) { + Ok(s) => s.into_raw(), + Err(e) => { + error!("Failed to create JString: {}", e); + ptr::null_mut() + } + }, + Err(e) => { + error!("Failed to serialize to JSON: {}", e); + ptr::null_mut() + } + } +} + +/// Get tip header +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeGetTipHeader( + mut env: JNIEnv, + _class: JClass, +) -> jstring { + check_running!(env); + + let binding = STORAGE_WITH_DATA.read().unwrap(); + let swc = match binding.as_ref() { + Some(s) => s, + None => { + error!("Storage not initialized"); + return ptr::null_mut(); + } + }; + + let tip_header = swc.storage().get_tip_header(); + let header_view: HeaderView = tip_header.into_view().into(); + + to_jstring(&mut env, &header_view) +} + +/// Get genesis block +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeGetGenesisBlock( + mut env: JNIEnv, + _class: JClass, +) -> jstring { + check_running!(env); + + let binding = STORAGE_WITH_DATA.read().unwrap(); + let swc = match binding.as_ref() { + Some(s) => s, + None => { + error!("Storage not initialized"); + return ptr::null_mut(); + } + }; + + let genesis_block = swc.storage().get_genesis_block(); + + // Convert packed::Block to BlockView via core::BlockView + use ckb_types::prelude::Unpack; + let core_block_view: ckb_types::core::BlockView = genesis_block.into_view(); + let block_view: BlockView = core_block_view.into(); + to_jstring(&mut env, &block_view) +} + +/// Get header by hash +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeGetHeader( + mut env: JNIEnv, + _class: JClass, + hash: JString, +) -> jstring { + check_running!(env); + + let hash_str: String = match env.get_string(&hash) { + Ok(s) => s.into(), + Err(e) => { + error!("Failed to get hash string: {}", e); + return ptr::null_mut(); + } + }; + + let binding = STORAGE_WITH_DATA.read().unwrap(); + let swc = match binding.as_ref() { + Some(s) => s, + None => { + error!("Storage not initialized"); + return ptr::null_mut(); + } + }; + + let h256 = match H256::from_str(&hash_str) { + Ok(h) => h, + Err(e) => { + error!("Invalid hash: {}", e); + return ptr::null_mut(); + } + }; + + let hash = packed::Byte32::from_slice(h256.as_bytes()).expect("H256 to Byte32"); + + match swc.storage().get_header(&hash) { + Some(header) => { + let header_view: HeaderView = header.into(); + to_jstring(&mut env, &header_view) + } + None => ptr::null_mut(), + } +} + +/// Fetch header (with fetch status) +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeFetchHeader( + mut env: JNIEnv, + _class: JClass, + hash: JString, +) -> jstring { + check_running!(env); + + let hash_str: String = match env.get_string(&hash) { + Ok(s) => s.into(), + Err(e) => { + error!("Failed to get hash string: {}", e); + return ptr::null_mut(); + } + }; + + let binding = STORAGE_WITH_DATA.read().unwrap(); + let swc = match binding.as_ref() { + Some(s) => s, + None => { + error!("Storage not initialized"); + return ptr::null_mut(); + } + }; + + let peers_binding = PEERS.read().unwrap(); + let peers = match peers_binding.as_ref() { + Some(p) => p, + None => { + error!("Peers not initialized"); + return ptr::null_mut(); + } + }; + + let h256 = match H256::from_str(&hash_str) { + Ok(h) => h, + Err(e) => { + error!("Invalid hash: {}", e); + return ptr::null_mut(); + } + }; + + let hash = packed::Byte32::from_slice(h256.as_bytes()).expect("H256 to Byte32"); + + let fetch_status: FetchStatus = + if let Some(header) = swc.storage().get_header(&hash) { + FetchStatus::Fetched { + data: header.into(), + } + } else if peers.fetching_headers().contains_key(&hash) { + FetchStatus::Fetching { + first_sent: 0.into(), + } + } else { + // Add to fetch queue + let net_binding = NET_CONTROL.read().unwrap(); + let _net_controller = match net_binding.as_ref() { + Some(nc) => nc, + None => { + error!("Network controller not initialized"); + return ptr::null_mut(); + } + }; + + let timestamp = unix_time_as_millis(); + peers.add_fetch_header(hash.clone(), timestamp); + + FetchStatus::Added { + timestamp: timestamp.into(), + } + }; + + to_jstring(&mut env, &fetch_status) +} + +/// Set scripts +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeSetScripts( + mut env: JNIEnv, + _class: JClass, + scripts_json: JString, + command: i32, +) -> jni::sys::jboolean { + if !is_running() { + warn!("Light client not running"); + return jni::sys::JNI_FALSE; + } + + let scripts_str: String = match env.get_string(&scripts_json) { + Ok(s) => s.into(), + Err(e) => { + error!("Failed to get scripts JSON: {}", e); + return jni::sys::JNI_FALSE; + } + }; + + let scripts_json: Vec = match serde_json::from_str(&scripts_str) { + Ok(s) => s, + Err(e) => { + error!("Failed to parse scripts JSON: {}", e); + return jni::sys::JNI_FALSE; + } + }; + + // Convert service::ScriptStatus to storage::ScriptStatus + let scripts: Vec = scripts_json + .into_iter() + .map(|s| storage::ScriptStatus { + script: s.script.into(), + script_type: match s.script_type { + crate::service::ScriptType::Lock => storage::ScriptType::Lock, + crate::service::ScriptType::Type => storage::ScriptType::Type, + }, + block_number: s.block_number.into(), + }) + .collect(); + + let cmd = match command { + 0 => SetScriptsCommand::All, + 1 => SetScriptsCommand::Partial, + 2 => SetScriptsCommand::Delete, + _ => { + error!("Invalid command: {}", command); + return jni::sys::JNI_FALSE; + } + }; + + let binding = STORAGE_WITH_DATA.read().unwrap(); + let swc = match binding.as_ref() { + Some(s) => s, + None => { + error!("Storage not initialized"); + return jni::sys::JNI_FALSE; + } + }; + + swc.storage().update_filter_scripts(scripts, cmd.into()); + + // Clear matched blocks when scripts change + let peers_binding = PEERS.read().unwrap(); + let peers = match peers_binding.as_ref() { + Some(p) => p, + None => { + error!("Peers not initialized"); + return jni::sys::JNI_FALSE; + } + }; + + // Lock matched_blocks and clear them + let mut matched_blocks = peers.matched_blocks().blocking_write(); + peers.clear_matched_blocks(&mut matched_blocks); + + jni::sys::JNI_TRUE +} + +/// Get scripts +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeGetScripts( + mut env: JNIEnv, + _class: JClass, +) -> jstring { + check_running!(env); + + let binding = STORAGE_WITH_DATA.read().unwrap(); + let swc = match binding.as_ref() { + Some(s) => s, + None => { + error!("Storage not initialized"); + return ptr::null_mut(); + } + }; + + let scripts = swc.storage().get_filter_scripts(); + // Convert storage::ScriptStatus to service::ScriptStatus for serialization + let scripts: Vec = scripts + .into_iter() + .map(|s| crate::service::ScriptStatus { + script: s.script.into(), + script_type: match s.script_type { + storage::ScriptType::Lock => crate::service::ScriptType::Lock, + storage::ScriptType::Type => crate::service::ScriptType::Type, + }, + block_number: s.block_number.into(), + }) + .collect(); + to_jstring(&mut env, &scripts) +} + +/// Get local node info +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeLocalNodeInfo( + mut env: JNIEnv, + _class: JClass, +) -> jstring { + check_running!(env); + + let net_binding = NET_CONTROL.read().unwrap(); + let net_controller = match net_binding.as_ref() { + Some(nc) => nc, + None => { + error!("Network controller not initialized"); + return ptr::null_mut(); + } + }; + + let consensus_binding = CONSENSUS.read().unwrap(); + let _consensus = match consensus_binding.as_ref() { + Some(c) => c, + None => { + error!("Consensus not initialized"); + return ptr::null_mut(); + } + }; + + let node_id = net_controller.node_id(); + + let node_info = LocalNode { + active: is_running(), + addresses: vec![], // TODO: get actual addresses + connections: (net_controller.connected_peers().len() as u64).into(), + node_id, + protocols: vec![], // TODO: get actual protocols + version: env!("CARGO_PKG_VERSION").to_owned(), + }; + + to_jstring(&mut env, &node_info) +} + +/// Get peers +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeGetPeers( + mut env: JNIEnv, + _class: JClass, +) -> jstring { + check_running!(env); + + let net_binding = NET_CONTROL.read().unwrap(); + let net_controller = match net_binding.as_ref() { + Some(nc) => nc, + None => { + error!("Network controller not initialized"); + return ptr::null_mut(); + } + }; + + let mut remote_nodes = Vec::new(); + + // connected_peers() returns Vec<(SessionId, Peer)> + for (_session_id, peer) in net_controller.connected_peers() { + // Extract peer_id from the connected address + let node_id = extract_peer_id(&peer.connected_addr) + .map(|id| id.to_base58()) + .unwrap_or_else(|| "unknown".to_owned()); + + // Calculate connection duration in milliseconds + let connected_duration_ms = peer.connected_time.elapsed().as_millis() as u64; + + let remote_node = RemoteNode { + version: peer + .identify_info + .as_ref() + .map(|info| info.client_version.clone()) + .unwrap_or_else(|| "unknown".to_owned()), + node_id, + addresses: vec![], // TODO: get actual addresses + connected_duration: connected_duration_ms.into(), + sync_state: None, // TODO: get sync state + protocols: vec![], // TODO: get actual protocols + }; + + remote_nodes.push(remote_node); + } + + to_jstring(&mut env, &remote_nodes) +} + +// TODO: Implement remaining 10 APIs: +// - nativeGetCells +// - nativeGetTransactions +// - nativeGetCellsCapacity +// - nativeSendTransaction +// - nativeGetTransaction +// - nativeFetchTransaction +// - nativeEstimateCycles +// (Plus the 3 already implemented: GetTipHeader, GetGenesisBlock, GetHeader, FetchHeader, +// SetScripts, GetScripts, LocalNodeInfo, GetPeers) + +// Placeholder implementations for remaining APIs +// These return null for now and can be implemented as needed + +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeGetCells( + _env: JNIEnv, + _class: JClass, + _search_key_json: JString, + _order: JString, + _limit: jni::sys::jint, + _cursor: JString, +) -> jstring { + // TODO: Implement + warn!("nativeGetCells not yet implemented"); + ptr::null_mut() +} + +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeGetTransactions( + _env: JNIEnv, + _class: JClass, + _search_key_json: JString, + _order: JString, + _limit: jni::sys::jint, + _cursor: JString, +) -> jstring { + // TODO: Implement + warn!("nativeGetTransactions not yet implemented"); + ptr::null_mut() +} + +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeGetCellsCapacity( + _env: JNIEnv, + _class: JClass, + _search_key_json: JString, +) -> jstring { + // TODO: Implement + warn!("nativeGetCellsCapacity not yet implemented"); + ptr::null_mut() +} + +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeSendTransaction( + _env: JNIEnv, + _class: JClass, + _tx_json: JString, +) -> jstring { + // TODO: Implement + warn!("nativeSendTransaction not yet implemented"); + ptr::null_mut() +} + +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeGetTransaction( + _env: JNIEnv, + _class: JClass, + _hash: JString, +) -> jstring { + // TODO: Implement + warn!("nativeGetTransaction not yet implemented"); + ptr::null_mut() +} + +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeFetchTransaction( + _env: JNIEnv, + _class: JClass, + _hash: JString, +) -> jstring { + // TODO: Implement + warn!("nativeFetchTransaction not yet implemented"); + ptr::null_mut() +} + +#[no_mangle] +pub extern "C" fn Java_com_nervosnetwork_ckblightclient_LightClientNative_nativeEstimateCycles( + _env: JNIEnv, + _class: JClass, + _tx_json: JString, +) -> jstring { + // TODO: Implement + warn!("nativeEstimateCycles not yet implemented"); + ptr::null_mut() +} diff --git a/light-client-lib/src/jni_bridge/types.rs b/light-client-lib/src/jni_bridge/types.rs new file mode 100644 index 00000000..2d1286b0 --- /dev/null +++ b/light-client-lib/src/jni_bridge/types.rs @@ -0,0 +1,92 @@ +//! Global state management for JNI bridge +//! +//! Uses RwLock> for resettable state, OnceLock for permanent state. + +use crate::protocols::Peers; +use crate::runtime::StartedLightClient; +use crate::storage::StorageWithChainData; +use crate::types::Mutex; +use ckb_chain_spec::consensus::Consensus; +use ckb_network::NetworkController; +use jni::objects::GlobalRef; +use jni::JavaVM; +use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::{Arc, OnceLock, RwLock}; +use tokio::runtime::Runtime; + +/// State machine values +/// - 0 (INIT): Not initialized +/// - 1 (RUNNING): Running +/// - 2 (STOPPED): Stopped +pub static STATE: AtomicU8 = AtomicU8::new(0); + +pub const STATE_INIT: u8 = 0; +pub const STATE_RUNNING: u8 = 1; +pub const STATE_STOPPED: u8 = 2; + +/// Global storage with chain data (resettable) +pub static STORAGE_WITH_DATA: RwLock> = RwLock::new(None); + +/// Global network controller (resettable) +pub static NET_CONTROL: RwLock> = RwLock::new(None); + +/// Global consensus (resettable) +pub static CONSENSUS: RwLock>> = RwLock::new(None); + +/// Global peers manager (resettable) +pub static PEERS: RwLock>> = RwLock::new(None); + +/// Global tokio runtime (resettable) +pub static RUNTIME: RwLock> = RwLock::new(None); + +/// Global JavaVM for callbacks (permanent, set once) +pub static JAVA_VM: OnceLock = OnceLock::new(); + +/// Global log callback (permanent, set once) +pub static LOG_CALLBACK: OnceLock = OnceLock::new(); + +/// Global status callback (permanent, set once) +pub static STATUS_CALLBACK: OnceLock = OnceLock::new(); + +/// Global handle to the running light client (keeps runtime alive) +pub static CLIENT_HANDLE: Mutex> = Mutex::new(None); + +/// Check if state matches the given flag +pub fn is_state(state: u8) -> bool { + STATE.load(Ordering::SeqCst) == state +} + +/// Change state +pub fn set_state(state: u8) { + STATE.store(state, Ordering::SeqCst); +} + +/// Get current state +pub fn get_state() -> u8 { + STATE.load(Ordering::SeqCst) +} + +/// Helper to check if initialized +pub fn is_initialized() -> bool { + STORAGE_WITH_DATA.read().unwrap().is_some() +} + +/// Helper to check if running +pub fn is_running() -> bool { + is_state(STATE_RUNNING) +} + +/// Helper to check if stopped +pub fn is_stopped() -> bool { + is_state(STATE_STOPPED) +} + +/// Clear all resettable global state for full shutdown +pub fn clear_state() { + *CLIENT_HANDLE.lock().unwrap() = None; + *STORAGE_WITH_DATA.write().unwrap() = None; + *NET_CONTROL.write().unwrap() = None; + *CONSENSUS.write().unwrap() = None; + *PEERS.write().unwrap() = None; + *RUNTIME.write().unwrap() = None; +} diff --git a/light-client-lib/src/lib.rs b/light-client-lib/src/lib.rs index 3e2e9406..f8d416d6 100644 --- a/light-client-lib/src/lib.rs +++ b/light-client-lib/src/lib.rs @@ -6,8 +6,14 @@ mod tests; pub mod error; pub mod protocols; +#[cfg(not(target_arch = "wasm32"))] +pub mod runtime; pub mod service; pub mod storage; pub mod types; pub mod utils; pub mod verify; + +// JNI bridge for Android +#[cfg(all(feature = "jni-bridge", target_os = "android"))] +pub mod jni_bridge; diff --git a/light-client-lib/src/runtime.rs b/light-client-lib/src/runtime.rs new file mode 100644 index 00000000..71ddb6dc --- /dev/null +++ b/light-client-lib/src/runtime.rs @@ -0,0 +1,190 @@ +//! Shared runtime bootstrap for native targets (CLI and JNI). +//! +//! This module contains the same startup path the binary uses, but returns +//! the handles required to shut the light client down gracefully from +//! embedded environments (Android JNI, tests, etc). + +use crate::error::{Error, Result}; +use crate::protocols::{ + FilterProtocol, LightClientProtocol, Peers, PendingTxs, RelayProtocol, SyncProtocol, + BAD_MESSAGE_ALLOWED_EACH_HOUR, CHECK_POINT_INTERVAL, +}; +use crate::storage::{Storage, StorageWithChainData}; +use crate::types::RunEnv; +use crate::utils; +use ckb_async_runtime::{new_global_runtime, tokio, Handle, Runtime}; +use ckb_chain_spec::{consensus::Consensus, ChainSpec}; +use ckb_network::{ + network::TransportType, CKBProtocol, CKBProtocolHandler, Flags, NetworkService, NetworkState, + SupportProtocols, +}; +use ckb_resource::Resource; +use std::sync::{Arc, RwLock}; + +/// Holds the running light client pieces so callers can control shutdown. +pub struct StartedLightClient { + runtime_handle: Handle, + runtime_stop_rx: tokio::sync::mpsc::Receiver<()>, + runtime: Runtime, + network_controller: ckb_network::NetworkController, + storage_with_data: StorageWithChainData, + consensus: Arc, + peers: Arc, + pending_txs: Arc>, +} + +impl StartedLightClient { + /// Boot the light client using the provided run environment. + pub fn start(run_env: RunEnv) -> Result { + utils::fs::need_directory(&run_env.network.path)?; + + let storage = Storage::new(&run_env.store.path); + let chain_spec = ChainSpec::load_from(&match run_env.chain.as_str() { + "mainnet" => Resource::bundled("specs/mainnet.toml".to_string()), + "testnet" => Resource::bundled("specs/testnet.toml".to_string()), + path => Resource::file_system(path.into()), + }) + .map_err(|err| Error::runtime(format!("failed to load spec since {}", err)))?; + + let consensus = chain_spec + .build_consensus() + .map_err(|err| Error::runtime(format!("failed to build consensus since {}", err)))?; + + storage.init_genesis_block(consensus.genesis_block().data()); + + // Cleanup any invalid matched blocks from previous runs (e.g., uncle blocks from chain reorgs) + log::info!("Cleaning up invalid matched blocks..."); + storage.cleanup_invalid_matched_blocks(); + + let pending_txs = Arc::new(RwLock::new(PendingTxs::default())); + let max_outbound_peers = run_env.network.max_outbound_peers; + let network_state = NetworkState::from_config(run_env.network) + .map(|network_state| { + Arc::new(network_state.required_flags( + Flags::DISCOVERY + | Flags::SYNC + | Flags::RELAY + | Flags::LIGHT_CLIENT + | Flags::BLOCK_FILTER, + )) + }) + .map_err(|err| { + let errmsg = format!("failed to initialize network state since {}", err); + Error::runtime(errmsg) + })?; + let required_protocol_ids = vec![ + SupportProtocols::Sync.protocol_id(), + SupportProtocols::LightClient.protocol_id(), + SupportProtocols::Filter.protocol_id(), + ]; + + let peers = Arc::new(Peers::new( + max_outbound_peers, + CHECK_POINT_INTERVAL, + storage.get_last_check_point(), + BAD_MESSAGE_ALLOWED_EACH_HOUR, + )); + let sync_protocol = SyncProtocol::new(storage.clone(), Arc::clone(&peers)); + let relay_protocol = + RelayProtocol::new(pending_txs.clone(), Arc::clone(&peers), storage.clone()); + let light_client: Box = Box::new(LightClientProtocol::new( + storage.clone(), + Arc::clone(&peers), + consensus.clone(), + )); + let filter_protocol = FilterProtocol::new(storage.clone(), Arc::clone(&peers)); + + let protocols = vec![ + CKBProtocol::new_with_support_protocol( + SupportProtocols::Sync, + Box::new(sync_protocol), + Arc::clone(&network_state), + ), + CKBProtocol::new_with_support_protocol( + SupportProtocols::RelayV3, + Box::new(relay_protocol), + Arc::clone(&network_state), + ), + CKBProtocol::new_with_support_protocol( + SupportProtocols::LightClient, + light_client, + Arc::clone(&network_state), + ), + CKBProtocol::new_with_support_protocol( + SupportProtocols::Filter, + Box::new(filter_protocol), + Arc::clone(&network_state), + ), + ]; + + let (runtime_handle, runtime_stop_rx, runtime) = new_global_runtime(None); + + let network_controller = NetworkService::new( + Arc::clone(&network_state), + protocols, + required_protocol_ids, + ( + consensus.identify_name(), + env!("CARGO_PKG_VERSION").to_owned(), + Flags::DISCOVERY, + ), + // Usually native light-client only connects to peers through TCP + TransportType::Tcp, + ) + .start(&runtime_handle) + .map_err(|err| { + let errmsg = format!("failed to start network since {}", err); + Error::runtime(errmsg) + })?; + + let storage_with_data = + StorageWithChainData::new(storage.clone(), Arc::clone(&peers), pending_txs.clone()); + + Ok(Self { + runtime_handle, + runtime_stop_rx, + runtime, + network_controller, + storage_with_data, + consensus: Arc::new(consensus), + peers, + pending_txs, + }) + } + + pub fn network_controller(&self) -> ckb_network::NetworkController { + self.network_controller.clone() + } + + pub fn storage(&self) -> Storage { + self.storage_with_data.storage().clone() + } + + pub fn storage_with_data(&self) -> StorageWithChainData { + self.storage_with_data.clone() + } + + pub fn peers(&self) -> Arc { + Arc::clone(&self.peers) + } + + pub fn pending_txs(&self) -> Arc> { + Arc::clone(&self.pending_txs) + } + + pub fn consensus(&self) -> Arc { + Arc::clone(&self.consensus) + } + + pub fn runtime(&self) -> &Runtime { + &self.runtime + } + + pub fn runtime_handle(&mut self) -> &mut Handle { + &mut self.runtime_handle + } + + pub fn stop_receiver(&mut self) -> &mut tokio::sync::mpsc::Receiver<()> { + &mut self.runtime_stop_rx + } +} diff --git a/light-client-lib/src/storage/mod.rs b/light-client-lib/src/storage/mod.rs index 60b262ea..01feff0f 100644 --- a/light-client-lib/src/storage/mod.rs +++ b/light-client-lib/src/storage/mod.rs @@ -85,7 +85,7 @@ pub enum SetScriptsCommand { Delete, } -#[derive(PartialEq, Eq, Hash)] +#[derive(PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum ScriptType { Lock, Type,