From 060e54a71863e8f3e3d12e18b7ff2452e9f5b05b Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Wed, 30 Jun 2021 17:44:46 -0700 Subject: [PATCH] Refactor to return results by value, add wallet list_unspent and related types --- .gitignore | 1 - README.md | 18 ++++ build.sh | 1 + src/lib.rs | 1 + src/types.rs | 34 ++++++++ src/wallet.rs | 231 ++++++++++++++++++++++++++++---------------------- test.sh | 6 +- 7 files changed, 188 insertions(+), 104 deletions(-) create mode 100644 README.md create mode 100644 src/types.rs diff --git a/.gitignore b/.gitignore index 9bf4fb6..fd662a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ target build Cargo.lock -*.h /bdk-kotlin/local.properties .gradle wallet_db diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d3f227 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ + + +Adding new structs and functions + +1. Create C safe Rust structs and related functions using safer-ffi + +2. Test generated library and `bdk_ffi.h` file with c language tests in `cc/bdk_ffi_test.c` + +3. Use `build.sh` and `test.sh` to build c test program and verify functionality and + memory de-allocation via `valgrind` + +4. Update the kotlin native interface LibJna.kt in the `bdk-kotlin` `jvm` module to match `bdk_ffi.h` + +5. Create kotlin wrapper classes and interfaces as needed + +6. Add tests to `bdk-kotlin` `test-fixtures` module + +7. Use `build.sh` and `test.sh` to build and test `bdk-kotlin` `jvm` and `android` modules \ No newline at end of file diff --git a/build.sh b/build.sh index bbcc9bf..6b5a530 100755 --- a/build.sh +++ b/build.sh @@ -2,6 +2,7 @@ set -eo pipefail -o xtrace # rust +cargo fmt cargo build cargo test --features c-headers -- generate_headers diff --git a/src/lib.rs b/src/lib.rs index 72e9910..e1a9c00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ mod blockchain; mod database; mod error; +mod types; mod wallet; /// The following test function is necessary for the header generation. diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..25a1f2a --- /dev/null +++ b/src/types.rs @@ -0,0 +1,34 @@ +use ::safer_ffi::prelude::*; +use safer_ffi::char_p::char_p_boxed; + +#[derive_ReprC] +#[repr(C)] +#[derive(Debug)] +pub struct FfiResult { + pub ok: Option>, + pub err: Option>, +} + +#[derive_ReprC] +#[repr(C)] +pub struct FfiResultVec { + pub ok: repr_c::Vec, + pub err: Option>, +} + +#[ffi_export] +fn free_string_result(string_result: FfiResult) { + drop(string_result) +} + +#[ffi_export] +fn free_void_result(void_result: FfiResult<()>) { + drop(void_result) +} + +// TODO do we need this? remove? +/// Frees a Rust-allocated string +#[ffi_export] +fn free_string(string: Option) { + drop(string) +} diff --git a/src/wallet.rs b/src/wallet.rs index 454f547..b7996dc 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,82 +1,23 @@ -use crate::blockchain::BlockchainConfig; -use crate::database::DatabaseConfig; -use crate::error::get_name; +use std::convert::TryFrom; + use ::safer_ffi::prelude::*; use bdk::bitcoin::network::constants::Network::Testnet; -use bdk::blockchain::{ - log_progress, AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain, - ElectrumBlockchainConfig, -}; +use bdk::blockchain::{log_progress, AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain}; use bdk::database::{AnyDatabase, AnyDatabaseConfig, ConfigurableDatabase}; use bdk::wallet::AddressIndex::New; use bdk::{Error, Wallet}; use safer_ffi::boxed::Box; use safer_ffi::char_p::{char_p_boxed, char_p_ref}; -#[derive_ReprC] -#[ReprC::opaque] -pub struct VoidResult { - raw: Result<(), Error>, -} - -#[ffi_export] -fn get_void_err(void_result: &VoidResult) -> Option { - void_result - .raw - .as_ref() - .err() - .map(|e| char_p_boxed::try_from(get_name(e)).unwrap()) -} - -#[ffi_export] -fn free_void_result(void_result: Option>) { - drop(void_result) -} +use crate::blockchain::BlockchainConfig; +use crate::database::DatabaseConfig; +use crate::error::get_name; +use crate::types::{FfiResult, FfiResultVec}; #[derive_ReprC] #[ReprC::opaque] -pub struct StringResult { - raw: Result, -} - -#[ffi_export] -fn get_string_ok(string_result: &StringResult) -> Option { - string_result - .raw - .as_ref() - .ok() - .map(|s| char_p_boxed::try_from(s.clone()).unwrap()) -} - -#[ffi_export] -fn get_string_err(string_result: &StringResult) -> Option { - string_result - .raw - .as_ref() - .err() - .map(|e| char_p_boxed::try_from(get_name(e)).unwrap()) -} - -#[ffi_export] -fn free_string_result(string_result: Option>) { - drop(string_result) -} - -#[derive_ReprC] -#[ReprC::opaque] -pub struct WalletRef<'lt> { - raw: &'lt Wallet, -} - -#[ffi_export] -fn free_wallet_ref(wallet_ref: Option>) { - drop(wallet_ref) -} - -#[derive_ReprC] -#[ReprC::opaque] -pub struct WalletResult { - raw: Result, Error>, +pub struct OpaqueWallet { + raw: Wallet, } #[ffi_export] @@ -85,13 +26,83 @@ fn new_wallet_result( change_descriptor: Option, blockchain_config: &BlockchainConfig, database_config: &DatabaseConfig, -) -> Box { +) -> FfiResult { let descriptor = descriptor.to_string(); let change_descriptor = change_descriptor.map(|s| s.to_string()); let bc_config = &blockchain_config.raw; let db_config = &database_config.raw; let wallet_result = new_wallet(descriptor, change_descriptor, bc_config, db_config); - Box::new(WalletResult { raw: wallet_result }) + + match wallet_result { + Ok(w) => FfiResult { + ok: Some(Box::new(OpaqueWallet { raw: w })), + err: None, + }, + Err(e) => FfiResult { + ok: None, + err: Some(Box::new(char_p_boxed::try_from(get_name(&e)).unwrap())), + }, + } +} + +#[ffi_export] +fn free_wallet_result(wallet_result: FfiResult) { + drop(wallet_result); +} + +#[ffi_export] +fn free_unspent_result(unspent_result: FfiResultVec) { + drop(unspent_result) +} + +#[ffi_export] +fn sync_wallet(opaque_wallet: &OpaqueWallet) -> FfiResult<()> { + let void_result = opaque_wallet.raw.sync(log_progress(), Some(100)); + match void_result { + Ok(v) => FfiResult { + ok: Some(Box::new(v)), + err: None, + }, + Err(e) => FfiResult { + ok: None, + err: Some(Box::new(char_p_boxed::try_from(get_name(&e)).unwrap())), + }, + } +} + +#[ffi_export] +fn new_address(opaque_wallet: &OpaqueWallet) -> FfiResult { + let new_address = opaque_wallet.raw.get_address(New); + let string_result = new_address.map(|a| a.to_string()); + match string_result { + Ok(a) => FfiResult { + ok: Some(Box::new(char_p_boxed::try_from(a).unwrap())), + err: None, + }, + Err(e) => FfiResult { + ok: None, + err: Some(Box::new(char_p_boxed::try_from(get_name(&e)).unwrap())), + }, + } +} + +#[ffi_export] +fn list_unspent(opaque_wallet: &OpaqueWallet) -> FfiResultVec { + let unspent_result = opaque_wallet.raw.list_unspent(); + + match unspent_result { + Ok(v) => FfiResultVec { + ok: { + let ve: Vec = v.iter().map(|lu| LocalUtxo::from(lu)).collect(); + repr_c::Vec::from(ve) + }, + err: None, + }, + Err(e) => FfiResultVec { + ok: repr_c::Vec::EMPTY, + err: Some(Box::new(char_p_boxed::try_from(get_name(&e)).unwrap())), + }, + } } fn new_wallet( @@ -111,44 +122,64 @@ fn new_wallet( Wallet::new(descriptor, change_descriptor, network, database, client) } -#[ffi_export] -fn get_wallet_err(wallet_result: &WalletResult) -> Option { - wallet_result - .raw - .as_ref() - .err() - .map(|e| char_p_boxed::try_from(get_name(&e)).unwrap()) +// Non-opaque returned structs + +#[derive_ReprC] +#[repr(C)] +#[derive(Debug, Clone)] +pub struct OutPoint { + /// The referenced transaction's txid, as hex string + pub txid: char_p_boxed, + /// The index of the referenced output in its transaction's vout + pub vout: u32, } -#[ffi_export] -fn get_wallet_ok<'lt>(wallet_result: &'lt WalletResult) -> Option>> { - wallet_result - .raw - .as_ref() - .ok() - .map(|w| Box::new(WalletRef { raw: w })) +impl From<&bdk::bitcoin::OutPoint> for OutPoint { + fn from(op: &bdk::bitcoin::OutPoint) -> Self { + OutPoint { + txid: char_p_boxed::try_from(op.txid.to_string()).unwrap(), + vout: op.vout, + } + } } -#[ffi_export] -fn sync_wallet<'lt>(wallet_ref: &'lt WalletRef<'lt>) -> Box { - let void_result = wallet_ref.raw.sync(log_progress(), Some(100)); - Box::new(VoidResult { raw: void_result }) +#[derive_ReprC] +#[repr(C)] +#[derive(Debug, Clone)] +pub struct TxOut { + /// The value of the output, in satoshis + pub value: u64, + /// The script which must satisfy for the output to be spent, as hex string + pub script_pubkey: char_p_boxed, } -#[ffi_export] -fn new_address<'lt>(wallet_ref: &'lt WalletRef<'lt>) -> Box { - let new_address = wallet_ref.raw.get_address(New); - let string_result = new_address.map(|a| a.to_string()); - Box::new(StringResult { raw: string_result }) +impl From<&bdk::bitcoin::TxOut> for TxOut { + fn from(to: &bdk::bitcoin::TxOut) -> Self { + TxOut { + value: to.value, + script_pubkey: char_p_boxed::try_from(to.script_pubkey.to_string()).unwrap(), + } + } } -#[ffi_export] -fn free_wallet_result(wallet_result: Option>) { - drop(wallet_result) +#[derive_ReprC] +#[repr(C)] +#[derive(Debug, Clone)] +pub struct LocalUtxo { + /// Reference to a transaction output + pub outpoint: OutPoint, + /// Transaction output + pub txout: TxOut, + /// Type of keychain, as short 0 for "external" or 1 for "internal" + pub keychain: u16, } -/// Frees a Rust-allocated string -#[ffi_export] -fn free_string(string: Option) { - drop(string) +impl From<&bdk::LocalUtxo> for LocalUtxo { + fn from(lu: &bdk::LocalUtxo) -> Self { + LocalUtxo { + outpoint: OutPoint::from(&lu.outpoint), + txout: TxOut::from(&lu.txout), + keychain: lu.keychain as u16, + } + } } diff --git a/test.sh b/test.sh index 03855c5..99a3ac8 100755 --- a/test.sh +++ b/test.sh @@ -6,9 +6,9 @@ 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 +valgrind --leak-check=full --show-leak-kinds=all cc/bdk_ffi_test +#cc/bdk_ffi_test -## bdk-kotlin +# bdk-kotlin (cd bdk-kotlin && gradle test) (cd bdk-kotlin && gradle :android:connectedDebugAndroidTest)