diff --git a/.gitignore b/.gitignore index d295440..17d844a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ Cargo.lock .gradle wallet_db bdk_ffi_test -local.properties \ No newline at end of file +local.properties +*.log \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index c8afbd2..cb9787c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" crate-type = ["cdylib"] [dependencies] -bdk = { version = "^0.7", features = ["all-keys"] } +bdk = { version = "^0.11", features = ["all-keys"] } safer-ffi = { version = "0.0.6", features = ["proc_macros"]} [features] diff --git a/README.md b/README.md index 6d3f227..2935e1c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,16 @@ +Setup build environment +1. Add Android rust targets + +```sh +rustup target add x86_64-apple-darwin x86_64-unknown-linux-gnu x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android +``` + +2. Set ANDROID_NDK_HOME + +```sh +export ANDROID_NDK_HOME=/home//Android/Sdk/ndk/ +``` Adding new structs and functions diff --git a/build.sh b/build.sh index bf0fc42..67ca036 100755 --- a/build.sh +++ b/build.sh @@ -1,64 +1,114 @@ #!/usr/bin/env bash -set -eo pipefail -o xtrace +set -eo pipefail -# rust -cargo fmt -cargo build -cargo test --features c-headers -- generate_headers +# functions -# cc -export LD_LIBRARY_PATH=`pwd`/target/debug -cc cc/bdk_ffi_test.c -o cc/bdk_ffi_test -L target/debug -l bdk_ffi -l pthread -l dl -l m +## help +help() +{ + # Display Help + echo "Build bdk-ffi and related libraries." + echo + echo "Syntax: build [-a|h|k]" + echo "options:" + echo "-a Android aar." + echo "-h Print this Help." + echo "-k JVM jar." + echo +} + +## rust +build_rust() { + echo "Build Rust library and C headers" + cargo fmt + cargo build + cargo test --features c-headers -- generate_headers +} + +## cc +build_cc() { + echo "Build C test library" + export LD_LIBRARY_PATH=`pwd`/target/debug + cc cc/bdk_ffi_test.c -o cc/bdk_ffi_test -L target/debug -l bdk_ffi -l pthread -l dl -l m +} + +## copy to bdk-kotlin +copy_lib_kotlin() { + echo -n "Copy " + case $OS in + "Darwin") + echo -n "darwin " + mkdir -p bdk-kotlin/jvm/src/main/resources/darwin-x86-64 + cp target/debug/libbdk_ffi.dylib bdk-kotlin/jvm/src/main/resources/darwin-x86-64 + ;; + "Linux") + echo -n "linux " + mkdir -p bdk-kotlin/jvm/src/main/resources/linux-x86-64 + cp target/debug/libbdk_ffi.so bdk-kotlin/jvm/src/main/resources/linux-x86-64 + ;; + esac + echo "libs to kotlin sub-project" +} + +## bdk-kotlin jar +build_kotlin() { + (cd bdk-kotlin && ./gradlew :jvm:build && ./gradlew :jvm:publishToMavenLocal) +} + +## rust android +build_android() { + # If ANDROID_NDK_HOME is not set then set it to github actions default + [ -z "$ANDROID_NDK_HOME" ] && export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle + + # Update this line accordingly if you are not building *from* darwin-x86_64 or linux-x86_64 + export PATH=$PATH:$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/`uname | tr '[:upper:]' '[:lower:]'`-x86_64/bin + + # Required for 'ring' dependency to cross-compile to Android platform, must be at least 21 + export CFLAGS="-D__ANDROID_API__=21" + + # IMPORTANT: make sure every target is not a substring of a different one. We check for them with grep later on + BUILD_TARGETS="${BUILD_TARGETS:-aarch64,armv7,x86_64,i686}" + + mkdir -p bdk-kotlin/android/src/main/jniLibs/ bdk-kotlin/android/src/main/jniLibs/arm64-v8a bdk-kotlin/android/src/main/jniLibs/x86_64 bdk-kotlin/android/src/main/jniLibs/armeabi-v7a bdk-kotlin/android/src/main/jniLibs/x86 + + if echo $BUILD_TARGETS | grep "aarch64"; then + CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang" CC="aarch64-linux-android21-clang" cargo build --target=aarch64-linux-android + cp target/aarch64-linux-android/debug/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/arm64-v8a + fi + if echo $BUILD_TARGETS | grep "x86_64"; then + CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android21-clang" CC="x86_64-linux-android21-clang" cargo build --target=x86_64-linux-android + cp target/x86_64-linux-android/debug/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/x86_64 + fi + if echo $BUILD_TARGETS | grep "armv7"; then + CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi21-clang" CC="armv7a-linux-androideabi21-clang" cargo build --target=armv7-linux-androideabi + cp target/armv7-linux-androideabi/debug/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/armeabi-v7a + fi + if echo $BUILD_TARGETS | grep "i686"; then + CARGO_TARGET_I686_LINUX_ANDROID_LINKER="i686-linux-android21-clang" CC="i686-linux-android21-clang" cargo build --target=i686-linux-android + cp target/i686-linux-android/debug/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/x86 + fi + + # bdk-kotlin aar + (cd bdk-kotlin && ./gradlew :android:build && ./gradlew :android:publishToMavenLocal) +} -# bdk-kotlin jar OS=$(uname) -case $OS in - "Darwin") - echo "Darwin build system" - mkdir -p bdk-kotlin/jvm/src/main/resources/darwin-x86-64 - cp target/debug/libbdk_ffi.dylib bdk-kotlin/jvm/src/main/resources/darwin-x86-64 - ;; - "Linux") - echo "Linux build system" - mkdir -p bdk-kotlin/jvm/src/main/resources/linux-x86-64 - cp target/debug/libbdk_ffi.so bdk-kotlin/jvm/src/main/resources/linux-x86-64 - ;; -esac -(cd bdk-kotlin && gradle :jvm:build && gradle :jvm:publishToMavenLocal) +if [ $1 = "-h" ] +then + help +else + build_rust + build_cc + copy_lib_kotlin -# rust android - -# If ANDROID_NDK_HOME is not set then set it to github actions default -[ -z "$ANDROID_NDK_HOME" ] && export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle - -# Update this line accordingly if you are not building *from* darwin-x86_64 or linux-x86_64 -export PATH=$PATH:$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/`uname | tr '[:upper:]' '[:lower:]'`-x86_64/bin - -# Required for 'ring' dependency to cross-compile to Android platform, must be at least 21 -export CFLAGS="-D__ANDROID_API__=21" - -# IMPORTANT: make sure every target is not a substring of a different one. We check for them with grep later on -BUILD_TARGETS="${BUILD_TARGETS:-aarch64,armv7,x86_64,i686}" - -mkdir -p bdk-kotlin/android/src/main/jniLibs/ bdk-kotlin/android/src/main/jniLibs/arm64-v8a bdk-kotlin/android/src/main/jniLibs/x86_64 bdk-kotlin/android/src/main/jniLibs/armeabi-v7a bdk-kotlin/android/src/main/jniLibs/x86 - -if echo $BUILD_TARGETS | grep "aarch64"; then - CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang" CC="aarch64-linux-android21-clang" cargo build --target=aarch64-linux-android - cp target/aarch64-linux-android/debug/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/arm64-v8a + while [ -n "$1" ]; do # while loop starts + case "$1" in + -k) build_kotlin ;; + -a) build_android ;; + -h) help ;; + *) echo "Option $1 not recognized" ;; + esac + shift + done fi -if echo $BUILD_TARGETS | grep "x86_64"; then - CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android21-clang" CC="x86_64-linux-android21-clang" cargo build --target=x86_64-linux-android - cp target/x86_64-linux-android/debug/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/x86_64 -fi -if echo $BUILD_TARGETS | grep "armv7"; then - CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi21-clang" CC="armv7a-linux-androideabi21-clang" cargo build --target=armv7-linux-androideabi - cp target/armv7-linux-androideabi/debug/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/armeabi-v7a -fi -if echo $BUILD_TARGETS | grep "i686"; then - CARGO_TARGET_I686_LINUX_ANDROID_LINKER="i686-linux-android21-clang" CC="i686-linux-android21-clang" cargo build --target=i686-linux-android - cp target/i686-linux-android/debug/libbdk_ffi.so bdk-kotlin/android/src/main/jniLibs/x86 -fi - -# bdk-kotlin aar -(cd bdk-kotlin && gradle :android:build && gradle :android:publishToMavenLocal) diff --git a/src/error.rs b/src/error.rs index f27c6dd..b0631b3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,8 +9,6 @@ pub enum FfiError { InvalidU32Bytes, Generic, ScriptDoesntHaveAddressForm, - SingleRecipientMultipleOutputs, - SingleRecipientNoInputs, NoRecipients, NoUtxosSelected, OutputBelowDustLimit, @@ -23,12 +21,14 @@ pub enum FfiError { IrreplaceableTransaction, FeeRateTooLow, FeeTooLow, + FeeRateUnavailable, MissingKeyOrigin, Key, ChecksumMismatch, SpendingPolicyRequired, InvalidPolicyPathError, Signer, + InvalidNetwork, InvalidProgressValue, ProgressUpdateError, InvalidOutpoint, @@ -41,6 +41,7 @@ pub enum FfiError { Json, Hex, Psbt, + PsbtParse, Electrum, // Esplora, // CompactFilters, @@ -53,8 +54,6 @@ impl From<&bdk::Error> for FfiError { Error::InvalidU32Bytes(_) => FfiError::InvalidU32Bytes, Error::Generic(_) => FfiError::Generic, Error::ScriptDoesntHaveAddressForm => FfiError::ScriptDoesntHaveAddressForm, - Error::SingleRecipientMultipleOutputs => FfiError::SingleRecipientMultipleOutputs, - Error::SingleRecipientNoInputs => FfiError::SingleRecipientNoInputs, Error::NoRecipients => FfiError::NoRecipients, Error::NoUtxosSelected => FfiError::NoUtxosSelected, Error::OutputBelowDustLimit(_) => FfiError::OutputBelowDustLimit, @@ -67,12 +66,14 @@ impl From<&bdk::Error> for FfiError { Error::IrreplaceableTransaction => FfiError::IrreplaceableTransaction, Error::FeeRateTooLow { .. } => FfiError::FeeRateTooLow, Error::FeeTooLow { .. } => FfiError::FeeTooLow, + Error::FeeRateUnavailable => FfiError::FeeRateUnavailable, Error::MissingKeyOrigin(_) => FfiError::MissingKeyOrigin, Error::Key(_) => FfiError::Key, Error::ChecksumMismatch => FfiError::ChecksumMismatch, Error::SpendingPolicyRequired(_) => FfiError::SpendingPolicyRequired, Error::InvalidPolicyPathError(_) => FfiError::InvalidPolicyPathError, Error::Signer(_) => FfiError::Signer, + Error::InvalidNetwork { .. } => FfiError::InvalidNetwork, Error::InvalidProgressValue(_) => FfiError::InvalidProgressValue, Error::ProgressUpdateError => FfiError::ProgressUpdateError, Error::InvalidOutpoint(_) => FfiError::InvalidOutpoint, @@ -85,9 +86,11 @@ impl From<&bdk::Error> for FfiError { Error::Json(_) => FfiError::Json, Error::Hex(_) => FfiError::Hex, Error::Psbt(_) => FfiError::Psbt, + Error::PsbtParse(_) => FfiError::PsbtParse, Error::Electrum(_) => FfiError::Electrum, // Error::Esplora(_) => JniError::Esplora, // Error::CompactFilters(_) => JniError::CompactFilters, + // Error::Rpc(_) => JniError::Rpc, Error::Sled(_) => FfiError::Sled, } } diff --git a/src/wallet/blockchain.rs b/src/wallet/blockchain.rs index b3ecd8b..50742f8 100644 --- a/src/wallet/blockchain.rs +++ b/src/wallet/blockchain.rs @@ -16,6 +16,7 @@ fn new_electrum_config( socks5: Option, retry: i16, timeout: i16, + stop_gap: usize, ) -> Box { let url = url.to_string(); let socks5 = socks5.map(|s| s.to_string()); @@ -27,6 +28,7 @@ fn new_electrum_config( socks5, retry, timeout, + stop_gap, }); Box::new(BlockchainConfig { raw: electrum_config, diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 90327eb..4637efc 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -2,7 +2,6 @@ use std::convert::TryFrom; use std::ffi::CString; use ::safer_ffi::prelude::*; -use bdk::bitcoin::network::constants::Network::Testnet; use bdk::blockchain::{log_progress, AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain}; use bdk::database::{AnyDatabase, AnyDatabaseConfig, ConfigurableDatabase}; use bdk::wallet::AddressIndex::New; @@ -16,6 +15,8 @@ use database::DatabaseConfig; use crate::error::FfiError; use crate::types::{FfiResult, FfiResultVoid}; use crate::wallet::transaction::{LocalUtxo, TransactionDetails}; +use bdk::bitcoin::Network; +use std::str::FromStr; mod blockchain; mod database; @@ -33,14 +34,16 @@ pub struct OpaqueWallet { fn new_wallet_result( descriptor: char_p_ref, change_descriptor: Option, + network: char_p_ref, blockchain_config: &BlockchainConfig, database_config: &DatabaseConfig, ) -> FfiResult>> { let descriptor = descriptor.to_string(); let change_descriptor = change_descriptor.map(|s| s.to_string()); + let net = Network::from_str(network.to_str()).expect("Network name"); let bc_config = &blockchain_config.raw; let db_config = &database_config.raw; - let wallet_result = new_wallet(descriptor, change_descriptor, bc_config, db_config); + let wallet_result = new_wallet(descriptor, change_descriptor, net, bc_config, db_config); match wallet_result { Ok(w) => FfiResult { @@ -57,11 +60,10 @@ fn new_wallet_result( fn new_wallet( descriptor: String, change_descriptor: Option, + network: Network, blockchain_config: &AnyBlockchainConfig, database_config: &AnyDatabaseConfig, ) -> Result, Error> { - let network = Testnet; - let client = AnyBlockchain::from_config(blockchain_config)?; let database = AnyDatabase::from_config(database_config)?; diff --git a/src/wallet/transaction.rs b/src/wallet/transaction.rs index 378d4cf..5c2194c 100644 --- a/src/wallet/transaction.rs +++ b/src/wallet/transaction.rs @@ -13,27 +13,52 @@ pub struct TransactionDetails { // pub transaction: Option, /// Transaction id pub txid: char_p_boxed, - /// Timestamp - pub timestamp: u64, /// Received value (sats) pub received: u64, /// Sent value (sats) pub sent: u64, - /// Fee value (sats) - pub fees: u64, - /// Confirmed in block height, `None` means unconfirmed - pub height: i32, + /// Fee value (sats) if known, -1 if unknown, based on backend + pub fee: i64, + /// true if confirmed + pub is_confirmed: bool, + /// Confirmed in block height + pub confirmation_time: ConfirmationTime, + /// Whether the tx has been verified against the consensus rules + pub verified: bool, +} + +#[derive_ReprC] +#[repr(C)] +#[derive(Debug, Clone)] +pub struct ConfirmationTime { + /// confirmation block height, 0 if is_confirmed is false + pub height: u32, + /// confirmation block timestamp, 0 if is_confirmed is false + pub timestamp: u64, } impl From<&bdk::TransactionDetails> for TransactionDetails { fn from(op: &bdk::TransactionDetails) -> Self { + let fee = op.fee.map(|f| i64::try_from(f).unwrap()).unwrap_or(-1); + let confirmation_time = op + .confirmation_time + .as_ref() + .map(|c| ConfirmationTime { + height: c.height, + timestamp: c.timestamp, + }) + .unwrap_or(ConfirmationTime { + height: 0, + timestamp: 0, + }); TransactionDetails { txid: char_p_boxed::try_from(op.txid.to_string()).unwrap(), - timestamp: op.timestamp, received: op.received, sent: op.sent, - fees: op.fees, - height: op.height.map(|h| h as i32).unwrap_or(-1), + fee, + is_confirmed: op.confirmation_time.is_some(), + confirmation_time, + verified: op.verified, } } } diff --git a/test.sh b/test.sh index c5a23fc..45b497e 100755 --- a/test.sh +++ b/test.sh @@ -1,14 +1,62 @@ #!/usr/bin/env bash -set -eo pipefail -o xtrace +set -eo pipefail + +# functions + +## help +help() +{ + # Display Help + echo "Test bdk-ffi and related libraries." + echo + echo "Syntax: build [-a|h|k|v]" + echo "options:" + echo "-a Android aar tests." + echo "-h Print this Help." + echo "-k JVM jar tests." + echo "-v Valgrind tests." + echo +} # rust -cargo test --features c-headers -- generate_headers +c_headers() { + cargo test --features c-headers -- generate_headers +} # cc -export LD_LIBRARY_PATH=`pwd`/target/debug -#valgrind --leak-check=full --show-leak-kinds=all cc/bdk_ffi_test -cc/bdk_ffi_test +test_c() { + export LD_LIBRARY_PATH=`pwd`/target/debug + cc/bdk_ffi_test +} -# bdk-kotlin -(cd bdk-kotlin && gradle test) -(cd bdk-kotlin && gradle :android:connectedDebugAndroidTest) +test_valgrind() { + valgrind --leak-check=full --show-leak-kinds=all cc/bdk_ffi_test +} + +test_kotlin() { + (cd bdk-kotlin && ./gradlew test) +} + +test_android() { + (cd bdk-kotlin && ./gradlew :android:connectedDebugAndroidTest) +} + +if [ $1 = "-h" ] +then + help +else + c_headers + test_c + + # optional tests + while [ -n "$1" ]; do # while loop starts + case "$1" in + -a) test_android ;; + -h) help ;; + -k) test_kotlin ;; + -v) test_valgrind ;; + *) echo "Option $1 not recognized" ;; + esac + shift + done +fi \ No newline at end of file