From 56f57082269034852b3c0bf6d49ab9e91ef287f3 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Thu, 13 Nov 2025 19:27:49 -0300 Subject: [PATCH 01/13] chore(`ffi`): expand errors --- rust/lib/{build.rs => build.rs.bak} | 0 rust/lib/src/lib.rs | 465 +++++++++++++++++----------- rust/lib/src/zingo.udl | 224 -------------- rust/local.Dockerfile | 40 ++- 4 files changed, 299 insertions(+), 430 deletions(-) rename rust/lib/{build.rs => build.rs.bak} (100%) delete mode 100644 rust/lib/src/zingo.udl diff --git a/rust/lib/build.rs b/rust/lib/build.rs.bak similarity index 100% rename from rust/lib/build.rs rename to rust/lib/build.rs.bak diff --git a/rust/lib/src/lib.rs b/rust/lib/src/lib.rs index bac9153d2..938a8bf64 100644 --- a/rust/lib/src/lib.rs +++ b/rust/lib/src/lib.rs @@ -1,4 +1,4 @@ -uniffi::include_scaffolding!("zingo"); +uniffi::setup_scaffolding!(); #[macro_use] extern crate lazy_static; @@ -39,13 +39,13 @@ use tokio::runtime::Runtime; use zcash_address::ZcashAddress; use zcash_primitives::memo::MemoBytes; use zcash_protocol::value::Zatoshis; +use zingo_common_components::protocol::activation_heights::for_test::{self, all_height_one_nus}; use zingolib::config::{ChainType, ZingoConfig, construct_lightwalletd_uri}; use zingolib::data::PollReport; use zingolib::data::proposal::total_fee; use zingolib::data::receivers::Receivers; use zingolib::data::receivers::transaction_request_from_receivers; use zingolib::lightclient::LightClient; -use zingo_common_components::protocol::activation_heights::for_test; use zingolib::utils::{conversion::address_from_str, conversion::txid_from_hex_encoded_str}; use zingolib::wallet::keys::{ WalletAddressRef, @@ -53,7 +53,7 @@ use zingolib::wallet::keys::{ }; use zingolib::wallet::{LightWallet, WalletBase, WalletSettings}; -#[derive(Debug, thiserror::Error)] +#[derive(uniffi::Enum, Debug, thiserror::Error)] pub enum ZingolibError { #[error("Error: Lightclient is not initialized")] LightclientNotInitialized, @@ -63,6 +63,69 @@ pub enum ZingolibError { Panic(String), } +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum InitError { + #[error("invalid input: {0}")] + InvalidInput(String), + + #[error("base64 decode failed")] + Base64Decode, + + #[error("config error")] + Config(#[from] ConfigError), + + #[error("wallet read failed")] + WalletRead, + + #[error("lightwalletd query failed")] + Network, + + #[error("anchor height underflow (tip {tip}, offset {offset})")] + HeightUnderflow { tip: u32, offset: u32 }, + + #[error("lightclient creation failed")] + LightClient, + + #[error("wallet creation failed")] + WalletNew, + + #[error("seed error")] + Seed(#[from] SeedError), + + #[error("ufvk error")] + Ufvk(#[from] UfvkError), + + #[error("mnemonic parsing failed")] + Mnemonic, +} + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum ConfigError { + #[error("invalid chain hint: {0}")] + InvalidChainHint(String), + + #[error("invalid performance level: {0}")] + InvalidPerformanceLevel(String), + + #[error("invalid min_confirmations: {0}")] + InvalidMinConfirmations(String), + + #[error("loading client config failed")] + Load, +} + +#[derive(Debug, Clone, uniffi::Record)] +pub struct InitResult { + pub kind: InitResultKind, + pub value: String, +} + +#[derive(Debug, Clone, uniffi::Enum)] +pub enum InitResultKind { + Seed, + Ufvk, +} + pub fn with_panic_guard(f: F) -> Result where F: FnOnce() -> Result + UnwindSafe, @@ -231,24 +294,28 @@ fn construct_uri_load_config( chain_hint: String, performance_level: String, min_confirmations: u32, -) -> Result<(ZingoConfig, http::Uri), String> { +) -> Result<(ZingoConfig, http::Uri), ConfigError> { // if uri is empty -> Offline Mode. let lightwalletd_uri = construct_lightwalletd_uri(Some(uri)); let chaintype = match chain_hint.as_str() { "main" => ChainType::Mainnet, "test" => ChainType::Testnet, - "regtest" => ChainType::Regtest(for_test::all_height_one_nus()), - _ => return Err("Error: Not a valid chain hint!".to_string()), + "regtest" => ChainType::Regtest(all_height_one_nus()), + _ => return Err(ConfigError::InvalidChainHint(chain_hint)), }; let performancetype = match performance_level.as_str() { "Maximum" => PerformanceLevel::Maximum, "High" => PerformanceLevel::High, "Medium" => PerformanceLevel::Medium, "Low" => PerformanceLevel::Low, - _ => return Err("Error: Not a valid performance level!".to_string()), + _ => return Err(ConfigError::InvalidPerformanceLevel(performance_level)), }; - let config = match zingolib::config::load_clientconfig( + + let confirmations = NonZeroU32::try_from(min_confirmations) + .map_err(|_| ConfigError::InvalidMinConfirmations(min_confirmations.to_string()))?; + + let config = zingolib::config::load_clientconfig( lightwalletd_uri.clone(), None, chaintype, @@ -257,16 +324,12 @@ fn construct_uri_load_config( transparent_address_discovery: TransparentAddressDiscovery::minimal(), performance_level: performancetype, }, - min_confirmations: NonZeroU32::try_from(min_confirmations).unwrap(), + min_confirmations: confirmations, }, NonZeroU32::try_from(1).expect("hard-coded integer"), - "".to_string() - ) { - Ok(c) => c, - Err(e) => { - return Err(format!("Error: Config load: {e}")); - } - }; + "".to_string(), + ) + .map_err(|_| ConfigError::Load)?; Ok((config, lightwalletd_uri)) } @@ -291,34 +354,38 @@ pub fn init_new( chain_hint: String, performance_level: String, min_confirmations: u32, -) -> Result { - with_panic_guard(|| { - reset_lightclient(); - let (config, lightwalletd_uri) = match construct_uri_load_config( - server_uri, - chain_hint, - performance_level, - min_confirmations, - ) { - Ok(c) => c, - Err(e) => return Ok(format!("Error: {e}")), - }; - let chain_height = match RT.block_on(async move { +) -> Result { + // with_panic_guard(|| { + reset_lightclient(); + let (config, lightwalletd_uri) = + construct_uri_load_config(server_uri, chain_hint, performance_level, min_confirmations)?; + + // Query tip height + let tip: u32 = RT + .block_on(async move { zingolib::grpc_connector::get_latest_block(lightwalletd_uri) .await - .map(|block_id| BlockHeight::from_u32(block_id.height as u32)) - }) { - Ok(h) => h, - Err(e) => return Ok(format!("Error: {e}")), - }; - let lightclient = match LightClient::new(config, chain_height - 100, false) { - Ok(l) => l, - Err(e) => return Ok(format!("Error: {e}")), - }; - let _ = store_client(lightclient); - - get_seed() + .map(|b| b.height as u32) + }) + .map_err(|_| InitError::Network)?; + + // Derive anchor height safely + let offset = 100u32; + let anchor = tip + .checked_sub(offset) + .ok_or(InitError::HeightUnderflow { tip, offset })?; + let anchor = BlockHeight::from_u32(anchor); + + let lightclient = + LightClient::new(config, anchor, false).map_err(|_| InitError::LightClient)?; + let _ = store_client(lightclient); + + let seed = get_seed()?; + Ok(InitResult { + kind: InitResultKind::Seed, + value: seed, }) + // })? } // TODO: change `seed` to `seed_phrase` or `mnemonic_phrase` @@ -329,42 +396,36 @@ pub fn init_from_seed( chain_hint: String, performance_level: String, min_confirmations: u32, -) -> Result { - with_panic_guard(|| { - reset_lightclient(); - let (config, _lightwalletd_uri) = match construct_uri_load_config( - server_uri, - chain_hint, - performance_level, - min_confirmations, - ) { - Ok(c) => c, - Err(e) => return Ok(format!("Error: {e}")), - }; - let mnemonic = match Mnemonic::from_phrase(seed) { - Ok(m) => m, - Err(e) => return Ok(format!("Error: {e}")), - }; - let wallet = match LightWallet::new( - config.chain, - WalletBase::Mnemonic { - mnemonic, - no_of_accounts: config.no_of_accounts, - }, - BlockHeight::from_u32(birthday), - config.wallet_settings.clone(), - ) { - Ok(w) => w, - Err(e) => return Ok(format!("Error: {e}")), - }; - let lightclient = match LightClient::create_from_wallet(wallet, config, false) { - Ok(l) => l, - Err(e) => return Ok(format!("Error: {e}")), - }; - let _ = store_client(lightclient); +) -> Result { + // with_panic_guard(|| { + reset_lightclient(); + + let (config, _lightwalletd_uri) = + construct_uri_load_config(server_uri, chain_hint, performance_level, min_confirmations)?; - get_seed() + let mnemonic = Mnemonic::from_phrase(seed).map_err(|_| InitError::Mnemonic)?; + + let wallet = LightWallet::new( + config.chain, + WalletBase::Mnemonic { + mnemonic, + no_of_accounts: config.no_of_accounts, + }, + BlockHeight::from_u32(birthday), + config.wallet_settings.clone(), + ) + .map_err(|_| InitError::WalletNew)?; + + let lightclient = LightClient::create_from_wallet(wallet, config, false) + .map_err(|_| InitError::LightClient)?; + let _ = store_client(lightclient); + + let seed = get_seed()?; + Ok(InitResult { + kind: InitResultKind::Seed, + value: seed, }) + // }) } pub fn init_from_ufvk( @@ -374,86 +435,80 @@ pub fn init_from_ufvk( chain_hint: String, performance_level: String, min_confirmations: u32, -) -> Result { - with_panic_guard(|| { - reset_lightclient(); - let (config, _lightwalletd_uri) = match construct_uri_load_config( - server_uri, - chain_hint, - performance_level, - min_confirmations, - ) { - Ok(c) => c, - Err(e) => return Ok(format!("Error: {e}")), - }; - let wallet = match LightWallet::new( - config.chain, - WalletBase::Ufvk(ufvk), - BlockHeight::from_u32(birthday), - config.wallet_settings.clone(), - ) { - Ok(w) => w, - Err(e) => return Ok(format!("Error: {e}")), - }; - let lightclient = match LightClient::create_from_wallet(wallet, config, false) { - Ok(l) => l, - Err(e) => return Ok(format!("Error: {e}")), - }; - let _ = store_client(lightclient); - - get_ufvk() +) -> Result { + // with_panic_guard(|| { + reset_lightclient(); + let (config, _lightwalletd_uri) = + construct_uri_load_config(server_uri, chain_hint, performance_level, min_confirmations)?; + + let wallet = LightWallet::new( + config.chain, + WalletBase::Ufvk(ufvk), + BlockHeight::from_u32(birthday), + config.wallet_settings.clone(), + ) + .map_err(|_| InitError::WalletNew)?; + + let lightclient = LightClient::create_from_wallet(wallet, config, false) + .map_err(|_| InitError::LightClient)?; + let _ = store_client(lightclient); + + let seed = get_ufvk()?; + Ok(InitResult { + kind: InitResultKind::Ufvk, + value: seed.ufvk, }) + // }) } +#[uniffi::export] pub fn init_from_b64( base64_data: String, server_uri: String, chain_hint: String, performance_level: String, min_confirmations: u32, -) -> Result { - with_panic_guard(|| { - reset_lightclient(); - let (config, _lightwalletd_uri) = match construct_uri_load_config( - server_uri, - chain_hint, - performance_level, - min_confirmations, - ) { - Ok(c) => c, - Err(e) => return Ok(format!("Error: {e}")), - }; - let decoded_bytes = match STANDARD.decode(&base64_data) { - Ok(b) => b, - Err(e) => { - return Ok(format!( - "Error: Decoding Base64: {}, Size: {}, Content: {}", - e, - base64_data.len(), - base64_data - )); - } - }; +) -> Result { + // with_panic_guard(|| { + reset_lightclient(); + let (config, _lightwalletd_uri) = + construct_uri_load_config(server_uri, chain_hint, performance_level, min_confirmations)?; - let wallet = match LightWallet::read(&decoded_bytes[..], config.chain) { - Ok(w) => w, - Err(e) => return Ok(format!("Error: {e}")), - }; - let has_seed = wallet.mnemonic().is_some(); - let lightclient = match LightClient::create_from_wallet(wallet, config, false) { - Ok(l) => l, - Err(e) => return Ok(format!("Error: {e}")), - }; - let _ = store_client(lightclient); + let decoded_bytes = base64::engine::general_purpose::STANDARD + .decode(&base64_data) + .map_err(|_| InitError::Base64Decode)?; - if has_seed { get_seed() } else { get_ufvk() } - }) + let wallet = + LightWallet::read(&decoded_bytes[..], config.chain).map_err(|_| InitError::WalletRead)?; + + let has_seed = wallet.mnemonic().is_some(); + + let lightclient = LightClient::create_from_wallet(wallet, config, false) + .map_err(|_| InitError::LightClient)?; + + let _ = store_client(lightclient); + + if has_seed { + Ok(InitResult { + kind: InitResultKind::Seed, + value: get_seed()?, + }) + } else { + Ok(InitResult { + kind: InitResultKind::Ufvk, + value: get_ufvk()?.to_string(), + }) + } + // }) + // .map(ZingolibError::from)? } pub fn save_to_b64() -> Result { with_panic_guard(|| { // Return the wallet as a base64 encoded string - let mut guard = LIGHTCLIENT.write().map_err(|_| ZingolibError::LightclientLockPoisoned)?; + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; if let Some(lightclient) = &mut *guard { // we need to use STANDARD because swift is expecting the encoded String with padding // I tried with STANDARD_NO_PAD and the decoding return `nil`. @@ -576,6 +631,7 @@ pub fn poll_sync() -> Result { }) } +#[uniffi::export] fn run_sync() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -615,6 +671,7 @@ pub fn pause_sync() -> Result { }) } +#[uniffi::export] fn status_sync() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -665,56 +722,94 @@ pub fn info_server() -> Result { }) } +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum SeedError { + #[error("lightclient not initialized")] + NotInitialized, + + #[error("failed to lock lightclient")] + LockPoisoned, + + #[error("no mnemonic found (wallet loaded from key)")] + NoMnemonic, + + #[error("failed to serialize recovery info")] + Serialize, +} + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum UfvkError { + #[error("lightclient not initialized")] + NotInitialized, + + #[error("failed to lock lightclient")] + LockPoisoned, + + #[error("account 0 not found")] + NoAccount0, + + #[error("account 0 could not be converted to UnifiedFullViewingKey")] + NotUfvk, +} + +#[derive(Debug, Clone, uniffi::Record)] +pub struct UfvkInfo { + pub ufvk: String, + pub birthday: u32, +} + +impl ToString for UfvkInfo { + fn to_string(&self) -> String { + self.ufvk.clone() + } +} + // TODO: rename "get_seed_phrase" or "get_mnemonic_phrase" // or if other recovery info is being used could rename "get_recovery_info" ? -pub fn get_seed() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.read().await; - match wallet.recovery_info() { - Some(recovery_info) => serde_json::to_string_pretty(&recovery_info) - .unwrap_or_else(|_| "Error: get seed. failed to serialize".to_string()), - None => { - "Error: get seed. no mnemonic found. wallet loaded from key.".to_string() - } - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) +pub fn get_seed() -> Result { + let wallet_handle = { + let mut guard = LIGHTCLIENT.write().map_err(|_| SeedError::LockPoisoned)?; + let Some(lightclient) = &mut *guard else { + return Err(SeedError::NotInitialized); + }; + // Get a handle we can await on without the LIGHTCLIENT lock + lightclient.wallet.clone() + }; + + let recovery_json = RT.block_on(async move { + let wallet = wallet_handle.read().await; + let Some(recovery_info) = wallet.recovery_info() else { + return Err(SeedError::NoMnemonic); + }; + serde_json::to_string_pretty(&recovery_info).map_err(|_| SeedError::Serialize) + })?; + + Ok(recovery_json) } -pub fn get_ufvk() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.read().await; - let ufvk: UnifiedFullViewingKey = match wallet - .unified_key_store - .get(&AccountId::ZERO) - .expect("account 0 must always exist") - .try_into() - { - Ok(ufvk) => ufvk, - Err(e) => return format!("Error: {e}"), - }; - object! { - "ufvk" => ufvk.encode(&wallet.network), - "birthday" => u32::from(wallet.birthday) - } - .pretty(2) - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } +pub fn get_ufvk() -> Result { + let wallet_handle = { + let mut guard = LIGHTCLIENT.write().map_err(|_| UfvkError::LockPoisoned)?; + let Some(lightclient) = &mut *guard else { + return Err(UfvkError::NotInitialized); + }; + lightclient.wallet.clone() + }; + + RT.block_on(async move { + let wallet = wallet_handle.read().await; + + // Avoid `expect("account 0 must always exist")` + let Some(k) = wallet.unified_key_store.get(&AccountId::ZERO) else { + return Err(UfvkError::NoAccount0); + }; + + let ufvk: UnifiedFullViewingKey = k.try_into().map_err(|_| UfvkError::NotUfvk)?; + + Ok(UfvkInfo { + ufvk: ufvk.encode(&wallet.network), + birthday: u32::from(wallet.birthday), + }) }) } diff --git a/rust/lib/src/zingo.udl b/rust/lib/src/zingo.udl deleted file mode 100644 index 09069ada3..000000000 --- a/rust/lib/src/zingo.udl +++ /dev/null @@ -1,224 +0,0 @@ -[Error] -enum ZingolibError { - "LightclientNotInitialized", - "LightclientLockPoisoned", - "Panic" -}; - -namespace zingo { - - [Throws=ZingolibError] - string init_logging(); - - [Throws=ZingolibError] - string init_new( - string serveruri, - string chainhint, - string performancelevel, - u32 minconfirmations - ); - - [Throws=ZingolibError] - string init_from_seed( - string seed, - u32 birthday, - string serveruri, - string chainhint, - string performancelevel, - u32 minconfirmations - ); - - [Throws=ZingolibError] - string init_from_ufvk( - string ufvk, - u32 birthday, - string serveruri, - string chainhint, - string performancelevel, - u32 minconfirmations - ); - - [Throws=ZingolibError] - string init_from_b64( - string datab64, - string serveruri, - string chainhint, - string performancelevel, - u32 minconfirmations - ); - - [Throws=ZingolibError] - string save_to_b64(); - - string check_b64( - string datab64 - ); - - [Throws=ZingolibError] - string get_latest_block_server( - string serveruri - ); - - [Throws=ZingolibError] - string get_latest_block_wallet(); - - [Throws=ZingolibError] - string get_developer_donation_address(); - - [Throws=ZingolibError] - string get_zennies_for_zingo_donation_address(); - - [Throws=ZingolibError] - string get_value_transfers(); - - [Throws=ZingolibError] - string set_crypto_default_provider_to_ring(); - - [Throws=ZingolibError] - string poll_sync(); - - [Throws=ZingolibError] - string run_sync(); - - [Throws=ZingolibError] - string pause_sync(); - - [Throws=ZingolibError] - string status_sync(); - - [Throws=ZingolibError] - string run_rescan(); - - [Throws=ZingolibError] - string info_server(); - - [Throws=ZingolibError] - string get_seed(); - - [Throws=ZingolibError] - string get_ufvk(); - - [Throws=ZingolibError] - string change_server( - string serveruri - ); - - [Throws=ZingolibError] - string wallet_kind(); - - [Throws=ZingolibError] - string parse_address( - string address - ); - - [Throws=ZingolibError] - string parse_ufvk( - string ufvk - ); - - [Throws=ZingolibError] - string get_version(); - - [Throws=ZingolibError] - string get_messages( - string address - ); - - [Throws=ZingolibError] - string get_balance(); - - [Throws=ZingolibError] - string get_total_memobytes_to_address(); - - [Throws=ZingolibError] - string get_total_value_to_address(); - - [Throws=ZingolibError] - string get_total_spends_to_address(); - - [Throws=ZingolibError] - string zec_price( - string tor - ); - - [Throws=ZingolibError] - string resend_transaction( - string txid - ); - - [Throws=ZingolibError] - string remove_transaction( - string txid - ); - - [Throws=ZingolibError] - string get_spendable_balance_with_address( - string address, - string zennies - ); - - [Throws=ZingolibError] - string get_spendable_balance_total(); - - [Throws=ZingolibError] - string set_option_wallet(); - - [Throws=ZingolibError] - string get_option_wallet(); - - [Throws=ZingolibError] - string create_tor_client( - string datadir - ); - - [Throws=ZingolibError] - string remove_tor_client(); - - [Throws=ZingolibError] - string get_unified_addresses(); - - [Throws=ZingolibError] - string get_transparent_addresses(); - - [Throws=ZingolibError] - string create_new_unified_address( - string receivers - ); - - [Throws=ZingolibError] - string create_new_transparent_address(); - - [Throws=ZingolibError] - string check_my_address( - string address - ); - - [Throws=ZingolibError] - string get_wallet_save_required(); - - [Throws=ZingolibError] - string set_config_wallet_to_test(); - - [Throws=ZingolibError] - string set_config_wallet_to_prod( - string performancelevel, - u32 minconfirmations - ); - - [Throws=ZingolibError] - string get_config_wallet_performance(); - - [Throws=ZingolibError] - string get_wallet_version(); - - [Throws=ZingolibError] - string send( - string send_json - ); - - [Throws=ZingolibError] - string shield(); - - [Throws=ZingolibError] - string confirm(); -}; diff --git a/rust/local.Dockerfile b/rust/local.Dockerfile index c7ef79b02..7cb5cd05e 100644 --- a/rust/local.Dockerfile +++ b/rust/local.Dockerfile @@ -16,38 +16,36 @@ WORKDIR /opt/zingo/rust/lib/ # TODO: use cargo chef WORKDIR /opt/zingo/rust/lib/ - -# Copy just the lib crate to avoid docker cache invalidation -COPY lib/ ./ - -COPY Cargo.lock ./Cargo.lock - ENV CARGO_TARGET_DIR=/opt/zingo/rust/target +ENV LIBCLANG_PATH=/usr/lib/llvm-18/lib +# forcing to 24 API LEVEL +ENV CARGO_NDK_PLATFORM=24 +ENV CARGO_NDK_ANDROID_PLATFORM=24 +ENV AR=llvm-ar +ENV RANLIB=llvm-ranlib RUN rustup default nightly +RUN rustup target add x86_64-linux-android RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash +RUN cargo binstall --force --locked bindgen-cli -RUN rustup target add x86_64-linux-android +RUN cargo binstall --version 4.0.1 cargo-ndk -RUN cargo binstall --force --locked bindgen-cli +# Copy just the lib crate to avoid docker cache invalidation +COPY lib/ ./ -RUN cargo run --release --features=uniffi/cli --bin uniffi-bindgen \ - generate ./src/zingo.udl --language kotlin \ - --out-dir ./src +COPY Cargo.lock ./Cargo.lock -RUN cargo binstall --version 4.0.1 cargo-ndk +RUN cargo build --release -ENV LIBCLANG_PATH=/usr/lib/llvm-18/lib -# forcing to 24 API LEVEL -ENV CARGO_NDK_PLATFORM=24 -ENV CARGO_NDK_ANDROID_PLATFORM=24 -ENV AR=llvm-ar -ENV RANLIB=llvm-ranlib +RUN cargo run --release --features=uniffi/cli --bin uniffi-bindgen \ + generate --library ../target/release/libzingo.so --language kotlin \ + --out-dir ./src RUN cargo ndk --target x86_64-linux-android build --release -RUN llvm-strip --strip-all ../target/x86_64-linux-android/release/libzingo.so +RUN llvm-strip --strip-all ../target/release/libzingo.so RUN llvm-objcopy \ --remove-section .comment \ - ../target/x86_64-linux-android/release/libzingo.so -RUN sha256sum ../target/x86_64-linux-android/release/libzingo.so + ../target/release/libzingo.so +RUN sha256sum ../target/release/libzingo.so From 3d3fe74ff1e1fcc090c9b5eb917da28456f6b4c9 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Fri, 14 Nov 2025 01:05:22 -0300 Subject: [PATCH 02/13] chore(`ffi`): re-apply panic guard --- rust/lib/build.rs.bak | 3 - rust/lib/src/lib.rs | 350 +++++++++++++++++++++++++----------------- 2 files changed, 211 insertions(+), 142 deletions(-) delete mode 100644 rust/lib/build.rs.bak diff --git a/rust/lib/build.rs.bak b/rust/lib/build.rs.bak deleted file mode 100644 index 5ae42f5a4..000000000 --- a/rust/lib/build.rs.bak +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - uniffi_build::generate_scaffolding("src/zingo.udl").expect("A valid UDL file"); -} diff --git a/rust/lib/src/lib.rs b/rust/lib/src/lib.rs index 938a8bf64..d19526fd5 100644 --- a/rust/lib/src/lib.rs +++ b/rust/lib/src/lib.rs @@ -12,7 +12,7 @@ use log::Level; use std::any::Any; use std::backtrace::Backtrace; use std::num::NonZeroU32; -use std::panic::{self, PanicHookInfo, UnwindSafe}; +use std::panic::{self, PanicHookInfo}; use std::str::FromStr; use std::sync::Mutex; use std::sync::Once; @@ -53,14 +53,20 @@ use zingolib::wallet::keys::{ }; use zingolib::wallet::{LightWallet, WalletBase, WalletSettings}; -#[derive(uniffi::Enum, Debug, thiserror::Error)] +#[derive(uniffi::Error, Debug, thiserror::Error)] pub enum ZingolibError { #[error("Error: Lightclient is not initialized")] LightclientNotInitialized, #[error("Error: Lightclient lock poisoned")] LightclientLockPoisoned, - #[error("Error: panic: {0}")] - Panic(String), + #[error("Panic")] + Panic, +} + +impl FromPanic for ZingolibError { + fn from_panic(_msg: String) -> Self { + ZingolibError::Panic + } } #[derive(Debug, thiserror::Error, uniffi::Error)] @@ -97,6 +103,15 @@ pub enum InitError { #[error("mnemonic parsing failed")] Mnemonic, + + #[error("panic")] + Panic, +} + +impl FromPanic for InitError { + fn from_panic(_msg: String) -> Self { + InitError::Panic + } } #[derive(Debug, thiserror::Error, uniffi::Error)] @@ -112,6 +127,15 @@ pub enum ConfigError { #[error("loading client config failed")] Load, + + #[error("panic")] + Panic, +} + +impl FromPanic for ConfigError { + fn from_panic(_msg: String) -> Self { + ConfigError::Panic + } } #[derive(Debug, Clone, uniffi::Record)] @@ -126,17 +150,28 @@ pub enum InitResultKind { Ufvk, } -pub fn with_panic_guard(f: F) -> Result +pub trait FromPanic { + fn from_panic(msg: String) -> Self; +} + +pub fn with_panic_guard(f: F) -> Result where - F: FnOnce() -> Result + UnwindSafe, + F: FnOnce() -> Result + std::panic::UnwindSafe, + E: FromPanic, { install_panic_hook_once(); match panic::catch_unwind(f) { Ok(res) => res, - Err(payload) => Err(ZingolibError::Panic(format_panic_text(payload))), + Err(payload) => Err(E::from_panic(format_panic_text(payload))), } } +#[uniffi::export] +pub fn last_panic_message() -> String { + // summarize what you saved in `LAST_PANIC` + take_last_panic().msg +} + #[derive(Clone, Default)] struct PanicReport { msg: String, @@ -355,37 +390,41 @@ pub fn init_new( performance_level: String, min_confirmations: u32, ) -> Result { - // with_panic_guard(|| { - reset_lightclient(); - let (config, lightwalletd_uri) = - construct_uri_load_config(server_uri, chain_hint, performance_level, min_confirmations)?; - - // Query tip height - let tip: u32 = RT - .block_on(async move { - zingolib::grpc_connector::get_latest_block(lightwalletd_uri) - .await - .map(|b| b.height as u32) + with_panic_guard(|| { + reset_lightclient(); + let (config, lightwalletd_uri) = construct_uri_load_config( + server_uri, + chain_hint, + performance_level, + min_confirmations, + )?; + + // Query tip height + let tip: u32 = RT + .block_on(async move { + zingolib::grpc_connector::get_latest_block(lightwalletd_uri) + .await + .map(|b| b.height as u32) + }) + .map_err(|_| InitError::Network)?; + + // Derive anchor height safely + let offset = 100u32; + let anchor = tip + .checked_sub(offset) + .ok_or(InitError::HeightUnderflow { tip, offset })?; + let anchor = BlockHeight::from_u32(anchor); + + let lightclient = + LightClient::new(config, anchor, false).map_err(|_| InitError::LightClient)?; + let _ = store_client(lightclient); + + let seed = get_seed()?; + Ok(InitResult { + kind: InitResultKind::Seed, + value: seed, }) - .map_err(|_| InitError::Network)?; - - // Derive anchor height safely - let offset = 100u32; - let anchor = tip - .checked_sub(offset) - .ok_or(InitError::HeightUnderflow { tip, offset })?; - let anchor = BlockHeight::from_u32(anchor); - - let lightclient = - LightClient::new(config, anchor, false).map_err(|_| InitError::LightClient)?; - let _ = store_client(lightclient); - - let seed = get_seed()?; - Ok(InitResult { - kind: InitResultKind::Seed, - value: seed, }) - // })? } // TODO: change `seed` to `seed_phrase` or `mnemonic_phrase` @@ -397,35 +436,39 @@ pub fn init_from_seed( performance_level: String, min_confirmations: u32, ) -> Result { - // with_panic_guard(|| { - reset_lightclient(); - - let (config, _lightwalletd_uri) = - construct_uri_load_config(server_uri, chain_hint, performance_level, min_confirmations)?; - - let mnemonic = Mnemonic::from_phrase(seed).map_err(|_| InitError::Mnemonic)?; - - let wallet = LightWallet::new( - config.chain, - WalletBase::Mnemonic { - mnemonic, - no_of_accounts: config.no_of_accounts, - }, - BlockHeight::from_u32(birthday), - config.wallet_settings.clone(), - ) - .map_err(|_| InitError::WalletNew)?; + with_panic_guard(|| { + reset_lightclient(); + + let (config, _lightwalletd_uri) = construct_uri_load_config( + server_uri, + chain_hint, + performance_level, + min_confirmations, + )?; + + let mnemonic = Mnemonic::from_phrase(seed).map_err(|_| InitError::Mnemonic)?; + + let wallet = LightWallet::new( + config.chain, + WalletBase::Mnemonic { + mnemonic, + no_of_accounts: config.no_of_accounts, + }, + BlockHeight::from_u32(birthday), + config.wallet_settings.clone(), + ) + .map_err(|_| InitError::WalletNew)?; - let lightclient = LightClient::create_from_wallet(wallet, config, false) - .map_err(|_| InitError::LightClient)?; - let _ = store_client(lightclient); + let lightclient = LightClient::create_from_wallet(wallet, config, false) + .map_err(|_| InitError::LightClient)?; + let _ = store_client(lightclient); - let seed = get_seed()?; - Ok(InitResult { - kind: InitResultKind::Seed, - value: seed, + let seed = get_seed()?; + Ok(InitResult { + kind: InitResultKind::Seed, + value: seed, + }) }) - // }) } pub fn init_from_ufvk( @@ -436,29 +479,33 @@ pub fn init_from_ufvk( performance_level: String, min_confirmations: u32, ) -> Result { - // with_panic_guard(|| { - reset_lightclient(); - let (config, _lightwalletd_uri) = - construct_uri_load_config(server_uri, chain_hint, performance_level, min_confirmations)?; - - let wallet = LightWallet::new( - config.chain, - WalletBase::Ufvk(ufvk), - BlockHeight::from_u32(birthday), - config.wallet_settings.clone(), - ) - .map_err(|_| InitError::WalletNew)?; + with_panic_guard(|| { + reset_lightclient(); + let (config, _lightwalletd_uri) = construct_uri_load_config( + server_uri, + chain_hint, + performance_level, + min_confirmations, + )?; + + let wallet = LightWallet::new( + config.chain, + WalletBase::Ufvk(ufvk), + BlockHeight::from_u32(birthday), + config.wallet_settings.clone(), + ) + .map_err(|_| InitError::WalletNew)?; - let lightclient = LightClient::create_from_wallet(wallet, config, false) - .map_err(|_| InitError::LightClient)?; - let _ = store_client(lightclient); + let lightclient = LightClient::create_from_wallet(wallet, config, false) + .map_err(|_| InitError::LightClient)?; + let _ = store_client(lightclient); - let seed = get_ufvk()?; - Ok(InitResult { - kind: InitResultKind::Ufvk, - value: seed.ufvk, + let seed = get_ufvk()?; + Ok(InitResult { + kind: InitResultKind::Ufvk, + value: seed.ufvk, + }) }) - // }) } #[uniffi::export] @@ -469,38 +516,41 @@ pub fn init_from_b64( performance_level: String, min_confirmations: u32, ) -> Result { - // with_panic_guard(|| { - reset_lightclient(); - let (config, _lightwalletd_uri) = - construct_uri_load_config(server_uri, chain_hint, performance_level, min_confirmations)?; + with_panic_guard(|| { + reset_lightclient(); + let (config, _lightwalletd_uri) = construct_uri_load_config( + server_uri, + chain_hint, + performance_level, + min_confirmations, + )?; - let decoded_bytes = base64::engine::general_purpose::STANDARD - .decode(&base64_data) - .map_err(|_| InitError::Base64Decode)?; + let decoded_bytes = base64::engine::general_purpose::STANDARD + .decode(&base64_data) + .map_err(|_| InitError::Base64Decode)?; - let wallet = - LightWallet::read(&decoded_bytes[..], config.chain).map_err(|_| InitError::WalletRead)?; + let wallet = LightWallet::read(&decoded_bytes[..], config.chain) + .map_err(|_| InitError::WalletRead)?; - let has_seed = wallet.mnemonic().is_some(); + let has_seed = wallet.mnemonic().is_some(); - let lightclient = LightClient::create_from_wallet(wallet, config, false) - .map_err(|_| InitError::LightClient)?; + let lightclient = LightClient::create_from_wallet(wallet, config, false) + .map_err(|_| InitError::LightClient)?; - let _ = store_client(lightclient); + let _ = store_client(lightclient); - if has_seed { - Ok(InitResult { - kind: InitResultKind::Seed, - value: get_seed()?, - }) - } else { - Ok(InitResult { - kind: InitResultKind::Ufvk, - value: get_ufvk()?.to_string(), - }) - } - // }) - // .map(ZingolibError::from)? + if has_seed { + Ok(InitResult { + kind: InitResultKind::Seed, + value: get_seed()?, + }) + } else { + Ok(InitResult { + kind: InitResultKind::Ufvk, + value: get_ufvk()?.to_string(), + }) + } + }) } pub fn save_to_b64() -> Result { @@ -735,6 +785,15 @@ pub enum SeedError { #[error("failed to serialize recovery info")] Serialize, + + #[error("panic")] + Panic, +} + +impl FromPanic for SeedError { + fn from_panic(_msg: String) -> Self { + SeedError::Panic + } } #[derive(Debug, thiserror::Error, uniffi::Error)] @@ -750,6 +809,15 @@ pub enum UfvkError { #[error("account 0 could not be converted to UnifiedFullViewingKey")] NotUfvk, + + #[error("panic")] + Panic, +} + +impl FromPanic for UfvkError { + fn from_panic(_msg: String) -> Self { + UfvkError::Panic + } } #[derive(Debug, Clone, uniffi::Record)] @@ -767,48 +835,52 @@ impl ToString for UfvkInfo { // TODO: rename "get_seed_phrase" or "get_mnemonic_phrase" // or if other recovery info is being used could rename "get_recovery_info" ? pub fn get_seed() -> Result { - let wallet_handle = { - let mut guard = LIGHTCLIENT.write().map_err(|_| SeedError::LockPoisoned)?; - let Some(lightclient) = &mut *guard else { - return Err(SeedError::NotInitialized); + with_panic_guard(|| { + let wallet_handle = { + let mut guard = LIGHTCLIENT.write().map_err(|_| SeedError::LockPoisoned)?; + let Some(lightclient) = &mut *guard else { + return Err(SeedError::NotInitialized); + }; + // Get a handle we can await on without the LIGHTCLIENT lock + lightclient.wallet.clone() }; - // Get a handle we can await on without the LIGHTCLIENT lock - lightclient.wallet.clone() - }; - let recovery_json = RT.block_on(async move { - let wallet = wallet_handle.read().await; - let Some(recovery_info) = wallet.recovery_info() else { - return Err(SeedError::NoMnemonic); - }; - serde_json::to_string_pretty(&recovery_info).map_err(|_| SeedError::Serialize) - })?; + let recovery_json = RT.block_on(async move { + let wallet = wallet_handle.read().await; + let Some(recovery_info) = wallet.recovery_info() else { + return Err(SeedError::NoMnemonic); + }; + serde_json::to_string_pretty(&recovery_info).map_err(|_| SeedError::Serialize) + })?; - Ok(recovery_json) + Ok(recovery_json) + }) } pub fn get_ufvk() -> Result { - let wallet_handle = { - let mut guard = LIGHTCLIENT.write().map_err(|_| UfvkError::LockPoisoned)?; - let Some(lightclient) = &mut *guard else { - return Err(UfvkError::NotInitialized); + with_panic_guard(|| { + let wallet_handle = { + let mut guard = LIGHTCLIENT.write().map_err(|_| UfvkError::LockPoisoned)?; + let Some(lightclient) = &mut *guard else { + return Err(UfvkError::NotInitialized); + }; + lightclient.wallet.clone() }; - lightclient.wallet.clone() - }; - RT.block_on(async move { - let wallet = wallet_handle.read().await; + RT.block_on(async move { + let wallet = wallet_handle.read().await; - // Avoid `expect("account 0 must always exist")` - let Some(k) = wallet.unified_key_store.get(&AccountId::ZERO) else { - return Err(UfvkError::NoAccount0); - }; + // Avoid `expect("account 0 must always exist")` + let Some(k) = wallet.unified_key_store.get(&AccountId::ZERO) else { + return Err(UfvkError::NoAccount0); + }; - let ufvk: UnifiedFullViewingKey = k.try_into().map_err(|_| UfvkError::NotUfvk)?; + let ufvk: UnifiedFullViewingKey = k.try_into().map_err(|_| UfvkError::NotUfvk)?; - Ok(UfvkInfo { - ufvk: ufvk.encode(&wallet.network), - birthday: u32::from(wallet.birthday), + Ok(UfvkInfo { + ufvk: ufvk.encode(&wallet.network), + birthday: u32::from(wallet.birthday), + }) }) }) } From 0d179bb1619d5d28bb005b95e829078958030745 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Fri, 14 Nov 2025 19:18:57 -0300 Subject: [PATCH 03/13] chore(`ffi`): split into multiple errors & add panic queue --- rust/.config/nextest.toml | 3 +- rust/lib/src/lib.rs | 340 ++++++++++++++++++++++++++++++++++---- 2 files changed, 312 insertions(+), 31 deletions(-) diff --git a/rust/.config/nextest.toml b/rust/.config/nextest.toml index 41b84a9cf..cf0b1f053 100644 --- a/rust/.config/nextest.toml +++ b/rust/.config/nextest.toml @@ -1,9 +1,8 @@ [profile.default] -test-threads = 1 +fail-fast = false [profile.ci] retries = 3 test-threads = 1 failure-output = "immediate-final" fail-fast = false - diff --git a/rust/lib/src/lib.rs b/rust/lib/src/lib.rs index d19526fd5..d7adb8eb0 100644 --- a/rust/lib/src/lib.rs +++ b/rust/lib/src/lib.rs @@ -11,6 +11,7 @@ use log::Level; use std::any::Any; use std::backtrace::Backtrace; +use std::collections::VecDeque; use std::num::NonZeroU32; use std::panic::{self, PanicHookInfo}; use std::str::FromStr; @@ -53,19 +54,27 @@ use zingolib::wallet::keys::{ }; use zingolib::wallet::{LightWallet, WalletBase, WalletSettings}; +const MAX_PANIC_HISTORY: usize = 16; + +pub trait FromPanic { + fn from_panic(msg: String) -> Self; +} + #[derive(uniffi::Error, Debug, thiserror::Error)] pub enum ZingolibError { #[error("Error: Lightclient is not initialized")] LightclientNotInitialized, + #[error("Error: Lightclient lock poisoned")] LightclientLockPoisoned, - #[error("Panic")] - Panic, + + #[error("panic: {0}")] + Panic(String), } impl FromPanic for ZingolibError { - fn from_panic(_msg: String) -> Self { - ZingolibError::Panic + fn from_panic(msg: String) -> Self { + ZingolibError::Panic(msg) } } @@ -104,13 +113,13 @@ pub enum InitError { #[error("mnemonic parsing failed")] Mnemonic, - #[error("panic")] - Panic, + #[error("{0}")] + Panic(String), } impl FromPanic for InitError { - fn from_panic(_msg: String) -> Self { - InitError::Panic + fn from_panic(msg: String) -> Self { + InitError::Panic(msg) } } @@ -150,10 +159,6 @@ pub enum InitResultKind { Ufvk, } -pub trait FromPanic { - fn from_panic(msg: String) -> Self; -} - pub fn with_panic_guard(f: F) -> Result where F: FnOnce() -> Result + std::panic::UnwindSafe, @@ -168,8 +173,7 @@ where #[uniffi::export] pub fn last_panic_message() -> String { - // summarize what you saved in `LAST_PANIC` - take_last_panic().msg + last_panic().map(|p| p.msg).unwrap_or_default() } #[derive(Clone, Default)] @@ -181,22 +185,35 @@ struct PanicReport { backtrace: Option, } -static LAST_PANIC: Lazy> = Lazy::new(|| Mutex::new(PanicReport::default())); +static LAST_PANICS: Lazy>> = + Lazy::new(|| Mutex::new(VecDeque::with_capacity(MAX_PANIC_HISTORY))); -fn set_last_panic(report: PanicReport) { - if let Ok(mut r) = LAST_PANIC.lock() { - *r = report; +fn push_panic(report: PanicReport) { + if let Ok(mut q) = LAST_PANICS.lock() { + if q.len() == MAX_PANIC_HISTORY { + q.pop_front(); + } + q.push_back(report); } } -fn take_last_panic() -> PanicReport { - if let Ok(mut r) = LAST_PANIC.lock() { - let out = r.clone(); - *r = PanicReport::default(); - out - } else { - PanicReport::default() - } +fn last_panic() -> Option { + LAST_PANICS.lock().ok().and_then(|q| q.back().cloned()) +} + +fn recent_panics(limit: usize) -> Vec { + LAST_PANICS + .lock() + .map(|q| q.iter().rev().take(limit).cloned().collect()) + .unwrap_or_default() +} + +#[uniffi::export] +pub fn recent_panic_messages(limit: u32) -> Vec { + recent_panics(limit as usize) + .into_iter() + .map(|r| r.msg) + .collect() } static PANIC_HOOK_ONCE: Once = Once::new(); @@ -219,7 +236,7 @@ fn install_panic_hook_once() { let bt = Backtrace::force_capture().to_string(); - set_last_panic(PanicReport { + push_panic(PanicReport { msg: payload, file, line, @@ -252,8 +269,6 @@ fn clean_backtrace(bt_raw: &str) -> String { } fn format_panic_text(payload: Box) -> String { - let rpt = take_last_panic(); - let fallback = if let Some(s) = payload.downcast_ref::<&str>() { (*s).to_string() } else if let Some(s) = payload.downcast_ref::() { @@ -262,11 +277,20 @@ fn format_panic_text(payload: Box) -> String { "unknown panic payload".to_string() }; + let rpt = last_panic().unwrap_or_else(|| PanicReport { + msg: fallback.clone(), + file: None, + line: None, + col: None, + backtrace: None, + }); + let mut out = String::new(); if let (Some(f), Some(l), Some(c)) = (rpt.file.as_ref(), rpt.line, rpt.col) { out.push_str(&format!("{f}:{l}:{c}: ")); } + if !rpt.msg.is_empty() { out.push_str(&rpt.msg); } else { @@ -1798,3 +1822,261 @@ pub fn confirm() -> Result { } }) } + +#[cfg(test)] +mod tests { + use super::*; + use std::panic; + + fn drain_last_panic() { + if let Ok(mut q) = LAST_PANICS.lock() { + q.clear(); + } + } + + #[test] + fn set_and_take_last_panic_roundtrip() { + drain_last_panic(); + + let report = PanicReport { + msg: "test message".to_string(), + file: Some("src/lib.rs".to_string()), + line: Some(42), + col: Some(7), + backtrace: Some("frame1\nframe2".to_string()), + }; + + push_panic(report.clone()); + + let panics = recent_panics(2); + + let first_panic = panics.get(0).unwrap(); + + // Second read returns empty + let second_panic = panics.get(1); + assert!(second_panic.is_none()); + + // First read returns what we stored + assert_eq!(first_panic.msg, report.msg); + assert_eq!(first_panic.file, report.file); + assert_eq!(first_panic.line, report.line); + assert_eq!(first_panic.col, report.col); + assert_eq!(first_panic.backtrace.is_some(), report.backtrace.is_some()); + } + + #[test] + fn clean_backtrace_filters_unknown_and_blank_lines() { + let input = "frame1\n something\n\n frame2\n"; + let cleaned = clean_backtrace(input); + + assert_eq!(cleaned, "frame1\n frame2\n"); + assert!(!cleaned.contains("")); + assert!(!cleaned.contains("something")); + } + + #[test] + fn format_panic_text_uses_fallback_when_no_report() { + drain_last_panic(); + + let payload: Box = Box::new(String::from("fallback payload")); + let text = format_panic_text(payload); + + // With no PanicReport stored, it should fall back to the payload string. + assert!( + text.contains("fallback payload"), + "panic text did not contain fallback payload: {text}" + ); + + // LAST_PANIC should be empty (it was already empty). + let msg = last_panic_message(); + assert_eq!(msg, ""); + } + + #[test] + fn format_panic_text_prefers_report_over_payload_and_keeps_it() { + drain_last_panic(); + + let bt = "frame1\n ignore me\nframe2\n"; + let report = PanicReport { + msg: "stored panic message".to_string(), + file: Some("src/lib.rs".to_string()), + line: Some(12), + col: Some(34), + backtrace: Some(bt.to_string()), + }; + push_panic(report); + + let payload: Box = Box::new(String::from("payload should be ignored")); + let text = format_panic_text(payload); + + assert!( + text.contains("stored panic message"), + "formatted text did not contain stored panic message: {text}" + ); + assert!( + text.contains("src/lib.rs:12:34:"), + "formatted text did not contain file/line/col: {text}" + ); + + assert!(text.contains("frame1")); + assert!(text.contains("frame2")); + assert!( + !text.contains(""), + "formatted text should have had cleaned backtrace: {text}" + ); + + assert!( + !text.contains("payload should be ignored"), + "format_panic_text unexpectedly used fallback payload: {text}" + ); + + // PanicReport should remain in the history + let after = last_panic_message(); + assert_eq!( + after, "stored panic message", + "last_panic_message should still return the stored panic, since we keep a history now" + ); + } + + #[test] + fn with_panic_guard_propagates_ok_and_does_not_touch_last_panic() { + drain_last_panic(); + + let result: Result = with_panic_guard(|| Ok(123)); + assert_eq!(result.unwrap(), 123); + + // No panic + let msg = last_panic_message(); + assert_eq!(msg, ""); + } + + #[test] + fn with_panic_guard_propagates_err_without_using_from_panic() { + drain_last_panic(); + + let result: Result<(), ZingolibError> = + with_panic_guard(|| Err(ZingolibError::LightclientNotInitialized)); + + match result { + Err(ZingolibError::LightclientNotInitialized) => {} + other => panic!("Expected LightclientNotInitialized, got {other:?}"), + } + + let msg = last_panic_message(); + assert_eq!(msg, ""); + } + + #[test] + fn with_panic_guard_converts_panic_to_zingoliberror_panic_with_message() { + drain_last_panic(); + + let result: Result<(), ZingolibError> = with_panic_guard(|| { + panic!("zingolib_error test panic"); + }); + + match result { + Err(ZingolibError::Panic(msg)) => { + assert!( + msg.contains("zingolib_error test panic"), + "panic message did not contain original payload: {msg}" + ); + } + other => panic!("Expected ZingolibError::Panic, got {other:?}"), + } + } + + #[test] + fn with_panic_guard_converts_panic_to_initerror_panic() { + // Make sure we start from a clean slate + drain_last_panic(); + + let result: Result<(), InitError> = with_panic_guard(|| { + panic!("init panic payload"); + }); + + // Must be the [`InitError::Panic`] variant + let err = match result { + Err(e @ InitError::Panic(_)) => e, + other => panic!("expected InitError::Panic, got {other:?}"), + }; + + // This is the raw payload captured by the panic hook + let lp = last_panic_message(); + assert_eq!(lp, "init panic payload"); + + // This is the fully formatted panic text file:line:col + payload + backtrace + let formatted = err.to_string(); + + // Should contain the raw payload + assert!( + formatted.contains(&lp), + "formatted error does not contain payload: {formatted:?}", + ); + + // Should contain a backtrace header, proving format_panic_text was used + assert!( + formatted.contains("Backtrace:"), + "formatted error does not contain a backtrace: {formatted:?}", + ); + } + + #[test] + fn with_panic_guard_converts_panic_to_configerror_panic() { + drain_last_panic(); + + let result: Result<(), ConfigError> = with_panic_guard(|| { + panic!("config panic payload"); + }); + + assert!(matches!(result, Err(ConfigError::Panic))); + } + + #[test] + fn with_panic_guard_converts_panic_to_seederror_panic() { + drain_last_panic(); + + let result: Result<(), SeedError> = with_panic_guard(|| { + panic!("seed panic payload"); + }); + + assert!(matches!(result, Err(SeedError::Panic))); + } + + #[test] + fn with_panic_guard_converts_panic_to_ufvkerror_panic() { + drain_last_panic(); + + let result: Result<(), UfvkError> = with_panic_guard(|| { + panic!("ufvk panic payload"); + }); + + assert!(matches!(result, Err(UfvkError::Panic))); + } + + #[test] + fn last_panic_message_returns_message_from_raw_panic_when_guard_is_not_used() { + drain_last_panic(); + + install_panic_hook_once(); + + let res = panic::catch_unwind(|| { + panic!("raw panic for last_panic_message"); + }); + assert!(res.is_err()); + + let msg = last_panic_message(); + assert!( + msg.contains("raw panic for last_panic_message"), + "last_panic_message did not contain original panic payload: {msg}" + ); + } + + #[test] + fn check_b64_reports_true_for_valid_and_false_for_invalid_data() { + let encoded = base64::engine::general_purpose::STANDARD.encode(b"hello world"); + assert_eq!(check_b64(encoded), "true"); + + let invalid = "not base64!!"; + assert_eq!(check_b64(invalid.to_string()), "false"); + } +} From c86837d33bde490916fe50ef3c1d95e1e310eb3f Mon Sep 17 00:00:00 2001 From: dorianvp Date: Sun, 16 Nov 2025 00:39:27 -0300 Subject: [PATCH 04/13] chore(`ffi`): split into modules --- rust/lib/src/error.rs | 136 +++++++++++++++ rust/lib/src/lib.rs | 311 ++-------------------------------- rust/lib/src/panic_handler.rs | 164 ++++++++++++++++++ 3 files changed, 313 insertions(+), 298 deletions(-) create mode 100644 rust/lib/src/error.rs create mode 100644 rust/lib/src/panic_handler.rs diff --git a/rust/lib/src/error.rs b/rust/lib/src/error.rs new file mode 100644 index 000000000..e58c0d185 --- /dev/null +++ b/rust/lib/src/error.rs @@ -0,0 +1,136 @@ +use crate::panic_handler::FromPanic; + +#[derive(uniffi::Error, Debug, thiserror::Error)] +pub enum ZingolibError { + #[error("Error: Lightclient is not initialized")] + LightclientNotInitialized, + + #[error("Error: Lightclient lock poisoned")] + LightclientLockPoisoned, + + #[error("panic: {0}")] + Panic(String), +} + +impl FromPanic for ZingolibError { + fn from_panic(msg: String) -> Self { + ZingolibError::Panic(msg) + } +} + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum InitError { + #[error("invalid input: {0}")] + InvalidInput(String), + + #[error("base64 decode failed")] + Base64Decode, + + #[error("config error")] + Config(#[from] ConfigError), + + #[error("wallet read failed")] + WalletRead, + + #[error("lightwalletd query failed")] + Network, + + #[error("anchor height underflow (tip {tip}, offset {offset})")] + HeightUnderflow { tip: u32, offset: u32 }, + + #[error("lightclient creation failed")] + LightClient, + + #[error("wallet creation failed")] + WalletNew, + + #[error("seed error")] + Seed(#[from] SeedError), + + #[error("ufvk error")] + Ufvk(#[from] UfvkError), + + #[error("mnemonic parsing failed")] + Mnemonic, + + #[error("{0}")] + Panic(String), +} + +impl FromPanic for InitError { + fn from_panic(msg: String) -> Self { + InitError::Panic(msg) + } +} + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum ConfigError { + #[error("invalid chain hint: {0}")] + InvalidChainHint(String), + + #[error("invalid performance level: {0}")] + InvalidPerformanceLevel(String), + + #[error("invalid min_confirmations: {0}")] + InvalidMinConfirmations(String), + + #[error("loading client config failed")] + Load, + + #[error("panic")] + Panic, +} + +impl FromPanic for ConfigError { + fn from_panic(_msg: String) -> Self { + ConfigError::Panic + } +} + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum SeedError { + #[error("lightclient not initialized")] + NotInitialized, + + #[error("failed to lock lightclient")] + LockPoisoned, + + #[error("no mnemonic found (wallet loaded from key)")] + NoMnemonic, + + #[error("failed to serialize recovery info")] + Serialize, + + #[error("panic")] + Panic, +} + +impl FromPanic for SeedError { + fn from_panic(_msg: String) -> Self { + SeedError::Panic + } +} + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum UfvkError { + #[error("lightclient not initialized")] + NotInitialized, + + #[error("failed to lock lightclient")] + LockPoisoned, + + #[error("account 0 not found")] + NoAccount0, + + #[error("account 0 could not be converted to UnifiedFullViewingKey")] + NotUfvk, + + #[error("panic")] + Panic, +} + +impl FromPanic for UfvkError { + fn from_panic(_msg: String) -> Self { + UfvkError::Panic + } +} diff --git a/rust/lib/src/lib.rs b/rust/lib/src/lib.rs index d7adb8eb0..72e501f9a 100644 --- a/rust/lib/src/lib.rs +++ b/rust/lib/src/lib.rs @@ -1,5 +1,8 @@ uniffi::setup_scaffolding!(); +pub mod error; +pub mod panic_handler; + #[macro_use] extern crate lazy_static; extern crate android_logger; @@ -9,21 +12,14 @@ use android_logger::{Config, FilterBuilder}; #[cfg(target_os = "android")] use log::Level; -use std::any::Any; -use std::backtrace::Backtrace; -use std::collections::VecDeque; use std::num::NonZeroU32; -use std::panic::{self, PanicHookInfo}; use std::str::FromStr; -use std::sync::Mutex; -use std::sync::Once; use std::sync::RwLock; use base64::Engine; use base64::engine::general_purpose::STANDARD; use bip0039::Mnemonic; use json::object; -use once_cell::sync::Lazy; use rustls::crypto::{CryptoProvider, ring::default_provider}; use zcash_address::unified::{Container, Encoding, Ufvk}; @@ -54,98 +50,8 @@ use zingolib::wallet::keys::{ }; use zingolib::wallet::{LightWallet, WalletBase, WalletSettings}; -const MAX_PANIC_HISTORY: usize = 16; - -pub trait FromPanic { - fn from_panic(msg: String) -> Self; -} - -#[derive(uniffi::Error, Debug, thiserror::Error)] -pub enum ZingolibError { - #[error("Error: Lightclient is not initialized")] - LightclientNotInitialized, - - #[error("Error: Lightclient lock poisoned")] - LightclientLockPoisoned, - - #[error("panic: {0}")] - Panic(String), -} - -impl FromPanic for ZingolibError { - fn from_panic(msg: String) -> Self { - ZingolibError::Panic(msg) - } -} - -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum InitError { - #[error("invalid input: {0}")] - InvalidInput(String), - - #[error("base64 decode failed")] - Base64Decode, - - #[error("config error")] - Config(#[from] ConfigError), - - #[error("wallet read failed")] - WalletRead, - - #[error("lightwalletd query failed")] - Network, - - #[error("anchor height underflow (tip {tip}, offset {offset})")] - HeightUnderflow { tip: u32, offset: u32 }, - - #[error("lightclient creation failed")] - LightClient, - - #[error("wallet creation failed")] - WalletNew, - - #[error("seed error")] - Seed(#[from] SeedError), - - #[error("ufvk error")] - Ufvk(#[from] UfvkError), - - #[error("mnemonic parsing failed")] - Mnemonic, - - #[error("{0}")] - Panic(String), -} - -impl FromPanic for InitError { - fn from_panic(msg: String) -> Self { - InitError::Panic(msg) - } -} - -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum ConfigError { - #[error("invalid chain hint: {0}")] - InvalidChainHint(String), - - #[error("invalid performance level: {0}")] - InvalidPerformanceLevel(String), - - #[error("invalid min_confirmations: {0}")] - InvalidMinConfirmations(String), - - #[error("loading client config failed")] - Load, - - #[error("panic")] - Panic, -} - -impl FromPanic for ConfigError { - fn from_panic(_msg: String) -> Self { - ConfigError::Panic - } -} +use crate::error::{ConfigError, InitError, SeedError, UfvkError, ZingolibError}; +use crate::panic_handler::with_panic_guard; #[derive(Debug, Clone, uniffi::Record)] pub struct InitResult { @@ -159,155 +65,6 @@ pub enum InitResultKind { Ufvk, } -pub fn with_panic_guard(f: F) -> Result -where - F: FnOnce() -> Result + std::panic::UnwindSafe, - E: FromPanic, -{ - install_panic_hook_once(); - match panic::catch_unwind(f) { - Ok(res) => res, - Err(payload) => Err(E::from_panic(format_panic_text(payload))), - } -} - -#[uniffi::export] -pub fn last_panic_message() -> String { - last_panic().map(|p| p.msg).unwrap_or_default() -} - -#[derive(Clone, Default)] -struct PanicReport { - msg: String, - file: Option, - line: Option, - col: Option, - backtrace: Option, -} - -static LAST_PANICS: Lazy>> = - Lazy::new(|| Mutex::new(VecDeque::with_capacity(MAX_PANIC_HISTORY))); - -fn push_panic(report: PanicReport) { - if let Ok(mut q) = LAST_PANICS.lock() { - if q.len() == MAX_PANIC_HISTORY { - q.pop_front(); - } - q.push_back(report); - } -} - -fn last_panic() -> Option { - LAST_PANICS.lock().ok().and_then(|q| q.back().cloned()) -} - -fn recent_panics(limit: usize) -> Vec { - LAST_PANICS - .lock() - .map(|q| q.iter().rev().take(limit).cloned().collect()) - .unwrap_or_default() -} - -#[uniffi::export] -pub fn recent_panic_messages(limit: u32) -> Vec { - recent_panics(limit as usize) - .into_iter() - .map(|r| r.msg) - .collect() -} - -static PANIC_HOOK_ONCE: Once = Once::new(); - -fn install_panic_hook_once() { - PANIC_HOOK_ONCE.call_once(|| { - panic::set_hook(Box::new(|info: &PanicHookInfo<'_>| { - let payload = if let Some(s) = info.payload().downcast_ref::<&str>() { - (*s).to_string() - } else if let Some(s) = info.payload().downcast_ref::() { - s.clone() - } else { - info.to_string() - }; - - let (file, line, col) = info - .location() - .map(|l| (Some(l.file().to_string()), Some(l.line()), Some(l.column()))) - .unwrap_or((None, None, None)); - - let bt = Backtrace::force_capture().to_string(); - - push_panic(PanicReport { - msg: payload, - file, - line, - col, - backtrace: Some(bt), - }); - })); - }); -} - -fn clean_backtrace(bt_raw: &str) -> String { - const DROP: &[&str] = &[""]; - - let mut out = String::new(); - - for line in bt_raw.lines() { - let l = line.trim(); - if l.is_empty() { - continue; - } - if DROP.iter().any(|d| l.contains(d)) { - continue; - } - - out.push_str(line); - out.push('\n'); - } - - out -} - -fn format_panic_text(payload: Box) -> String { - let fallback = if let Some(s) = payload.downcast_ref::<&str>() { - (*s).to_string() - } else if let Some(s) = payload.downcast_ref::() { - s.clone() - } else { - "unknown panic payload".to_string() - }; - - let rpt = last_panic().unwrap_or_else(|| PanicReport { - msg: fallback.clone(), - file: None, - line: None, - col: None, - backtrace: None, - }); - - let mut out = String::new(); - - if let (Some(f), Some(l), Some(c)) = (rpt.file.as_ref(), rpt.line, rpt.col) { - out.push_str(&format!("{f}:{l}:{c}: ")); - } - - if !rpt.msg.is_empty() { - out.push_str(&rpt.msg); - } else { - out.push_str(&fallback); - } - - if let Some(bt) = rpt.backtrace { - let cleaned = clean_backtrace(&bt); - if !cleaned.is_empty() { - out.push_str("\nBacktrace:\n"); - out.push_str(&cleaned); - } - } - - out -} - // We'll use a RwLock to store a global lightclient instance, // so we don't have to keep creating it. We need to store it here, in rust // because we can't return such a complex structure back to JS @@ -706,7 +463,7 @@ pub fn poll_sync() -> Result { } #[uniffi::export] -fn run_sync() -> Result { +pub fn run_sync() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() @@ -746,7 +503,7 @@ pub fn pause_sync() -> Result { } #[uniffi::export] -fn status_sync() -> Result { +pub fn status_sync() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() @@ -796,54 +553,6 @@ pub fn info_server() -> Result { }) } -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum SeedError { - #[error("lightclient not initialized")] - NotInitialized, - - #[error("failed to lock lightclient")] - LockPoisoned, - - #[error("no mnemonic found (wallet loaded from key)")] - NoMnemonic, - - #[error("failed to serialize recovery info")] - Serialize, - - #[error("panic")] - Panic, -} - -impl FromPanic for SeedError { - fn from_panic(_msg: String) -> Self { - SeedError::Panic - } -} - -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum UfvkError { - #[error("lightclient not initialized")] - NotInitialized, - - #[error("failed to lock lightclient")] - LockPoisoned, - - #[error("account 0 not found")] - NoAccount0, - - #[error("account 0 could not be converted to UnifiedFullViewingKey")] - NotUfvk, - - #[error("panic")] - Panic, -} - -impl FromPanic for UfvkError { - fn from_panic(_msg: String) -> Self { - UfvkError::Panic - } -} - #[derive(Debug, Clone, uniffi::Record)] pub struct UfvkInfo { pub ufvk: String, @@ -1825,6 +1534,12 @@ pub fn confirm() -> Result { #[cfg(test)] mod tests { + use crate::panic_handler::{ + LAST_PANICS, PanicReport, clean_backtrace, format_panic_text, install_panic_hook_once, + last_panic_message, push_panic, recent_panics, + }; + use std::any::Any; + use super::*; use std::panic; diff --git a/rust/lib/src/panic_handler.rs b/rust/lib/src/panic_handler.rs new file mode 100644 index 000000000..b8be7849b --- /dev/null +++ b/rust/lib/src/panic_handler.rs @@ -0,0 +1,164 @@ +use std::{ + any::Any, + backtrace::Backtrace, + collections::VecDeque, + panic::{self, PanicHookInfo}, + sync::{Mutex, Once}, +}; + +use once_cell::sync::Lazy; + +const MAX_PANIC_HISTORY: usize = 16; + +pub trait FromPanic { + fn from_panic(msg: String) -> Self; +} + +pub fn with_panic_guard(f: F) -> Result +where + F: FnOnce() -> Result + std::panic::UnwindSafe, + E: FromPanic, +{ + install_panic_hook_once(); + match panic::catch_unwind(f) { + Ok(res) => res, + Err(payload) => Err(E::from_panic(format_panic_text(payload))), + } +} + +#[uniffi::export] +pub fn last_panic_message() -> String { + last_panic().map(|p| p.msg).unwrap_or_default() +} + +#[derive(Clone, Default)] +pub struct PanicReport { + pub msg: String, + pub file: Option, + pub line: Option, + pub col: Option, + pub backtrace: Option, +} + +pub(crate) static LAST_PANICS: Lazy>> = + Lazy::new(|| Mutex::new(VecDeque::with_capacity(MAX_PANIC_HISTORY))); + +pub(crate) fn push_panic(report: PanicReport) { + if let Ok(mut q) = LAST_PANICS.lock() { + if q.len() == MAX_PANIC_HISTORY { + q.pop_front(); + } + q.push_back(report); + } +} + +pub(crate) fn last_panic() -> Option { + LAST_PANICS.lock().ok().and_then(|q| q.back().cloned()) +} + +pub(crate) fn recent_panics(limit: usize) -> Vec { + LAST_PANICS + .lock() + .map(|q| q.iter().rev().take(limit).cloned().collect()) + .unwrap_or_default() +} + +#[uniffi::export] +pub fn recent_panic_messages(limit: u32) -> Vec { + recent_panics(limit as usize) + .into_iter() + .map(|r| r.msg) + .collect() +} + +static PANIC_HOOK_ONCE: Once = Once::new(); + +pub(crate) fn install_panic_hook_once() { + PANIC_HOOK_ONCE.call_once(|| { + panic::set_hook(Box::new(|info: &PanicHookInfo<'_>| { + let payload = if let Some(s) = info.payload().downcast_ref::<&str>() { + (*s).to_string() + } else if let Some(s) = info.payload().downcast_ref::() { + s.clone() + } else { + info.to_string() + }; + + let (file, line, col) = info + .location() + .map(|l| (Some(l.file().to_string()), Some(l.line()), Some(l.column()))) + .unwrap_or((None, None, None)); + + let bt = Backtrace::force_capture().to_string(); + + push_panic(PanicReport { + msg: payload, + file, + line, + col, + backtrace: Some(bt), + }); + })); + }); +} + +pub(crate) fn clean_backtrace(bt_raw: &str) -> String { + const DROP: &[&str] = &[""]; + + let mut out = String::new(); + + for line in bt_raw.lines() { + let l = line.trim(); + if l.is_empty() { + continue; + } + if DROP.iter().any(|d| l.contains(d)) { + continue; + } + + out.push_str(line); + out.push('\n'); + } + + out +} + +pub(crate) fn format_panic_text(payload: Box) -> String { + let fallback = if let Some(s) = payload.downcast_ref::<&str>() { + (*s).to_string() + } else if let Some(s) = payload.downcast_ref::() { + s.clone() + } else { + "unknown panic payload".to_string() + }; + + let rpt = last_panic().unwrap_or_else(|| PanicReport { + msg: fallback.clone(), + file: None, + line: None, + col: None, + backtrace: None, + }); + + let mut out = String::new(); + + if let (Some(f), Some(l), Some(c)) = (rpt.file.as_ref(), rpt.line, rpt.col) { + out.push_str(&format!("{f}:{l}:{c}: ")); + } + + if !rpt.msg.is_empty() { + out.push_str(&rpt.msg); + } else { + out.push_str(&fallback); + } + + if let Some(bt) = rpt.backtrace { + let cleaned = clean_backtrace(&bt); + if !cleaned.is_empty() { + out.push_str("\nBacktrace:\n"); + out.push_str(&cleaned); + } + } + + out +} From e805a5fa9e0fe5226d352c56eadacfeaaf36eed5 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Sun, 16 Nov 2025 20:22:13 -0300 Subject: [PATCH 05/13] chore(`ffi`): fix compilation errors --- rust/lib/src/lib.rs | 1534 +----------------------------- rust/lib/src/lightclient.rs | 1429 ++++++++++++++++++++++++++++ rust/lib/src/lightclient/util.rs | 106 +++ 3 files changed, 1546 insertions(+), 1523 deletions(-) create mode 100644 rust/lib/src/lightclient.rs create mode 100644 rust/lib/src/lightclient/util.rs diff --git a/rust/lib/src/lib.rs b/rust/lib/src/lib.rs index 72e501f9a..0ba854ca4 100644 --- a/rust/lib/src/lib.rs +++ b/rust/lib/src/lib.rs @@ -1,6 +1,7 @@ uniffi::setup_scaffolding!(); pub mod error; +pub mod lightclient; pub mod panic_handler; #[macro_use] @@ -12,1535 +13,22 @@ use android_logger::{Config, FilterBuilder}; #[cfg(target_os = "android")] use log::Level; -use std::num::NonZeroU32; -use std::str::FromStr; -use std::sync::RwLock; - -use base64::Engine; -use base64::engine::general_purpose::STANDARD; -use bip0039::Mnemonic; -use json::object; -use rustls::crypto::{CryptoProvider, ring::default_provider}; - -use zcash_address::unified::{Container, Encoding, Ufvk}; -use zcash_keys::address::Address; -use zcash_keys::keys::UnifiedFullViewingKey; -use zcash_primitives::consensus::BlockHeight; -use zcash_primitives::zip32::AccountId; -use zcash_protocol::consensus::NetworkType; - -use pepper_sync::config::{PerformanceLevel, SyncConfig, TransparentAddressDiscovery}; -use pepper_sync::keys::transparent; -use pepper_sync::wallet::{KeyIdInterface, SyncMode}; -use tokio::runtime::Runtime; -use zcash_address::ZcashAddress; -use zcash_primitives::memo::MemoBytes; -use zcash_protocol::value::Zatoshis; -use zingo_common_components::protocol::activation_heights::for_test::{self, all_height_one_nus}; -use zingolib::config::{ChainType, ZingoConfig, construct_lightwalletd_uri}; -use zingolib::data::PollReport; -use zingolib::data::proposal::total_fee; -use zingolib::data::receivers::Receivers; -use zingolib::data::receivers::transaction_request_from_receivers; -use zingolib::lightclient::LightClient; -use zingolib::utils::{conversion::address_from_str, conversion::txid_from_hex_encoded_str}; -use zingolib::wallet::keys::{ - WalletAddressRef, - unified::{ReceiverSelection, UnifiedKeyStore}, -}; -use zingolib::wallet::{LightWallet, WalletBase, WalletSettings}; - -use crate::error::{ConfigError, InitError, SeedError, UfvkError, ZingolibError}; -use crate::panic_handler::with_panic_guard; - -#[derive(Debug, Clone, uniffi::Record)] -pub struct InitResult { - pub kind: InitResultKind, - pub value: String, -} - -#[derive(Debug, Clone, uniffi::Enum)] -pub enum InitResultKind { - Seed, - Ufvk, -} - -// We'll use a RwLock to store a global lightclient instance, -// so we don't have to keep creating it. We need to store it here, in rust -// because we can't return such a complex structure back to JS -lazy_static! { - static ref LIGHTCLIENT: RwLock> = RwLock::new(None); -} - -lazy_static! { - pub static ref RT: Runtime = tokio::runtime::Runtime::new().unwrap(); -} - -fn with_lightclient_write(f: F) -> R -where - F: FnOnce(&mut Option) -> R, -{ - let mut guard = match LIGHTCLIENT.write() { - Ok(g) => g, - Err(poisoned) => { - log::warn!("LIGHTCLIENT RwLock poisoned; recovering and clearing poison"); - let g = poisoned.into_inner(); - LIGHTCLIENT.clear_poison(); - g - } - }; - f(&mut guard) -} - -fn reset_lightclient() { - with_lightclient_write(|slot| { - *slot = None; - }); -} - -fn store_client(lightclient: LightClient) -> Result<(), ZingolibError> { - with_lightclient_write(|slot| { - *slot = Some(lightclient); - }); - Ok(()) -} - -fn construct_uri_load_config( - uri: String, - chain_hint: String, - performance_level: String, - min_confirmations: u32, -) -> Result<(ZingoConfig, http::Uri), ConfigError> { - // if uri is empty -> Offline Mode. - let lightwalletd_uri = construct_lightwalletd_uri(Some(uri)); - - let chaintype = match chain_hint.as_str() { - "main" => ChainType::Mainnet, - "test" => ChainType::Testnet, - "regtest" => ChainType::Regtest(all_height_one_nus()), - _ => return Err(ConfigError::InvalidChainHint(chain_hint)), - }; - let performancetype = match performance_level.as_str() { - "Maximum" => PerformanceLevel::Maximum, - "High" => PerformanceLevel::High, - "Medium" => PerformanceLevel::Medium, - "Low" => PerformanceLevel::Low, - _ => return Err(ConfigError::InvalidPerformanceLevel(performance_level)), - }; +#[cfg(test)] +mod tests { + use base64::Engine; - let confirmations = NonZeroU32::try_from(min_confirmations) - .map_err(|_| ConfigError::InvalidMinConfirmations(min_confirmations.to_string()))?; + use crate::error::{ConfigError, InitError, SeedError, UfvkError, ZingolibError}; + use crate::panic_handler::with_panic_guard; - let config = zingolib::config::load_clientconfig( - lightwalletd_uri.clone(), - None, - chaintype, - WalletSettings { - sync_config: SyncConfig { - transparent_address_discovery: TransparentAddressDiscovery::minimal(), - performance_level: performancetype, - }, - min_confirmations: confirmations, + use crate::{ + lightclient::check_b64, + panic_handler::{ + LAST_PANICS, PanicReport, clean_backtrace, format_panic_text, install_panic_hook_once, + last_panic_message, push_panic, recent_panics, }, - NonZeroU32::try_from(1).expect("hard-coded integer"), - "".to_string(), - ) - .map_err(|_| ConfigError::Load)?; - - Ok((config, lightwalletd_uri)) -} - -pub fn init_logging() -> Result { - with_panic_guard(|| { - // this is only for Android - #[cfg(target_os = "android")] - android_logger::init_once( - Config::default().with_min_level(Level::Trace).with_filter( - FilterBuilder::new() - .parse("debug,hello::crate=zingolib") - .build(), - ), - ); - Ok("OK".to_string()) - }) -} - -pub fn init_new( - server_uri: String, - chain_hint: String, - performance_level: String, - min_confirmations: u32, -) -> Result { - with_panic_guard(|| { - reset_lightclient(); - let (config, lightwalletd_uri) = construct_uri_load_config( - server_uri, - chain_hint, - performance_level, - min_confirmations, - )?; - - // Query tip height - let tip: u32 = RT - .block_on(async move { - zingolib::grpc_connector::get_latest_block(lightwalletd_uri) - .await - .map(|b| b.height as u32) - }) - .map_err(|_| InitError::Network)?; - - // Derive anchor height safely - let offset = 100u32; - let anchor = tip - .checked_sub(offset) - .ok_or(InitError::HeightUnderflow { tip, offset })?; - let anchor = BlockHeight::from_u32(anchor); - - let lightclient = - LightClient::new(config, anchor, false).map_err(|_| InitError::LightClient)?; - let _ = store_client(lightclient); - - let seed = get_seed()?; - Ok(InitResult { - kind: InitResultKind::Seed, - value: seed, - }) - }) -} - -// TODO: change `seed` to `seed_phrase` or `mnemonic_phrase` -pub fn init_from_seed( - seed: String, - birthday: u32, - server_uri: String, - chain_hint: String, - performance_level: String, - min_confirmations: u32, -) -> Result { - with_panic_guard(|| { - reset_lightclient(); - - let (config, _lightwalletd_uri) = construct_uri_load_config( - server_uri, - chain_hint, - performance_level, - min_confirmations, - )?; - - let mnemonic = Mnemonic::from_phrase(seed).map_err(|_| InitError::Mnemonic)?; - - let wallet = LightWallet::new( - config.chain, - WalletBase::Mnemonic { - mnemonic, - no_of_accounts: config.no_of_accounts, - }, - BlockHeight::from_u32(birthday), - config.wallet_settings.clone(), - ) - .map_err(|_| InitError::WalletNew)?; - - let lightclient = LightClient::create_from_wallet(wallet, config, false) - .map_err(|_| InitError::LightClient)?; - let _ = store_client(lightclient); - - let seed = get_seed()?; - Ok(InitResult { - kind: InitResultKind::Seed, - value: seed, - }) - }) -} - -pub fn init_from_ufvk( - ufvk: String, - birthday: u32, - server_uri: String, - chain_hint: String, - performance_level: String, - min_confirmations: u32, -) -> Result { - with_panic_guard(|| { - reset_lightclient(); - let (config, _lightwalletd_uri) = construct_uri_load_config( - server_uri, - chain_hint, - performance_level, - min_confirmations, - )?; - - let wallet = LightWallet::new( - config.chain, - WalletBase::Ufvk(ufvk), - BlockHeight::from_u32(birthday), - config.wallet_settings.clone(), - ) - .map_err(|_| InitError::WalletNew)?; - - let lightclient = LightClient::create_from_wallet(wallet, config, false) - .map_err(|_| InitError::LightClient)?; - let _ = store_client(lightclient); - - let seed = get_ufvk()?; - Ok(InitResult { - kind: InitResultKind::Ufvk, - value: seed.ufvk, - }) - }) -} - -#[uniffi::export] -pub fn init_from_b64( - base64_data: String, - server_uri: String, - chain_hint: String, - performance_level: String, - min_confirmations: u32, -) -> Result { - with_panic_guard(|| { - reset_lightclient(); - let (config, _lightwalletd_uri) = construct_uri_load_config( - server_uri, - chain_hint, - performance_level, - min_confirmations, - )?; - - let decoded_bytes = base64::engine::general_purpose::STANDARD - .decode(&base64_data) - .map_err(|_| InitError::Base64Decode)?; - - let wallet = LightWallet::read(&decoded_bytes[..], config.chain) - .map_err(|_| InitError::WalletRead)?; - - let has_seed = wallet.mnemonic().is_some(); - - let lightclient = LightClient::create_from_wallet(wallet, config, false) - .map_err(|_| InitError::LightClient)?; - - let _ = store_client(lightclient); - - if has_seed { - Ok(InitResult { - kind: InitResultKind::Seed, - value: get_seed()?, - }) - } else { - Ok(InitResult { - kind: InitResultKind::Ufvk, - value: get_ufvk()?.to_string(), - }) - } - }) -} - -pub fn save_to_b64() -> Result { - with_panic_guard(|| { - // Return the wallet as a base64 encoded string - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - // we need to use STANDARD because swift is expecting the encoded String with padding - // I tried with STANDARD_NO_PAD and the decoding return `nil`. - Ok(RT.block_on(async move { - let mut wallet = lightclient.wallet.write().await; - match wallet.save() { - Ok(Some(wallet_bytes)) => STANDARD.encode(wallet_bytes), - // TODO: check this is better than a custom error when save is not required (empty buffer) - Ok(None) => "".to_string(), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn check_b64(base64_data: String) -> String { - match STANDARD.decode(&base64_data) { - Ok(_) => "true".to_string(), - Err(_) => "false".to_string(), - } -} - -pub fn get_developer_donation_address() -> Result { - with_panic_guard(|| Ok(zingolib::config::DEVELOPER_DONATION_ADDRESS.to_string())) -} - -pub fn get_zennies_for_zingo_donation_address() -> Result { - with_panic_guard(|| Ok(zingolib::config::ZENNIES_FOR_ZINGO_DONATION_ADDRESS.to_string())) -} - -pub fn set_crypto_default_provider_to_ring() -> Result { - with_panic_guard(|| { - Ok(CryptoProvider::get_default().map_or_else( - || match default_provider().install_default() { - Ok(_) => "true".to_string(), - Err(_) => "Error: Failed to install crypto provider".to_string(), - }, - |_| "true".to_string(), - )) - }) -} - -pub fn get_latest_block_server(server_uri: String) -> Result { - with_panic_guard(|| { - let lightwalletd_uri: http::Uri = match server_uri.parse() { - Ok(uri) => uri, - Err(e) => { - return Ok(format!("Error: failed to parse uri. {e}")); - } - }; - Ok( - match RT.block_on(async move { - zingolib::grpc_connector::get_latest_block(lightwalletd_uri).await - }) { - Ok(block_id) => block_id.height.to_string(), - Err(e) => format!("Error: {e}"), - }, - ) - }) -} - -pub fn get_latest_block_wallet() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.write().await; - object! { "height" => json::JsonValue::from(wallet.sync_state.wallet_height().map(u32::from).unwrap_or(0))}.pretty(2) - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn get_value_transfers() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.read().await; - match wallet.value_transfers(true).await { - Ok(value_transfers) => json::JsonValue::from(value_transfers).pretty(2), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn poll_sync() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(match lightclient.poll_sync() { - PollReport::NoHandle => "Sync task has not been launched.".to_string(), - PollReport::NotReady => "Sync task is not complete.".to_string(), - PollReport::Ready(result) => match result { - Ok(sync_result) => { - json::object! { "sync_complete" => json::JsonValue::from(sync_result) } - .pretty(2) - } - Err(e) => format!("Error: {e}"), - }, - }) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -#[uniffi::export] -pub fn run_sync() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - if lightclient.sync_mode() == SyncMode::Paused { - lightclient.resume_sync().expect("sync should be paused"); - Ok("Resuming sync task...".to_string()) - } else { - Ok(RT.block_on(async { - match lightclient.sync().await { - Ok(_) => "Launching sync task...".to_string(), - Err(e) => format!("Error: {e}"), - } - })) - } - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn pause_sync() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(match lightclient.pause_sync() { - Ok(_) => "Pausing sync task...".to_string(), - Err(e) => format!("Error: {e}"), - }) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -#[uniffi::export] -pub fn status_sync() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async { - let wallet = lightclient.wallet.read().await; - match pepper_sync::sync_status(&*wallet).await { - Ok(status) => json::JsonValue::from(status).pretty(2), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn run_rescan() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - match lightclient.rescan().await { - Ok(_) => "Launching rescan...".to_string(), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn info_server() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { lightclient.do_info().await })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -#[derive(Debug, Clone, uniffi::Record)] -pub struct UfvkInfo { - pub ufvk: String, - pub birthday: u32, -} - -impl ToString for UfvkInfo { - fn to_string(&self) -> String { - self.ufvk.clone() - } -} - -// TODO: rename "get_seed_phrase" or "get_mnemonic_phrase" -// or if other recovery info is being used could rename "get_recovery_info" ? -pub fn get_seed() -> Result { - with_panic_guard(|| { - let wallet_handle = { - let mut guard = LIGHTCLIENT.write().map_err(|_| SeedError::LockPoisoned)?; - let Some(lightclient) = &mut *guard else { - return Err(SeedError::NotInitialized); - }; - // Get a handle we can await on without the LIGHTCLIENT lock - lightclient.wallet.clone() - }; - - let recovery_json = RT.block_on(async move { - let wallet = wallet_handle.read().await; - let Some(recovery_info) = wallet.recovery_info() else { - return Err(SeedError::NoMnemonic); - }; - serde_json::to_string_pretty(&recovery_info).map_err(|_| SeedError::Serialize) - })?; - - Ok(recovery_json) - }) -} - -pub fn get_ufvk() -> Result { - with_panic_guard(|| { - let wallet_handle = { - let mut guard = LIGHTCLIENT.write().map_err(|_| UfvkError::LockPoisoned)?; - let Some(lightclient) = &mut *guard else { - return Err(UfvkError::NotInitialized); - }; - lightclient.wallet.clone() - }; - - RT.block_on(async move { - let wallet = wallet_handle.read().await; - - // Avoid `expect("account 0 must always exist")` - let Some(k) = wallet.unified_key_store.get(&AccountId::ZERO) else { - return Err(UfvkError::NoAccount0); - }; - - let ufvk: UnifiedFullViewingKey = k.try_into().map_err(|_| UfvkError::NotUfvk)?; - - Ok(UfvkInfo { - ufvk: ufvk.encode(&wallet.network), - birthday: u32::from(wallet.birthday), - }) - }) - }) -} - -pub fn change_server(server_uri: String) -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - if server_uri.is_empty() { - lightclient.set_server(http::Uri::default()); - Ok("server set (default)".to_string()) - } else { - match http::Uri::from_str(&server_uri) { - Ok(uri) => { - lightclient.set_server(uri); - Ok("server set".to_string()) - } - Err(_) => Ok("Error: invalid server uri".to_string()), - } - } - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn wallet_kind() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.read().await; - if wallet.mnemonic().is_some() { - object! {"kind" => "Loaded from seed or mnemonic phrase)", - "transparent" => true, - "sapling" => true, - "orchard" => true, - } - .pretty(2) - } else { - match wallet - .unified_key_store - .get(&AccountId::ZERO) - .expect("account 0 must always exist") - { - UnifiedKeyStore::Spend(_) => object! { - "kind" => "Loaded from unified spending key", - "transparent" => true, - "sapling" => true, - "orchard" => true, - } - .pretty(2), - UnifiedKeyStore::View(ufvk) => object! { - "kind" => "Loaded from unified full viewing key", - "transparent" => ufvk.transparent().is_some(), - "sapling" => ufvk.sapling().is_some(), - "orchard" => ufvk.orchard().is_some(), - } - .pretty(2), - UnifiedKeyStore::Empty => object! { - "kind" => "No keys found", - "transparent" => false, - "sapling" => false, - "orchard" => false, - } - .pretty(2), - } - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn parse_address(address: String) -> Result { - with_panic_guard(|| { - if address.is_empty() { - Ok("Error: The address is empty".to_string()) - } else { - fn make_decoded_chain_pair( - address: &str, - ) -> Option<(zcash_client_backend::address::Address, ChainType)> { - [ - ChainType::Mainnet, - ChainType::Testnet, - ChainType::Regtest(for_test::all_height_one_nus()), - ] - .iter() - .find_map(|chain| Address::decode(chain, address).zip(Some(*chain))) - } - if let Some((recipient_address, chain_name)) = make_decoded_chain_pair(&address) { - let chain_name_string = match chain_name { - ChainType::Mainnet => "main", - ChainType::Testnet => "test", - ChainType::Regtest(_) => "regtest", - }; - Ok(match recipient_address { - Address::Sapling(_) => object! { - "status" => "success", - "chain_name" => chain_name_string, - "address_kind" => "sapling", - } - .pretty(2), - Address::Transparent(_) => object! { - "status" => "success", - "chain_name" => chain_name_string, - "address_kind" => "transparent", - } - .pretty(2), - Address::Tex(_) => object! { - "status" => "success", - "chain_name" => chain_name_string, - "address_kind" => "tex", - } - .pretty(2), - Address::Unified(ua) => { - let mut receivers_available = vec![]; - if ua.sapling().is_some() { - receivers_available.push("sapling") - } - if ua.transparent().is_some() { - receivers_available.push("transparent") - } - if ua.orchard().is_some() { - receivers_available.push("orchard"); - object! { - "status" => "success", - "chain_name" => chain_name_string, - "address_kind" => "unified", - "receivers_available" => receivers_available, - "only_orchard_ua" => zcash_keys::address::UnifiedAddress::from_receivers(ua.orchard().cloned(), None, None).expect("To construct UA").encode(&chain_name), - } - .pretty(2) - } else { - object! { - "status" => "success", - "chain_name" => chain_name_string, - "address_kind" => "unified", - "receivers_available" => receivers_available, - } - .pretty(2) - } - } - }) - } else { - Ok(object! { - "status" => "Invalid address", - "chain_name" => json::JsonValue::Null, - "address_kind" => json::JsonValue::Null, - } - .pretty(2)) - } - } - }) -} - -pub fn parse_ufvk(ufvk: String) -> Result { - with_panic_guard(|| { - if ufvk.is_empty() { - Ok("Error: The ufvk is empty".to_string()) - } else { - Ok(json::stringify_pretty( - match Ufvk::decode(&ufvk) { - Ok((network, ufvk)) => { - let mut pools_available = vec![]; - for fvk in ufvk.items_as_parsed() { - match fvk { - zcash_address::unified::Fvk::Orchard(_) => { - pools_available.push("orchard") - } - zcash_address::unified::Fvk::Sapling(_) => { - pools_available.push("sapling") - } - zcash_address::unified::Fvk::P2pkh(_) => { - pools_available.push("transparent") - } - zcash_address::unified::Fvk::Unknown { .. } => pools_available.push( - "Error: Unknown future protocol. Perhaps you're using old software", - ), - } - } - object! { - "status" => "success", - "chain_name" => match network { - NetworkType::Main => "main", - NetworkType::Test => "test", - NetworkType::Regtest => "regtest", - }, - "address_kind" => "ufvk", - "pools_available" => pools_available, - } - } - Err(_) => { - object! { - "status" => "Invalid viewkey", - "chain_name" => json::JsonValue::Null, - "address_kind" => json::JsonValue::Null - } - } - }, - 2, - )) - } - }) -} - -pub fn get_version() -> Result { - with_panic_guard(|| Ok(zingolib::git_description().to_string())) -} - -pub fn get_messages(address: String) -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - match lightclient - .messages_containing(Some(address.as_str())) - .await - { - Ok(value_transfers) => json::JsonValue::from(value_transfers).pretty(2), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn get_balance() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - match lightclient.account_balance(AccountId::ZERO).await { - Ok(bal) => json::JsonValue::from(bal).pretty(2), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn get_total_memobytes_to_address() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - match lightclient.do_total_memobytes_to_address().await { - Ok(total_memo_bytes) => json::JsonValue::from(total_memo_bytes).pretty(2), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn get_total_value_to_address() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - match lightclient.do_total_value_to_address().await { - Ok(total_values) => json::JsonValue::from(total_values).pretty(2), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn get_total_spends_to_address() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - match lightclient.do_total_spends_to_address().await { - Ok(total_spends) => json::JsonValue::from(total_spends).pretty(2), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn zec_price(tor: String) -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let Ok(tor_bool) = tor.parse() else { - return "Error: failed to parse tor setting.".to_string(); - }; - let client_check = match (tor_bool, lightclient.tor_client()) { - (true, Some(tc)) => Ok(Some(tc)), - (true, None) => Err(()), - (false, _) => Ok(None), - }; - let tor_client = match client_check { - Ok(tc) => tc, - Err(_) => { - return "Error: no tor client found. please create a tor client." - .to_string(); - } - }; - - let mut wallet = lightclient.wallet.write().await; - match wallet.update_current_price(tor_client).await { - Ok(price) => object! { "current_price" => price }.pretty(2), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn resend_transaction(txid: String) -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - let txid = match txid_from_hex_encoded_str(&txid) { - Ok(txid) => txid, - Err(e) => return Ok(format!("Error: {e}")), - }; - Ok(RT.block_on(async move { - match lightclient.resend(txid).await { - Ok(_) => "Successfully resent transaction.".to_string(), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn remove_transaction(txid: String) -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - let txid = match txid_from_hex_encoded_str(&txid) { - Ok(txid) => txid, - Err(e) => return Ok(format!("Error: {e}")), - }; - Ok(RT.block_on(async move { - let mut wallet = lightclient.wallet.write().await; - match wallet.remove_unconfirmed_transaction(txid) { - Ok(_) => "Successfully removed transaction.".to_string(), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -// we don't use this anymore... -pub fn get_spendable_balance_with_address( - address: String, - zennies: String, -) -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - let Ok(address) = address_from_str(&address) else { - return Ok("Error: unknown address format".to_string()); - }; - let Ok(zennies) = zennies.parse() else { - return Ok("Error: failed to parse zennies setting.".to_string()); - }; - Ok(RT.block_on(async move { - match lightclient - .max_send_value(address, zennies, AccountId::ZERO) - .await - { - Ok(bal) => object! { "spendable_balance" => bal.into_u64() }.pretty(2), - Err(e) => format!("error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn get_spendable_balance_total() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.write().await; - let spendable_balance = - match wallet.shielded_spendable_balance(AccountId::ZERO, false) { - Ok(bal) => bal, - Err(e) => return format!("Error: {e}"), - }; - object! { - "spendable_balance" => spendable_balance.into_u64(), - } - .pretty(2) - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn set_option_wallet() -> Result { - with_panic_guard(|| Ok("Error: unimplemented".to_string())) -} - -pub fn get_option_wallet() -> Result { - with_panic_guard(|| Ok("Error: unimplemented".to_string())) -} - -pub fn create_tor_client(data_dir: String) -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - if lightclient.tor_client().is_some() { - return Ok("Tor client already exists.".to_string()); - } - Ok( - match RT.block_on(async move { - lightclient.create_tor_client(Some(data_dir.into())).await - }) { - Ok(_) => "Successfully created tor client.".to_string(), - Err(e) => format!("Error: creating tor client: {e}"), - }, - ) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn remove_tor_client() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - if lightclient.tor_client().is_none() { - return Ok("Tor client is not active.".to_string()); - } - RT.block_on(async move { - lightclient.remove_tor_client().await; - }); - Ok("Successfully removed tor client.".to_string()) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn get_unified_addresses() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { lightclient.unified_addresses_json().await.pretty(2) })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn get_transparent_addresses() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok( - RT.block_on( - async move { lightclient.transparent_addresses_json().await.pretty(2) }, - ), - ) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn create_new_unified_address(receivers: String) -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let mut wallet = lightclient.wallet.write().await; - let network = wallet.network; - let receivers_available = ReceiverSelection { - orchard: receivers.contains('o'), - sapling: receivers.contains('z'), - }; - match wallet.generate_unified_address(receivers_available, AccountId::ZERO) { - Ok((id, unified_address)) => json::object! { - "account" => u32::from(AccountId::ZERO), - "address_index" => id.address_index, - "has_orchard" => unified_address.has_orchard(), - "has_sapling" => unified_address.has_sapling(), - "has_transparent" => unified_address.has_transparent(), - "encoded_address" => unified_address.encode(&network), - } - .pretty(2), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn create_new_transparent_address() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let mut wallet = lightclient.wallet.write().await; - let network = wallet.network; - match wallet.generate_transparent_address(AccountId::ZERO, true) { - Ok((id, transparent_address)) => { - json::object! { - "account" => u32::from(id.account_id()), - "address_index" => id.address_index().index(), - "scope" => id.scope().to_string(), - "encoded_address" => transparent::encode_address(&network, transparent_address), - }.pretty(2) - } - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn check_my_address(address: String) -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.read().await; - match wallet.is_address_derived_by_keys(&address) { - Ok(address_ref) => address_ref.map_or( - json::object! { "is_wallet_address" => false }, - |address_ref| match address_ref { - WalletAddressRef::Unified { - account_id, - address_index, - has_orchard, - has_sapling, - has_transparent, - encoded_address, - } => json::object! { - "is_wallet_address" => true, - "address_type" => "unified".to_string(), - "address_index" => address_index, - "account_id" => u32::from(account_id), - "has_orchard" => has_orchard, - "has_sapling" => has_sapling, - "has_transparent" => has_transparent, - "encoded_address" => encoded_address, - }, - WalletAddressRef::OrchardInternal { - account_id, - diversifier_index, - encoded_address, - } => json::object! { - "is_wallet_address" => true, - "address_type" => "orchard_internal".to_string(), - "account_id" => u32::from(account_id), - "diversifier_index" => u128::from(diversifier_index).to_string(), - "encoded_address" => encoded_address, - }, - WalletAddressRef::SaplingExternal { - account_id, - diversifier_index, - encoded_address, - } => json::object! { - "is_wallet_address" => true, - "address_type" => "sapling".to_string(), - "account_id" => u32::from(account_id), - "diversifier_index" => u128::from(diversifier_index).to_string(), - "encoded_address" => encoded_address, - }, - WalletAddressRef::Transparent { - account_id, - scope, - address_index, - encoded_address, - } => json::object! { - "is_wallet_address" => true, - "address_type" => "transparent".to_string(), - "account_id" => u32::from(account_id), - "scope" => scope.to_string(), - "address_index" => address_index.index(), - "encoded_address" => encoded_address, - }, - }, - ).pretty(2), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn get_wallet_save_required() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.read().await; - object! { "save_required" => wallet.save_required }.pretty(2) - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn set_config_wallet_to_test() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let mut wallet = lightclient.wallet.write().await; - wallet.wallet_settings.min_confirmations = NonZeroU32::try_from(1).unwrap(); - wallet.wallet_settings.sync_config.performance_level = PerformanceLevel::Medium; - wallet.save_required = true; - "Successfully set config wallet to test. (1 - Medium)".to_string() - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn set_config_wallet_to_prod( - performance_level: String, - min_confirmations: u32, -) -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let performancetype = match performance_level.as_str() { - "Maximum" => PerformanceLevel::Maximum, - "High" => PerformanceLevel::High, - "Medium" => PerformanceLevel::Medium, - "Low" => PerformanceLevel::Low, - _ => return "Error: Not a valid performance level!".to_string(), - }; - let mut wallet = lightclient.wallet.write().await; - wallet.wallet_settings.min_confirmations = - NonZeroU32::try_from(min_confirmations).unwrap(); - wallet.wallet_settings.sync_config.performance_level = performancetype; - wallet.save_required = true; - "Successfully set config wallet to prod.".to_string() - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn get_config_wallet_performance() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.read().await; - let performance_level = match wallet.wallet_settings.sync_config.performance_level { - PerformanceLevel::Low => "Low", - PerformanceLevel::Medium => "Medium", - PerformanceLevel::High => "High", - PerformanceLevel::Maximum => "Maximum", - }; - object! { "performance_level" => performance_level }.pretty(2) - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn get_wallet_version() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.read().await; - let current_version = wallet.current_version(); - let read_version = wallet.read_version(); - object! { - "current_version" => current_version, - "read_version" => read_version - } - .pretty(2) - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -// internal use -fn interpret_memo_string(memo_str: String) -> Result { - // If the string starts with an "0x", and contains only hex chars ([a-f0-9]+) then - // interpret it as a hex - let s_bytes = if memo_str.to_lowercase().starts_with("0x") { - match hex::decode(&memo_str[2..memo_str.len()]) { - Ok(data) => data, - Err(_) => Vec::from(memo_str.as_bytes()), - } - } else { - Vec::from(memo_str.as_bytes()) - }; - - MemoBytes::from_bytes(&s_bytes) - .map_err(|_| format!("Error: creating output. Memo '{:?}' is too long", memo_str)) -} - -pub fn send(send_json: String) -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let json_args = match json::parse(&send_json) { - Ok(parsed) => parsed, - Err(_) => return "Error: it is not a valid JSON".to_string(), - }; - - let mut receivers = Receivers::new(); - for j in json_args.members() { - let recipient_address = match j["address"].as_str() { - Some(addr) => match ZcashAddress::try_from_encoded(addr) { - Ok(a) => a, - Err(e) => return format!("Error: Invalid address: {e}"), - }, - None => return "Error: Missing address".to_string(), - }; - - let amount = match j["amount"].as_u64() { - Some(a) => match Zatoshis::from_u64(a) { - Ok(a) => a, - Err(e) => return format!("Error: Invalid amount: {e}"), - }, - None => return "Missing amount".to_string(), - }; - - let memo = if let Some(m) = j["memo"].as_str() { - match interpret_memo_string(m.to_string()) { - Ok(memo_bytes) => Some(memo_bytes), - Err(e) => return format!("Error: Invalid memo: {e}"), - } - } else { - None - }; - - receivers.push(zingolib::data::receivers::Receiver { - recipient_address, - amount, - memo, - }); - } - - let request = match transaction_request_from_receivers(receivers) { - Ok(request) => request, - Err(e) => return format!("Error: Request Error: {e}"), - }; - - match lightclient.propose_send(request, AccountId::ZERO).await { - Ok(proposal) => { - let fee = match total_fee(&proposal) { - Ok(fee) => fee, - Err(e) => return object! { "error" => e.to_string() }.pretty(2), - }; - object! { "fee" => fee.into_u64() } - } - Err(e) => { - object! { "error" => e.to_string() } - } - } - .pretty(2) - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn shield() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - match lightclient.propose_shield(AccountId::ZERO).await { - Ok(proposal) => { - if proposal.steps().len() != 1 { - return object! { "error" => "shielding transactions should not have multiple proposal steps" }.pretty(2); - } - let step = proposal.steps().first(); - let Some(value_to_shield) = step - .balance() - .proposed_change() - .iter() - .try_fold(Zatoshis::ZERO, |acc, c| acc + c.value()) else { - return object! { "error" => "shield amount outside valid range of zatoshis" } - .pretty(2); - }; - let fee = step.balance().fee_required(); - object! { - "value_to_shield" => value_to_shield.into_u64(), - "fee" => fee.into_u64(), - } - } - Err(e) => { - object! { "error" => e.to_string() } - } - } - .pretty(2) - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -pub fn confirm() -> Result { - with_panic_guard(|| { - let mut guard = LIGHTCLIENT - .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - match lightclient - .send_stored_proposal() - .await { - Ok(txids) => { - object! { "txids" => txids.iter().map(|txid| txid.to_string()).collect::>() } - } - Err(e) => { - object! { "error" => e.to_string() } - } - } - .pretty(2) - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } - }) -} - -#[cfg(test)] -mod tests { - use crate::panic_handler::{ - LAST_PANICS, PanicReport, clean_backtrace, format_panic_text, install_panic_hook_once, - last_panic_message, push_panic, recent_panics, }; use std::any::Any; - use super::*; use std::panic; fn drain_last_panic() { diff --git a/rust/lib/src/lightclient.rs b/rust/lib/src/lightclient.rs new file mode 100644 index 000000000..99cc81809 --- /dev/null +++ b/rust/lib/src/lightclient.rs @@ -0,0 +1,1429 @@ +pub mod util; + +use std::num::NonZeroU32; +use std::str::FromStr; +use std::sync::RwLock; + +use base64::Engine; +use bip0039::Mnemonic; +use json::object; +use rustls::crypto::CryptoProvider; +use rustls::crypto::ring::default_provider; +use tokio::runtime::Runtime; +use zcash_address::unified::{Container, Encoding, Ufvk}; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_primitives::zip32::AccountId; +use zcash_protocol::consensus::{BlockHeight, NetworkType}; +use zingo_common_components::protocol::activation_heights::for_test; +use zingolib::config::ChainType; +use zingolib::data::proposal::total_fee; +use zingolib::data::receivers::{Receivers, transaction_request_from_receivers}; +use zingolib::lightclient::LightClient; +use zingolib::utils::conversion::{address_from_str, txid_from_hex_encoded_str}; +use zingolib::wallet::keys::WalletAddressRef; +use zingolib::wallet::keys::unified::{ReceiverSelection, UnifiedKeyStore}; + +use crate::error::{SeedError, UfvkError, ZingolibError}; +use crate::lightclient::util::{construct_uri_load_config, reset_lightclient, store_client}; +use crate::{error::InitError, panic_handler::with_panic_guard}; +use base64::engine::general_purpose::STANDARD; +use pepper_sync::config::PerformanceLevel; +use pepper_sync::keys::transparent; +use pepper_sync::wallet::{KeyIdInterface, SyncMode}; +use zcash_address::ZcashAddress; +use zcash_primitives::memo::MemoBytes; +use zcash_protocol::value::Zatoshis; +use zingolib::data::PollReport; +use zingolib::wallet::{LightWallet, WalletBase}; + +#[derive(Debug, Clone, uniffi::Record)] +pub struct InitResult { + pub kind: InitResultKind, + pub value: String, +} + +#[derive(Debug, Clone, uniffi::Enum)] +pub enum InitResultKind { + Seed, + Ufvk, +} + +// We'll use a RwLock to store a global lightclient instance, +// so we don't have to keep creating it. We need to store it here, in rust +// because we can't return such a complex structure back to JS +lazy_static! { + static ref LIGHTCLIENT: RwLock> = RwLock::new(None); +} + +lazy_static! { + pub static ref RT: Runtime = tokio::runtime::Runtime::new().unwrap(); +} + +pub fn init_new( + server_uri: String, + chain_hint: String, + performance_level: String, + min_confirmations: u32, +) -> Result { + with_panic_guard(|| { + reset_lightclient(); + let (config, lightwalletd_uri) = construct_uri_load_config( + server_uri, + chain_hint, + performance_level, + min_confirmations, + )?; + + // Query tip height + let tip: u32 = RT + .block_on(async move { + zingolib::grpc_connector::get_latest_block(lightwalletd_uri) + .await + .map(|b| b.height as u32) + }) + .map_err(|_| InitError::Network)?; + + // Derive anchor height safely + let offset = 100u32; + let anchor = tip + .checked_sub(offset) + .ok_or(InitError::HeightUnderflow { tip, offset })?; + let anchor = BlockHeight::from_u32(anchor); + + let lightclient = + LightClient::new(config, anchor, false).map_err(|_| InitError::LightClient)?; + let _ = store_client(lightclient); + + let seed = get_seed()?; + Ok(InitResult { + kind: InitResultKind::Seed, + value: seed, + }) + }) +} + +// TODO: change `seed` to `seed_phrase` or `mnemonic_phrase` +pub fn init_from_seed( + seed: String, + birthday: u32, + server_uri: String, + chain_hint: String, + performance_level: String, + min_confirmations: u32, +) -> Result { + with_panic_guard(|| { + reset_lightclient(); + + let (config, _lightwalletd_uri) = construct_uri_load_config( + server_uri, + chain_hint, + performance_level, + min_confirmations, + )?; + + let mnemonic = Mnemonic::from_phrase(seed).map_err(|_| InitError::Mnemonic)?; + + let wallet = LightWallet::new( + config.chain, + WalletBase::Mnemonic { + mnemonic, + no_of_accounts: config.no_of_accounts, + }, + BlockHeight::from_u32(birthday), + config.wallet_settings.clone(), + ) + .map_err(|_| InitError::WalletNew)?; + + let lightclient = LightClient::create_from_wallet(wallet, config, false) + .map_err(|_| InitError::LightClient)?; + let _ = store_client(lightclient); + + let seed = get_seed()?; + Ok(InitResult { + kind: InitResultKind::Seed, + value: seed, + }) + }) +} + +pub fn init_from_ufvk( + ufvk: String, + birthday: u32, + server_uri: String, + chain_hint: String, + performance_level: String, + min_confirmations: u32, +) -> Result { + with_panic_guard(|| { + reset_lightclient(); + let (config, _lightwalletd_uri) = construct_uri_load_config( + server_uri, + chain_hint, + performance_level, + min_confirmations, + )?; + + let wallet = LightWallet::new( + config.chain, + WalletBase::Ufvk(ufvk), + BlockHeight::from_u32(birthday), + config.wallet_settings.clone(), + ) + .map_err(|_| InitError::WalletNew)?; + + let lightclient = LightClient::create_from_wallet(wallet, config, false) + .map_err(|_| InitError::LightClient)?; + let _ = store_client(lightclient); + + let seed = get_ufvk()?; + Ok(InitResult { + kind: InitResultKind::Ufvk, + value: seed.ufvk, + }) + }) +} + +#[uniffi::export] +pub fn init_from_b64( + base64_data: String, + server_uri: String, + chain_hint: String, + performance_level: String, + min_confirmations: u32, +) -> Result { + with_panic_guard(|| { + reset_lightclient(); + let (config, _lightwalletd_uri) = construct_uri_load_config( + server_uri, + chain_hint, + performance_level, + min_confirmations, + )?; + + let decoded_bytes = base64::engine::general_purpose::STANDARD + .decode(&base64_data) + .map_err(|_| InitError::Base64Decode)?; + + let wallet = LightWallet::read(&decoded_bytes[..], config.chain) + .map_err(|_| InitError::WalletRead)?; + + let has_seed = wallet.mnemonic().is_some(); + + let lightclient = LightClient::create_from_wallet(wallet, config, false) + .map_err(|_| InitError::LightClient)?; + + let _ = store_client(lightclient); + + if has_seed { + Ok(InitResult { + kind: InitResultKind::Seed, + value: get_seed()?, + }) + } else { + Ok(InitResult { + kind: InitResultKind::Ufvk, + value: get_ufvk()?.to_string(), + }) + } + }) +} + +pub fn save_to_b64() -> Result { + with_panic_guard(|| { + // Return the wallet as a base64 encoded string + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + // we need to use STANDARD because swift is expecting the encoded String with padding + // I tried with STANDARD_NO_PAD and the decoding return `nil`. + Ok(RT.block_on(async move { + let mut wallet = lightclient.wallet.write().await; + match wallet.save() { + Ok(Some(wallet_bytes)) => STANDARD.encode(wallet_bytes), + // TODO: check this is better than a custom error when save is not required (empty buffer) + Ok(None) => "".to_string(), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn check_b64(base64_data: String) -> String { + match STANDARD.decode(&base64_data) { + Ok(_) => "true".to_string(), + Err(_) => "false".to_string(), + } +} + +pub fn get_developer_donation_address() -> Result { + with_panic_guard(|| Ok(zingolib::config::DEVELOPER_DONATION_ADDRESS.to_string())) +} + +pub fn get_zennies_for_zingo_donation_address() -> Result { + with_panic_guard(|| Ok(zingolib::config::ZENNIES_FOR_ZINGO_DONATION_ADDRESS.to_string())) +} + +pub fn set_crypto_default_provider_to_ring() -> Result { + with_panic_guard(|| { + Ok(CryptoProvider::get_default().map_or_else( + || match default_provider().install_default() { + Ok(_) => "true".to_string(), + Err(_) => "Error: Failed to install crypto provider".to_string(), + }, + |_| "true".to_string(), + )) + }) +} + +pub fn get_latest_block_server(server_uri: String) -> Result { + with_panic_guard(|| { + let lightwalletd_uri: http::Uri = match server_uri.parse() { + Ok(uri) => uri, + Err(e) => { + return Ok(format!("Error: failed to parse uri. {e}")); + } + }; + Ok( + match RT.block_on(async move { + zingolib::grpc_connector::get_latest_block(lightwalletd_uri).await + }) { + Ok(block_id) => block_id.height.to_string(), + Err(e) => format!("Error: {e}"), + }, + ) + }) +} + +pub fn get_latest_block_wallet() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let wallet = lightclient.wallet.write().await; + object! { "height" => json::JsonValue::from(wallet.sync_state.wallet_height().map(u32::from).unwrap_or(0))}.pretty(2) + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn get_value_transfers() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let wallet = lightclient.wallet.read().await; + match wallet.value_transfers(true).await { + Ok(value_transfers) => json::JsonValue::from(value_transfers).pretty(2), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn poll_sync() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(match lightclient.poll_sync() { + PollReport::NoHandle => "Sync task has not been launched.".to_string(), + PollReport::NotReady => "Sync task is not complete.".to_string(), + PollReport::Ready(result) => match result { + Ok(sync_result) => { + json::object! { "sync_complete" => json::JsonValue::from(sync_result) } + .pretty(2) + } + Err(e) => format!("Error: {e}"), + }, + }) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +#[uniffi::export] +pub fn run_sync() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + if lightclient.sync_mode() == SyncMode::Paused { + lightclient.resume_sync().expect("sync should be paused"); + Ok("Resuming sync task...".to_string()) + } else { + Ok(RT.block_on(async { + match lightclient.sync().await { + Ok(_) => "Launching sync task...".to_string(), + Err(e) => format!("Error: {e}"), + } + })) + } + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn pause_sync() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(match lightclient.pause_sync() { + Ok(_) => "Pausing sync task...".to_string(), + Err(e) => format!("Error: {e}"), + }) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +#[uniffi::export] +pub fn status_sync() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async { + let wallet = lightclient.wallet.read().await; + match pepper_sync::sync_status(&*wallet).await { + Ok(status) => json::JsonValue::from(status).pretty(2), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn run_rescan() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + match lightclient.rescan().await { + Ok(_) => "Launching rescan...".to_string(), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn info_server() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { lightclient.do_info().await })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +#[derive(Debug, Clone, uniffi::Record)] +pub struct UfvkInfo { + pub ufvk: String, + pub birthday: u32, +} + +impl ToString for UfvkInfo { + fn to_string(&self) -> String { + self.ufvk.clone() + } +} + +// TODO: rename "get_seed_phrase" or "get_mnemonic_phrase" +// or if other recovery info is being used could rename "get_recovery_info" ? +pub fn get_seed() -> Result { + with_panic_guard(|| { + let wallet_handle = { + let mut guard = LIGHTCLIENT.write().map_err(|_| SeedError::LockPoisoned)?; + let Some(lightclient) = &mut *guard else { + return Err(SeedError::NotInitialized); + }; + // Get a handle we can await on without the LIGHTCLIENT lock + lightclient.wallet.clone() + }; + + let recovery_json = RT.block_on(async move { + let wallet = wallet_handle.read().await; + let Some(recovery_info) = wallet.recovery_info() else { + return Err(SeedError::NoMnemonic); + }; + serde_json::to_string_pretty(&recovery_info).map_err(|_| SeedError::Serialize) + })?; + + Ok(recovery_json) + }) +} + +pub fn get_ufvk() -> Result { + with_panic_guard(|| { + let wallet_handle = { + let mut guard = LIGHTCLIENT.write().map_err(|_| UfvkError::LockPoisoned)?; + let Some(lightclient) = &mut *guard else { + return Err(UfvkError::NotInitialized); + }; + lightclient.wallet.clone() + }; + + RT.block_on(async move { + let wallet = wallet_handle.read().await; + + // Avoid `expect("account 0 must always exist")` + let Some(k) = wallet.unified_key_store.get(&AccountId::ZERO) else { + return Err(UfvkError::NoAccount0); + }; + + let ufvk: UnifiedFullViewingKey = k.try_into().map_err(|_| UfvkError::NotUfvk)?; + + Ok(UfvkInfo { + ufvk: ufvk.encode(&wallet.network), + birthday: u32::from(wallet.birthday), + }) + }) + }) +} + +pub fn change_server(server_uri: String) -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + if server_uri.is_empty() { + lightclient.set_server(http::Uri::default()); + Ok("server set (default)".to_string()) + } else { + match http::Uri::from_str(&server_uri) { + Ok(uri) => { + lightclient.set_server(uri); + Ok("server set".to_string()) + } + Err(_) => Ok("Error: invalid server uri".to_string()), + } + } + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn wallet_kind() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let wallet = lightclient.wallet.read().await; + if wallet.mnemonic().is_some() { + object! {"kind" => "Loaded from seed or mnemonic phrase)", + "transparent" => true, + "sapling" => true, + "orchard" => true, + } + .pretty(2) + } else { + match wallet + .unified_key_store + .get(&AccountId::ZERO) + .expect("account 0 must always exist") + { + UnifiedKeyStore::Spend(_) => object! { + "kind" => "Loaded from unified spending key", + "transparent" => true, + "sapling" => true, + "orchard" => true, + } + .pretty(2), + UnifiedKeyStore::View(ufvk) => object! { + "kind" => "Loaded from unified full viewing key", + "transparent" => ufvk.transparent().is_some(), + "sapling" => ufvk.sapling().is_some(), + "orchard" => ufvk.orchard().is_some(), + } + .pretty(2), + UnifiedKeyStore::Empty => object! { + "kind" => "No keys found", + "transparent" => false, + "sapling" => false, + "orchard" => false, + } + .pretty(2), + } + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn parse_address(address: String) -> Result { + with_panic_guard(|| { + if address.is_empty() { + Ok("Error: The address is empty".to_string()) + } else { + fn make_decoded_chain_pair( + address: &str, + ) -> Option<(zcash_client_backend::address::Address, ChainType)> { + [ + ChainType::Mainnet, + ChainType::Testnet, + ChainType::Regtest(for_test::all_height_one_nus()), + ] + .iter() + .find_map(|chain| { + zcash_keys::address::Address::decode(chain, address).zip(Some(*chain)) + }) + } + if let Some((recipient_address, chain_name)) = make_decoded_chain_pair(&address) { + let chain_name_string = match chain_name { + ChainType::Mainnet => "main", + ChainType::Testnet => "test", + ChainType::Regtest(_) => "regtest", + }; + Ok(match recipient_address { + zcash_keys::address::Address::Sapling(_) => object! { + "status" => "success", + "chain_name" => chain_name_string, + "address_kind" => "sapling", + } + .pretty(2), + zcash_keys::address::Address::Transparent(_) => object! { + "status" => "success", + "chain_name" => chain_name_string, + "address_kind" => "transparent", + } + .pretty(2), + zcash_keys::address::Address::Tex(_) => object! { + "status" => "success", + "chain_name" => chain_name_string, + "address_kind" => "tex", + } + .pretty(2), + zcash_keys::address::Address::Unified(ua) => { + let mut receivers_available = vec![]; + if ua.sapling().is_some() { + receivers_available.push("sapling") + } + if ua.transparent().is_some() { + receivers_available.push("transparent") + } + if ua.orchard().is_some() { + receivers_available.push("orchard"); + object! { + "status" => "success", + "chain_name" => chain_name_string, + "address_kind" => "unified", + "receivers_available" => receivers_available, + "only_orchard_ua" => zcash_keys::address::UnifiedAddress::from_receivers(ua.orchard().cloned(), None, None).expect("To construct UA").encode(&chain_name), + } + .pretty(2) + } else { + object! { + "status" => "success", + "chain_name" => chain_name_string, + "address_kind" => "unified", + "receivers_available" => receivers_available, + } + .pretty(2) + } + } + }) + } else { + Ok(object! { + "status" => "Invalid address", + "chain_name" => json::JsonValue::Null, + "address_kind" => json::JsonValue::Null, + } + .pretty(2)) + } + } + }) +} + +pub fn parse_ufvk(ufvk: String) -> Result { + with_panic_guard(|| { + if ufvk.is_empty() { + Ok("Error: The ufvk is empty".to_string()) + } else { + Ok(json::stringify_pretty( + match Ufvk::decode(&ufvk) { + Ok((network, ufvk)) => { + let mut pools_available = vec![]; + for fvk in ufvk.items_as_parsed() { + match fvk { + zcash_address::unified::Fvk::Orchard(_) => { + pools_available.push("orchard") + } + zcash_address::unified::Fvk::Sapling(_) => { + pools_available.push("sapling") + } + zcash_address::unified::Fvk::P2pkh(_) => { + pools_available.push("transparent") + } + zcash_address::unified::Fvk::Unknown { .. } => pools_available.push( + "Error: Unknown future protocol. Perhaps you're using old software", + ), + } + } + object! { + "status" => "success", + "chain_name" => match network { + NetworkType::Main => "main", + NetworkType::Test => "test", + NetworkType::Regtest => "regtest", + }, + "address_kind" => "ufvk", + "pools_available" => pools_available, + } + } + Err(_) => { + object! { + "status" => "Invalid viewkey", + "chain_name" => json::JsonValue::Null, + "address_kind" => json::JsonValue::Null + } + } + }, + 2, + )) + } + }) +} + +pub fn get_version() -> Result { + with_panic_guard(|| Ok(zingolib::git_description().to_string())) +} + +pub fn get_messages(address: String) -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + match lightclient + .messages_containing(Some(address.as_str())) + .await + { + Ok(value_transfers) => json::JsonValue::from(value_transfers).pretty(2), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn get_balance() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + match lightclient.account_balance(AccountId::ZERO).await { + Ok(bal) => json::JsonValue::from(bal).pretty(2), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn get_total_memobytes_to_address() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + match lightclient.do_total_memobytes_to_address().await { + Ok(total_memo_bytes) => json::JsonValue::from(total_memo_bytes).pretty(2), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn get_total_value_to_address() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + match lightclient.do_total_value_to_address().await { + Ok(total_values) => json::JsonValue::from(total_values).pretty(2), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn get_total_spends_to_address() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + match lightclient.do_total_spends_to_address().await { + Ok(total_spends) => json::JsonValue::from(total_spends).pretty(2), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn zec_price(tor: String) -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let Ok(tor_bool) = tor.parse() else { + return "Error: failed to parse tor setting.".to_string(); + }; + let client_check = match (tor_bool, lightclient.tor_client()) { + (true, Some(tc)) => Ok(Some(tc)), + (true, None) => Err(()), + (false, _) => Ok(None), + }; + let tor_client = match client_check { + Ok(tc) => tc, + Err(_) => { + return "Error: no tor client found. please create a tor client." + .to_string(); + } + }; + + let mut wallet = lightclient.wallet.write().await; + match wallet.update_current_price(tor_client).await { + Ok(price) => object! { "current_price" => price }.pretty(2), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn resend_transaction(txid: String) -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + let txid = match txid_from_hex_encoded_str(&txid) { + Ok(txid) => txid, + Err(e) => return Ok(format!("Error: {e}")), + }; + Ok(RT.block_on(async move { + match lightclient.resend(txid).await { + Ok(_) => "Successfully resent transaction.".to_string(), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn remove_transaction(txid: String) -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + let txid = match txid_from_hex_encoded_str(&txid) { + Ok(txid) => txid, + Err(e) => return Ok(format!("Error: {e}")), + }; + Ok(RT.block_on(async move { + let mut wallet = lightclient.wallet.write().await; + match wallet.remove_unconfirmed_transaction(txid) { + Ok(_) => "Successfully removed transaction.".to_string(), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +// we don't use this anymore... +pub fn get_spendable_balance_with_address( + address: String, + zennies: String, +) -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + let Ok(address) = address_from_str(&address) else { + return Ok("Error: unknown address format".to_string()); + }; + let Ok(zennies) = zennies.parse() else { + return Ok("Error: failed to parse zennies setting.".to_string()); + }; + Ok(RT.block_on(async move { + match lightclient + .max_send_value(address, zennies, AccountId::ZERO) + .await + { + Ok(bal) => object! { "spendable_balance" => bal.into_u64() }.pretty(2), + Err(e) => format!("error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn get_spendable_balance_total() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let wallet = lightclient.wallet.write().await; + let spendable_balance = + match wallet.shielded_spendable_balance(AccountId::ZERO, false) { + Ok(bal) => bal, + Err(e) => return format!("Error: {e}"), + }; + object! { + "spendable_balance" => spendable_balance.into_u64(), + } + .pretty(2) + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn set_option_wallet() -> Result { + with_panic_guard(|| Ok("Error: unimplemented".to_string())) +} + +pub fn get_option_wallet() -> Result { + with_panic_guard(|| Ok("Error: unimplemented".to_string())) +} + +pub fn create_tor_client(data_dir: String) -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + if lightclient.tor_client().is_some() { + return Ok("Tor client already exists.".to_string()); + } + Ok( + match RT.block_on(async move { + lightclient.create_tor_client(Some(data_dir.into())).await + }) { + Ok(_) => "Successfully created tor client.".to_string(), + Err(e) => format!("Error: creating tor client: {e}"), + }, + ) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn remove_tor_client() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + if lightclient.tor_client().is_none() { + return Ok("Tor client is not active.".to_string()); + } + RT.block_on(async move { + lightclient.remove_tor_client().await; + }); + Ok("Successfully removed tor client.".to_string()) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn get_unified_addresses() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { lightclient.unified_addresses_json().await.pretty(2) })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn get_transparent_addresses() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok( + RT.block_on( + async move { lightclient.transparent_addresses_json().await.pretty(2) }, + ), + ) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn create_new_unified_address(receivers: String) -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let mut wallet = lightclient.wallet.write().await; + let network = wallet.network; + let receivers_available = ReceiverSelection { + orchard: receivers.contains('o'), + sapling: receivers.contains('z'), + }; + match wallet.generate_unified_address(receivers_available, AccountId::ZERO) { + Ok((id, unified_address)) => json::object! { + "account" => u32::from(AccountId::ZERO), + "address_index" => id.address_index, + "has_orchard" => unified_address.has_orchard(), + "has_sapling" => unified_address.has_sapling(), + "has_transparent" => unified_address.has_transparent(), + "encoded_address" => unified_address.encode(&network), + } + .pretty(2), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn create_new_transparent_address() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let mut wallet = lightclient.wallet.write().await; + let network = wallet.network; + match wallet.generate_transparent_address(AccountId::ZERO, true) { + Ok((id, transparent_address)) => { + json::object! { + "account" => u32::from(id.account_id()), + "address_index" => id.address_index().index(), + "scope" => id.scope().to_string(), + "encoded_address" => transparent::encode_address(&network, transparent_address), + }.pretty(2) + } + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn check_my_address(address: String) -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let wallet = lightclient.wallet.read().await; + match wallet.is_address_derived_by_keys(&address) { + Ok(address_ref) => address_ref.map_or( + json::object! { "is_wallet_address" => false }, + |address_ref| match address_ref { + WalletAddressRef::Unified { + account_id, + address_index, + has_orchard, + has_sapling, + has_transparent, + encoded_address, + } => json::object! { + "is_wallet_address" => true, + "address_type" => "unified".to_string(), + "address_index" => address_index, + "account_id" => u32::from(account_id), + "has_orchard" => has_orchard, + "has_sapling" => has_sapling, + "has_transparent" => has_transparent, + "encoded_address" => encoded_address, + }, + WalletAddressRef::OrchardInternal { + account_id, + diversifier_index, + encoded_address, + } => json::object! { + "is_wallet_address" => true, + "address_type" => "orchard_internal".to_string(), + "account_id" => u32::from(account_id), + "diversifier_index" => u128::from(diversifier_index).to_string(), + "encoded_address" => encoded_address, + }, + WalletAddressRef::SaplingExternal { + account_id, + diversifier_index, + encoded_address, + } => json::object! { + "is_wallet_address" => true, + "address_type" => "sapling".to_string(), + "account_id" => u32::from(account_id), + "diversifier_index" => u128::from(diversifier_index).to_string(), + "encoded_address" => encoded_address, + }, + WalletAddressRef::Transparent { + account_id, + scope, + address_index, + encoded_address, + } => json::object! { + "is_wallet_address" => true, + "address_type" => "transparent".to_string(), + "account_id" => u32::from(account_id), + "scope" => scope.to_string(), + "address_index" => address_index.index(), + "encoded_address" => encoded_address, + }, + }, + ).pretty(2), + Err(e) => format!("Error: {e}"), + } + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn get_wallet_save_required() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let wallet = lightclient.wallet.read().await; + object! { "save_required" => wallet.save_required }.pretty(2) + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn set_config_wallet_to_test() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let mut wallet = lightclient.wallet.write().await; + wallet.wallet_settings.min_confirmations = NonZeroU32::try_from(1).unwrap(); + wallet.wallet_settings.sync_config.performance_level = PerformanceLevel::Medium; + wallet.save_required = true; + "Successfully set config wallet to test. (1 - Medium)".to_string() + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn set_config_wallet_to_prod( + performance_level: String, + min_confirmations: u32, +) -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let performancetype = match performance_level.as_str() { + "Maximum" => PerformanceLevel::Maximum, + "High" => PerformanceLevel::High, + "Medium" => PerformanceLevel::Medium, + "Low" => PerformanceLevel::Low, + _ => return "Error: Not a valid performance level!".to_string(), + }; + let mut wallet = lightclient.wallet.write().await; + wallet.wallet_settings.min_confirmations = + NonZeroU32::try_from(min_confirmations).unwrap(); + wallet.wallet_settings.sync_config.performance_level = performancetype; + wallet.save_required = true; + "Successfully set config wallet to prod.".to_string() + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn get_config_wallet_performance() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let wallet = lightclient.wallet.read().await; + let performance_level = match wallet.wallet_settings.sync_config.performance_level { + PerformanceLevel::Low => "Low", + PerformanceLevel::Medium => "Medium", + PerformanceLevel::High => "High", + PerformanceLevel::Maximum => "Maximum", + }; + object! { "performance_level" => performance_level }.pretty(2) + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn get_wallet_version() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let wallet = lightclient.wallet.read().await; + let current_version = wallet.current_version(); + let read_version = wallet.read_version(); + object! { + "current_version" => current_version, + "read_version" => read_version + } + .pretty(2) + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +// internal use +fn interpret_memo_string(memo_str: String) -> Result { + // If the string starts with an "0x", and contains only hex chars ([a-f0-9]+) then + // interpret it as a hex + let s_bytes = if memo_str.to_lowercase().starts_with("0x") { + match hex::decode(&memo_str[2..memo_str.len()]) { + Ok(data) => data, + Err(_) => Vec::from(memo_str.as_bytes()), + } + } else { + Vec::from(memo_str.as_bytes()) + }; + + MemoBytes::from_bytes(&s_bytes) + .map_err(|_| format!("Error: creating output. Memo '{:?}' is too long", memo_str)) +} + +pub fn send(send_json: String) -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + let json_args = match json::parse(&send_json) { + Ok(parsed) => parsed, + Err(_) => return "Error: it is not a valid JSON".to_string(), + }; + + let mut receivers = Receivers::new(); + for j in json_args.members() { + let recipient_address = match j["address"].as_str() { + Some(addr) => match ZcashAddress::try_from_encoded(addr) { + Ok(a) => a, + Err(e) => return format!("Error: Invalid address: {e}"), + }, + None => return "Error: Missing address".to_string(), + }; + + let amount = match j["amount"].as_u64() { + Some(a) => match Zatoshis::from_u64(a) { + Ok(a) => a, + Err(e) => return format!("Error: Invalid amount: {e}"), + }, + None => return "Missing amount".to_string(), + }; + + let memo = if let Some(m) = j["memo"].as_str() { + match interpret_memo_string(m.to_string()) { + Ok(memo_bytes) => Some(memo_bytes), + Err(e) => return format!("Error: Invalid memo: {e}"), + } + } else { + None + }; + + receivers.push(zingolib::data::receivers::Receiver { + recipient_address, + amount, + memo, + }); + } + + let request = match transaction_request_from_receivers(receivers) { + Ok(request) => request, + Err(e) => return format!("Error: Request Error: {e}"), + }; + + match lightclient.propose_send(request, AccountId::ZERO).await { + Ok(proposal) => { + let fee = match total_fee(&proposal) { + Ok(fee) => fee, + Err(e) => return object! { "error" => e.to_string() }.pretty(2), + }; + object! { "fee" => fee.into_u64() } + } + Err(e) => { + object! { "error" => e.to_string() } + } + } + .pretty(2) + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn shield() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + match lightclient.propose_shield(AccountId::ZERO).await { + Ok(proposal) => { + if proposal.steps().len() != 1 { + return object! { "error" => "shielding transactions should not have multiple proposal steps" }.pretty(2); + } + let step = proposal.steps().first(); + let Some(value_to_shield) = step + .balance() + .proposed_change() + .iter() + .try_fold(Zatoshis::ZERO, |acc, c| acc + c.value()) else { + return object! { "error" => "shield amount outside valid range of zatoshis" } + .pretty(2); + }; + let fee = step.balance().fee_required(); + object! { + "value_to_shield" => value_to_shield.into_u64(), + "fee" => fee.into_u64(), + } + } + Err(e) => { + object! { "error" => e.to_string() } + } + } + .pretty(2) + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} + +pub fn confirm() -> Result { + with_panic_guard(|| { + let mut guard = LIGHTCLIENT + .write() + .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + if let Some(lightclient) = &mut *guard { + Ok(RT.block_on(async move { + match lightclient + .send_stored_proposal() + .await { + Ok(txids) => { + object! { "txids" => txids.iter().map(|txid| txid.to_string()).collect::>() } + } + Err(e) => { + object! { "error" => e.to_string() } + } + } + .pretty(2) + })) + } else { + Err(ZingolibError::LightclientNotInitialized) + } + }) +} diff --git a/rust/lib/src/lightclient/util.rs b/rust/lib/src/lightclient/util.rs new file mode 100644 index 000000000..7d116e804 --- /dev/null +++ b/rust/lib/src/lightclient/util.rs @@ -0,0 +1,106 @@ +use std::num::NonZeroU32; + +use pepper_sync::config::PerformanceLevel; +use zingo_common_components::protocol::activation_heights::for_test::all_height_one_nus; +use zingolib::{ + config::{ + ChainType, SyncConfig, TransparentAddressDiscovery, ZingoConfig, construct_lightwalletd_uri, + }, + lightclient::LightClient, + wallet::WalletSettings, +}; + +use crate::{ + error::{ConfigError, ZingolibError}, + lightclient::LIGHTCLIENT, + panic_handler::with_panic_guard, +}; + +fn with_lightclient_write(f: F) -> R +where + F: FnOnce(&mut Option) -> R, +{ + let mut guard = match LIGHTCLIENT.write() { + Ok(g) => g, + Err(poisoned) => { + log::warn!("LIGHTCLIENT RwLock poisoned; recovering and clearing poison"); + let g = poisoned.into_inner(); + LIGHTCLIENT.clear_poison(); + g + } + }; + f(&mut guard) +} + +pub(crate) fn reset_lightclient() { + with_lightclient_write(|slot| { + *slot = None; + }); +} + +pub(crate) fn store_client(lightclient: LightClient) -> Result<(), ZingolibError> { + with_lightclient_write(|slot| { + *slot = Some(lightclient); + }); + Ok(()) +} + +pub(crate) fn construct_uri_load_config( + uri: String, + chain_hint: String, + performance_level: String, + min_confirmations: u32, +) -> Result<(ZingoConfig, http::Uri), ConfigError> { + // if uri is empty -> Offline Mode. + let lightwalletd_uri = construct_lightwalletd_uri(Some(uri)); + + let chaintype = match chain_hint.as_str() { + "main" => ChainType::Mainnet, + "test" => ChainType::Testnet, + "regtest" => ChainType::Regtest(all_height_one_nus()), + _ => return Err(ConfigError::InvalidChainHint(chain_hint)), + }; + let performancetype = match performance_level.as_str() { + "Maximum" => PerformanceLevel::Maximum, + "High" => PerformanceLevel::High, + "Medium" => PerformanceLevel::Medium, + "Low" => PerformanceLevel::Low, + _ => return Err(ConfigError::InvalidPerformanceLevel(performance_level)), + }; + + let confirmations = NonZeroU32::try_from(min_confirmations) + .map_err(|_| ConfigError::InvalidMinConfirmations(min_confirmations.to_string()))?; + + let config = zingolib::config::load_clientconfig( + lightwalletd_uri.clone(), + None, + chaintype, + WalletSettings { + sync_config: SyncConfig { + transparent_address_discovery: TransparentAddressDiscovery::minimal(), + performance_level: performancetype, + }, + min_confirmations: confirmations, + }, + NonZeroU32::try_from(1).expect("hard-coded integer"), + "".to_string(), + ) + .map_err(|_| ConfigError::Load)?; + + Ok((config, lightwalletd_uri)) +} + +pub fn init_logging() -> Result { + with_panic_guard(|| { + // this is only for Android + #[cfg(target_os = "android")] + android_logger::init_once( + Config::default().with_min_level(Level::Trace).with_filter( + FilterBuilder::new() + .parse("debug,hello::crate=zingolib") + .build(), + ), + ); + Ok("OK".to_string()) + }) +} From 0d674fe4db946dc73540b4f057267e9a398d02da Mon Sep 17 00:00:00 2001 From: dorianvp Date: Sun, 16 Nov 2025 20:55:35 -0300 Subject: [PATCH 06/13] chore(`ffi`): fix android errors --- rust/android/Cargo.toml | 1 + rust/ios/Cargo.toml | 1 + rust/lib/src/lib.rs | 5 ----- rust/lib/src/lightclient/util.rs | 5 +++++ 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/rust/android/Cargo.toml b/rust/android/Cargo.toml index 5c2bcb85d..f5339bafe 100644 --- a/rust/android/Cargo.toml +++ b/rust/android/Cargo.toml @@ -3,6 +3,7 @@ name = "rustandroid" version = "2.0.0" authors = ["Zingolabs "] edition = "2024" +publish = false [features] ci = [] diff --git a/rust/ios/Cargo.toml b/rust/ios/Cargo.toml index 190f53500..bdb3a6bad 100644 --- a/rust/ios/Cargo.toml +++ b/rust/ios/Cargo.toml @@ -3,6 +3,7 @@ name = "rustios" version = "2.0.0" authors = ["Zingolabs "] edition = "2024" +publish = false [features] ci = [] diff --git a/rust/lib/src/lib.rs b/rust/lib/src/lib.rs index 0ba854ca4..7f423a7c3 100644 --- a/rust/lib/src/lib.rs +++ b/rust/lib/src/lib.rs @@ -8,11 +8,6 @@ pub mod panic_handler; extern crate lazy_static; extern crate android_logger; -#[cfg(target_os = "android")] -use android_logger::{Config, FilterBuilder}; -#[cfg(target_os = "android")] -use log::Level; - #[cfg(test)] mod tests { use base64::Engine; diff --git a/rust/lib/src/lightclient/util.rs b/rust/lib/src/lightclient/util.rs index 7d116e804..7ea9f033f 100644 --- a/rust/lib/src/lightclient/util.rs +++ b/rust/lib/src/lightclient/util.rs @@ -1,5 +1,10 @@ use std::num::NonZeroU32; +#[cfg(target_os = "android")] +use android_logger::{Config, FilterBuilder}; +#[cfg(target_os = "android")] +use log::Level; + use pepper_sync::config::PerformanceLevel; use zingo_common_components::protocol::activation_heights::for_test::all_height_one_nus; use zingolib::{ From e10189f7362c4f744e77b0f7d559c9ce95f396d1 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Tue, 18 Nov 2025 13:52:30 -0300 Subject: [PATCH 07/13] chore(`ffi`): partially add types to kotlin --- .../org/ZingoLabs/ZingoDelegator/RPCModule.kt | 106 +++++++++++++----- index.js | 4 - rust/Dockerfile | 6 +- rust/buildphases/exportbuiltartifacts.sh | 8 +- rust/lib/src/lightclient.rs | 49 ++++++++ rust/lib/src/lightclient/util.rs | 1 + 6 files changed, 133 insertions(+), 41 deletions(-) diff --git a/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/RPCModule.kt b/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/RPCModule.kt index ece06ff6d..e31ee6a05 100644 --- a/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/RPCModule.kt +++ b/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/RPCModule.kt @@ -140,19 +140,26 @@ class RPCModule internal constructor(private val reactContext: ReactApplicationC } @ReactMethod - fun createNewWallet(serveruri: String, chainhint: String, performancelevel: String, minconfirmations: String, promise: Promise) { + fun createNewWallet( + serveruri: String, + chainhint: String, + performancelevel: String, + minconfirmations: String, + promise: Promise, + ) { try { uniffi.zingo.initLogging() - // Create a seed - val resp = uniffi.zingo.initNew(serveruri, chainhint, performancelevel, minconfirmations.toUInt()) - // Log.i("MAIN-Seed", resp) + val result = uniffi.zingo.initNew( + serveruri, + chainhint, + performancelevel, + minconfirmations.toUInt() + ) - if (!resp.lowercase().startsWith(ErrorPrefix.value)) { - saveWalletFile() - } + val value = result.value - promise.resolve(resp) + promise.resolve(value) } catch (e: Exception) { val errorMessage = "Error: [Native] create new wallet: ${e.localizedMessage}" Log.e("MAIN", errorMessage, e) @@ -161,18 +168,29 @@ class RPCModule internal constructor(private val reactContext: ReactApplicationC } @ReactMethod - fun restoreWalletFromSeed(seed: String, birthday: String, serveruri: String, chainhint: String, performancelevel: String, minconfirmations: String, promise: Promise) { + fun restoreWalletFromSeed( + seed: String, + birthday: String, + serveruri: String, + chainhint: String, + performancelevel: String, + minconfirmations: String, + promise: Promise, + ) { try { uniffi.zingo.initLogging() - val resp = uniffi.zingo.initFromSeed(seed, birthday.toUInt(), serveruri, chainhint, performancelevel, minconfirmations.toUInt()) - // Log.i("MAIN", resp) - - if (!resp.lowercase().startsWith(ErrorPrefix.value)) { - saveWalletFile() - } - - promise.resolve(resp) + val result = uniffi.zingo.initFromSeed( + seed, + birthday.toUInt(), + serveruri, + chainhint, + performancelevel, + minconfirmations.toUInt() + ) + val value = result.value + + promise.resolve(value) } catch (e: Exception) { val errorMessage = "Error: [Native] restore wallet from seed: ${e.localizedMessage}" Log.e("MAIN", errorMessage, e) @@ -181,31 +199,48 @@ class RPCModule internal constructor(private val reactContext: ReactApplicationC } @ReactMethod - fun restoreWalletFromUfvk(ufvk: String, birthday: String, serveruri: String, chainhint: String, performancelevel: String, minconfirmations: String, promise: Promise) { + fun restoreWalletFromUfvk( + ufvk: String, + birthday: String, + serveruri: String, + chainhint: String, + performancelevel: String, + minconfirmations: String, + promise: Promise, + ) { try { uniffi.zingo.initLogging() - val resp = uniffi.zingo.initFromUfvk(ufvk, birthday.toUInt(), serveruri, chainhint, performancelevel, minconfirmations.toUInt()) - // Log.i("MAIN", resp) - - if (!resp.lowercase().startsWith(ErrorPrefix.value)) { - saveWalletFile() - } - - promise.resolve(resp) + val result = uniffi.zingo.initFromUfvk( + ufvk, + birthday.toUInt(), + serveruri, + chainhint, + performancelevel, + minconfirmations.toUInt() + ) + val value = result.value + + promise.resolve(value) } catch (e: Exception) { val errorMessage = "Error: [Native] restore wallet from ufvk: ${e.localizedMessage}" Log.e("MAIN", errorMessage, e) promise.resolve(errorMessage) } -} + } @ReactMethod fun loadExistingWallet(serveruri: String, chainhint: String, performancelevel: String, minconfirmations: String, promise: Promise) { promise.resolve(loadExistingWalletNative(serveruri, chainhint, performancelevel, minconfirmations)) } - fun loadExistingWalletNative(serveruri: String, chainhint: String, performancelevel: String, minconfirmations: String): String { + // TODO: https://github.com/zingolabs/crosslink-mobile-client/issues/8 + fun loadExistingWalletNative( + serveruri: String, + chainhint: String, + performancelevel: String, + minconfirmations: String + ): String { try { // Read the file val fileBytes = readFile(WalletFileName.value) @@ -367,9 +402,17 @@ class RPCModule internal constructor(private val reactContext: ReactApplicationC Log.i("MAIN", "file size: $middle8w") - val resp = uniffi.zingo.initFromB64(fileb64.toString(), serveruri, chainhint, performancelevel, minconfirmations.toUInt()) - - return resp + val result = uniffi.zingo.initFromB64( + fileb64.toString(), + serveruri, + chainhint, + performancelevel, + minconfirmations.toUInt() + ) + + // UniFFI now returns InitResult; JS expects a String here, + // so we keep returning the underlying value. + return result.value } catch (e: Exception) { val errorMessage = "Error: [Native] load existing wallet: ${e.localizedMessage}" Log.e("MAIN", errorMessage, e) @@ -377,6 +420,7 @@ class RPCModule internal constructor(private val reactContext: ReactApplicationC } } + @ReactMethod fun restoreExistingWalletBackup(promise: Promise) { val fileBytesBackup: ByteArray diff --git a/index.js b/index.js index 9b7393291..ab0ecbf4f 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,3 @@ -/** - * @format - */ - import { AppRegistry } from 'react-native'; import App from './App'; import { name as appName } from './app.json'; diff --git a/rust/Dockerfile b/rust/Dockerfile index b39fcae44..1dbd0f5a1 100644 --- a/rust/Dockerfile +++ b/rust/Dockerfile @@ -27,9 +27,11 @@ RUN rustup default nightly RUN rustup target add aarch64-apple-ios x86_64-apple-ios \ aarch64-linux-android armv7-linux-androideabi \ i686-linux-android x86_64-linux-android - + +RUN cargo build --release + RUN cargo run --release --features uniffi/cli --bin uniffi-bindgen -- \ - generate ./src/zingo.udl --language kotlin --out-dir ./src + generate --library ../target/release/libzingo.so --language kotlin --out-dir ./src RUN cargo install --version 4.0.1 cargo-ndk RUN cargo install sccache diff --git a/rust/buildphases/exportbuiltartifacts.sh b/rust/buildphases/exportbuiltartifacts.sh index 4be98b1c7..749b5d8d2 100755 --- a/rust/buildphases/exportbuiltartifacts.sh +++ b/rust/buildphases/exportbuiltartifacts.sh @@ -13,16 +13,16 @@ id=$(docker create devlocal/build_android) docker cp \ $id:/opt/zingo/rust/target/x86_64-linux-android/release/libzingo.so \ - ../android/app/src/main/jniLibs/x86_64/libuniffi_zingo.so + ../android/app/src/main/jniLibs/x86_64/libzingo.so docker cp \ $id:/opt/zingo/rust/target/i686-linux-android/release/libzingo.so \ - ../android/app/src/main/jniLibs/x86/libuniffi_zingo.so + ../android/app/src/main/jniLibs/x86/libzingo.so docker cp \ $id:/opt/zingo/rust/target/armv7-linux-androideabi/release/libzingo.so \ - ../android/app/src/main/jniLibs/armeabi-v7a/libuniffi_zingo.so + ../android/app/src/main/jniLibs/armeabi-v7a/libzingo.so docker cp \ $id:/opt/zingo/rust/target/aarch64-linux-android/release/libzingo.so \ - ../android/app/src/main/jniLibs/arm64-v8a/libuniffi_zingo.so + ../android/app/src/main/jniLibs/arm64-v8a/libzingo.so docker cp \ $id:/opt/zingo/rust/lib/src/uniffi/zingo/zingo.kt \ diff --git a/rust/lib/src/lightclient.rs b/rust/lib/src/lightclient.rs index 99cc81809..5490878d0 100644 --- a/rust/lib/src/lightclient.rs +++ b/rust/lib/src/lightclient.rs @@ -59,6 +59,7 @@ lazy_static! { pub static ref RT: Runtime = tokio::runtime::Runtime::new().unwrap(); } +#[uniffi::export] pub fn init_new( server_uri: String, chain_hint: String, @@ -103,6 +104,7 @@ pub fn init_new( } // TODO: change `seed` to `seed_phrase` or `mnemonic_phrase` +#[uniffi::export] pub fn init_from_seed( seed: String, birthday: u32, @@ -146,6 +148,7 @@ pub fn init_from_seed( }) } +#[uniffi::export] pub fn init_from_ufvk( ufvk: String, birthday: u32, @@ -228,6 +231,7 @@ pub fn init_from_b64( }) } +#[uniffi::export] pub fn save_to_b64() -> Result { with_panic_guard(|| { // Return the wallet as a base64 encoded string @@ -252,6 +256,7 @@ pub fn save_to_b64() -> Result { }) } +#[uniffi::export] pub fn check_b64(base64_data: String) -> String { match STANDARD.decode(&base64_data) { Ok(_) => "true".to_string(), @@ -259,14 +264,17 @@ pub fn check_b64(base64_data: String) -> String { } } +#[uniffi::export] pub fn get_developer_donation_address() -> Result { with_panic_guard(|| Ok(zingolib::config::DEVELOPER_DONATION_ADDRESS.to_string())) } +#[uniffi::export] pub fn get_zennies_for_zingo_donation_address() -> Result { with_panic_guard(|| Ok(zingolib::config::ZENNIES_FOR_ZINGO_DONATION_ADDRESS.to_string())) } +#[uniffi::export] pub fn set_crypto_default_provider_to_ring() -> Result { with_panic_guard(|| { Ok(CryptoProvider::get_default().map_or_else( @@ -279,6 +287,7 @@ pub fn set_crypto_default_provider_to_ring() -> Result { }) } +#[uniffi::export] pub fn get_latest_block_server(server_uri: String) -> Result { with_panic_guard(|| { let lightwalletd_uri: http::Uri = match server_uri.parse() { @@ -298,6 +307,7 @@ pub fn get_latest_block_server(server_uri: String) -> Result Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -314,6 +324,7 @@ pub fn get_latest_block_wallet() -> Result { }) } +#[uniffi::export] pub fn get_value_transfers() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -333,6 +344,7 @@ pub fn get_value_transfers() -> Result { }) } +#[uniffi::export] pub fn poll_sync() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -380,6 +392,7 @@ pub fn run_sync() -> Result { }) } +#[uniffi::export] pub fn pause_sync() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -416,6 +429,7 @@ pub fn status_sync() -> Result { }) } +#[uniffi::export] pub fn run_rescan() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -434,6 +448,7 @@ pub fn run_rescan() -> Result { }) } +#[uniffi::export] pub fn info_server() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -461,6 +476,7 @@ impl ToString for UfvkInfo { // TODO: rename "get_seed_phrase" or "get_mnemonic_phrase" // or if other recovery info is being used could rename "get_recovery_info" ? +#[uniffi::export] pub fn get_seed() -> Result { with_panic_guard(|| { let wallet_handle = { @@ -484,6 +500,7 @@ pub fn get_seed() -> Result { }) } +#[uniffi::export] pub fn get_ufvk() -> Result { with_panic_guard(|| { let wallet_handle = { @@ -512,6 +529,7 @@ pub fn get_ufvk() -> Result { }) } +#[uniffi::export] pub fn change_server(server_uri: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -536,6 +554,7 @@ pub fn change_server(server_uri: String) -> Result { }) } +#[uniffi::export] pub fn wallet_kind() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -587,6 +606,7 @@ pub fn wallet_kind() -> Result { }) } +#[uniffi::export] pub fn parse_address(address: String) -> Result { with_panic_guard(|| { if address.is_empty() { @@ -671,6 +691,7 @@ pub fn parse_address(address: String) -> Result { }) } +#[uniffi::export] pub fn parse_ufvk(ufvk: String) -> Result { with_panic_guard(|| { if ufvk.is_empty() { @@ -721,10 +742,12 @@ pub fn parse_ufvk(ufvk: String) -> Result { }) } +#[uniffi::export] pub fn get_version() -> Result { with_panic_guard(|| Ok(zingolib::git_description().to_string())) } +#[uniffi::export] pub fn get_messages(address: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -746,6 +769,7 @@ pub fn get_messages(address: String) -> Result { }) } +#[uniffi::export] pub fn get_balance() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -764,6 +788,7 @@ pub fn get_balance() -> Result { }) } +#[uniffi::export] pub fn get_total_memobytes_to_address() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -782,6 +807,7 @@ pub fn get_total_memobytes_to_address() -> Result { }) } +#[uniffi::export] pub fn get_total_value_to_address() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -800,6 +826,7 @@ pub fn get_total_value_to_address() -> Result { }) } +#[uniffi::export] pub fn get_total_spends_to_address() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -818,6 +845,7 @@ pub fn get_total_spends_to_address() -> Result { }) } +#[uniffi::export] pub fn zec_price(tor: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -853,6 +881,7 @@ pub fn zec_price(tor: String) -> Result { }) } +#[uniffi::export] pub fn resend_transaction(txid: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -875,6 +904,7 @@ pub fn resend_transaction(txid: String) -> Result { }) } +#[uniffi::export] pub fn remove_transaction(txid: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -899,6 +929,7 @@ pub fn remove_transaction(txid: String) -> Result { } // we don't use this anymore... +#[uniffi::export] pub fn get_spendable_balance_with_address( address: String, zennies: String, @@ -929,6 +960,7 @@ pub fn get_spendable_balance_with_address( }) } +#[uniffi::export] pub fn get_spendable_balance_total() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -953,14 +985,17 @@ pub fn get_spendable_balance_total() -> Result { }) } +#[uniffi::export] pub fn set_option_wallet() -> Result { with_panic_guard(|| Ok("Error: unimplemented".to_string())) } +#[uniffi::export] pub fn get_option_wallet() -> Result { with_panic_guard(|| Ok("Error: unimplemented".to_string())) } +#[uniffi::export] pub fn create_tor_client(data_dir: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -984,6 +1019,7 @@ pub fn create_tor_client(data_dir: String) -> Result { }) } +#[uniffi::export] pub fn remove_tor_client() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1003,6 +1039,7 @@ pub fn remove_tor_client() -> Result { }) } +#[uniffi::export] pub fn get_unified_addresses() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1016,6 +1053,7 @@ pub fn get_unified_addresses() -> Result { }) } +#[uniffi::export] pub fn get_transparent_addresses() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1033,6 +1071,7 @@ pub fn get_transparent_addresses() -> Result { }) } +#[uniffi::export] pub fn create_new_unified_address(receivers: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1065,6 +1104,7 @@ pub fn create_new_unified_address(receivers: String) -> Result Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1092,6 +1132,7 @@ pub fn create_new_transparent_address() -> Result { }) } +#[uniffi::export] pub fn check_my_address(address: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1167,6 +1208,7 @@ pub fn check_my_address(address: String) -> Result { }) } +#[uniffi::export] pub fn get_wallet_save_required() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1183,6 +1225,7 @@ pub fn get_wallet_save_required() -> Result { }) } +#[uniffi::export] pub fn set_config_wallet_to_test() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1202,6 +1245,7 @@ pub fn set_config_wallet_to_test() -> Result { }) } +#[uniffi::export] pub fn set_config_wallet_to_prod( performance_level: String, min_confirmations: u32, @@ -1232,6 +1276,7 @@ pub fn set_config_wallet_to_prod( }) } +#[uniffi::export] pub fn get_config_wallet_performance() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1254,6 +1299,7 @@ pub fn get_config_wallet_performance() -> Result { }) } +#[uniffi::export] pub fn get_wallet_version() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1293,6 +1339,7 @@ fn interpret_memo_string(memo_str: String) -> Result { .map_err(|_| format!("Error: creating output. Memo '{:?}' is too long", memo_str)) } +#[uniffi::export] pub fn send(send_json: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1364,6 +1411,7 @@ pub fn send(send_json: String) -> Result { }) } +#[uniffi::export] pub fn shield() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT @@ -1403,6 +1451,7 @@ pub fn shield() -> Result { }) } +#[uniffi::export] pub fn confirm() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT diff --git a/rust/lib/src/lightclient/util.rs b/rust/lib/src/lightclient/util.rs index 7ea9f033f..a9bce3e8e 100644 --- a/rust/lib/src/lightclient/util.rs +++ b/rust/lib/src/lightclient/util.rs @@ -95,6 +95,7 @@ pub(crate) fn construct_uri_load_config( Ok((config, lightwalletd_uri)) } +#[uniffi::export] pub fn init_logging() -> Result { with_panic_guard(|| { // this is only for Android From 9d3fe7635796e9bb25fe229eb8913d63f5e9ac0b Mon Sep 17 00:00:00 2001 From: dorianvp Date: Wed, 19 Nov 2025 15:36:04 -0300 Subject: [PATCH 08/13] chore(`ffi`): new types for Swift --- ios/RPCModule.swift | 30 +++++++++++++++--------------- rust/ios/buildsimulator.sh | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ios/RPCModule.swift b/ios/RPCModule.swift index 4b9a76f69..f7a9f02ed 100644 --- a/ios/RPCModule.swift +++ b/ios/RPCModule.swift @@ -198,7 +198,7 @@ class RPCModule: NSObject { NSLog("[Native] file size: \(size) bytes") if size > 0 { // check if the content is correct. Stored Encoded. - let correct = checkB64(datab64: walletEncodedString) + let correct = checkB64(base64Data: walletEncodedString) if correct == "true" { try self.saveWalletFile(walletEncodedString) } else { @@ -232,8 +232,8 @@ class RPCModule: NSObject { performancelevel: String, minconfirmations: String ) throws -> String { - let seed = try initNew(serveruri: serveruri, chainhint: chainhint, performancelevel: performancelevel, minconfirmations: UInt32(minconfirmations) ?? 0) - let seedStr = String(seed) + let seed = try initNew(serverUri: serveruri, chainHint: chainhint, performanceLevel: performancelevel, minConfirmations: UInt32(minconfirmations) ?? 0) + let seedStr = String(seed.value) if !seedStr.lowercased().hasPrefix(Constants.ErrorPrefix.rawValue) { try self.saveWalletInternal() } @@ -271,8 +271,8 @@ class RPCModule: NSObject { performancelevel: String, minconfirmations: String ) throws -> String { - let seed = try initFromSeed(seed: restoreSeed, birthday: UInt32(birthday) ?? 0, serveruri: serveruri, chainhint: chainhint, performancelevel: performancelevel, minconfirmations: UInt32(minconfirmations) ?? 0) - let seedStr = String(seed) + let seed = try initFromSeed(seed: restoreSeed, birthday: UInt32(birthday) ?? 0, serverUri: serveruri, chainHint: chainhint, performanceLevel: performancelevel, minConfirmations: UInt32(minconfirmations) ?? 0) + let seedStr = String(seed.value) if !seedStr.lowercased().hasPrefix(Constants.ErrorPrefix.rawValue) { try self.saveWalletInternal() } @@ -312,8 +312,8 @@ class RPCModule: NSObject { performancelevel: String, minconfirmations: String ) throws -> String { - let ufvk = try initFromUfvk(ufvk: restoreUfvk, birthday: UInt32(birthday) ?? 0, serveruri: serveruri, chainhint: chainhint, performancelevel: performancelevel, minconfirmations: UInt32(minconfirmations) ?? 0) - let ufvkStr = String(ufvk) + let ufvk = try initFromUfvk(ufvk: restoreUfvk, birthday: UInt32(birthday) ?? 0, serverUri: serveruri, chainHint: chainhint, performanceLevel: performancelevel, minConfirmations: UInt32(minconfirmations) ?? 0) + let ufvkStr = String(ufvk.value) if !ufvkStr.lowercased().hasPrefix(Constants.ErrorPrefix.rawValue) { try self.saveWalletInternal() } @@ -351,8 +351,8 @@ class RPCModule: NSObject { performancelevel: String, minconfirmations: String ) throws -> String { - let seed = try initFromB64(datab64: try self.readWalletUtf8String(), serveruri: serveruri, chainhint: chainhint, performancelevel: performancelevel, minconfirmations: UInt32(minconfirmations) ?? 0) - let seedStr = String(seed) + let seed = try initFromB64(base64Data: try self.readWalletUtf8String(), serverUri: serveruri, chainHint: chainhint, performanceLevel: performancelevel, minConfirmations: UInt32(minconfirmations) ?? 0) + let seedStr = String(seed.value) return seedStr } @@ -385,7 +385,7 @@ class RPCModule: NSObject { let backupEncodedData = try self.readWalletBackup() let walletEncodedData = try self.readWalletUtf8String() // check if the content is correct. Stored Encoded. - let correct = checkB64(datab64: backupEncodedData) + let correct = checkB64(base64Data: backupEncodedData) if correct == "true" { try self.saveWalletFile(backupEncodedData) try self.saveWalletBackupFile(walletEncodedData) @@ -468,7 +468,7 @@ class RPCModule: NSObject { if let serveruri = dict["serveruri"] as? String, let resolve = dict["resolve"] as? RCTPromiseResolveBlock { do { - let resp = try getLatestBlockServer(serveruri: serveruri) + let resp = try getLatestBlockServer(serverUri: serveruri) let respStr = String(resp) DispatchQueue.main.async { resolve(respStr) @@ -883,7 +883,7 @@ class RPCModule: NSObject { if let resolve = dict["resolve"] as? RCTPromiseResolveBlock { do { let resp = try getUfvk() - let respStr = String(resp) + let respStr = String(resp.ufvk) DispatchQueue.main.async { resolve(respStr) } @@ -914,7 +914,7 @@ class RPCModule: NSObject { if let serveruri = dict["serveruri"] as? String, let resolve = dict["resolve"] as? RCTPromiseResolveBlock { do { - let resp = try changeServer(serveruri: serveruri) + let resp = try changeServer(serverUri: serveruri) let respStr = String(resp) DispatchQueue.main.async { resolve(respStr) @@ -1489,7 +1489,7 @@ func fnGetBalanceInfo(_ dict: [AnyHashable: Any]) { func fnCreateTorClientProcess(_ dict: [AnyHashable: Any]) throws { if let resolve = dict["resolve"] as? RCTPromiseResolveBlock { do { - let resp = try createTorClient(datadir: try getDocumentsDirectory()) + let resp = try createTorClient(dataDir: try getDocumentsDirectory()) let respStr = String(resp) DispatchQueue.main.async { resolve(respStr) @@ -1763,7 +1763,7 @@ func fnGetBalanceInfo(_ dict: [AnyHashable: Any]) { let minconfirmations = dict["minconfirmations"] as? String, let resolve = dict["resolve"] as? RCTPromiseResolveBlock { do { - let resp = try setConfigWalletToProd(performancelevel: performancelevel, minconfirmations: UInt32(minconfirmations) ?? 0) + let resp = try setConfigWalletToProd(performanceLevel: performancelevel, minConfirmations: UInt32(minconfirmations) ?? 0) let respStr = String(resp) DispatchQueue.main.async { resolve(respStr) diff --git a/rust/ios/buildsimulator.sh b/rust/ios/buildsimulator.sh index 712870554..d813fe1b8 100755 --- a/rust/ios/buildsimulator.sh +++ b/rust/ios/buildsimulator.sh @@ -19,8 +19,8 @@ export AR_aarch64_apple_ios_sim="${AR_SIMULATOR}" # export AR_x86_64_apple_ios="${AR_SIMULATOR}" cd ../lib -cargo run --release --features="uniffi/cli" --bin uniffi-bindgen generate ./src/zingo.udl --language swift --out-dir ./Generated cargo build --release --target aarch64-apple-ios-sim +cargo run --release --features="uniffi/cli" --bin uniffi-bindgen -- generate --library ../target/aarch64-apple-ios-sim/release/libzingo.dylib --language swift --out-dir ./Generated cargo lipo --release --targets aarch64-apple-ios-sim cp ./Generated/zingo.swift ../../ios From 1ac085b4c6cbd262fc48b8b3fd9045d5c09d9659 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Wed, 19 Nov 2025 22:19:14 -0300 Subject: [PATCH 09/13] chore(`ffi`): Typed return values & Swift fixes --- app/RPCModule/RPCModule.ts | 41 ++- app/RPCModule/index.d.ts | 2 + ios/RPCModule.swift | 200 ++++++++------ rust/lib/src/error.rs | 38 ++- rust/lib/src/lib.rs | 15 +- rust/lib/src/lightclient.rs | 454 +++++++++++++++++-------------- rust/lib/src/lightclient/util.rs | 10 +- rust/lib/src/types.rs | 82 ++++++ 8 files changed, 542 insertions(+), 300 deletions(-) create mode 100644 app/RPCModule/index.d.ts create mode 100644 rust/lib/src/types.rs diff --git a/app/RPCModule/RPCModule.ts b/app/RPCModule/RPCModule.ts index 29a855947..1f7c2514b 100644 --- a/app/RPCModule/RPCModule.ts +++ b/app/RPCModule/RPCModule.ts @@ -1,3 +1,42 @@ import { NativeModules } from 'react-native'; -export default NativeModules.RPCModule; +const { RPCModule } = NativeModules; + +export type WalletKind = + | 'seed_or_mnemonic' + | 'unified_spending_key' + | 'unified_full_viewing_key' + | 'no_keys'; + +export interface WalletPools { + transparent: boolean; + sapling: boolean; + orchard: boolean; +} + +export interface WalletKindInfo { + kind: WalletKind; + pools: WalletPools; +} + +export interface LatestBlockWalletInfo { + height: number; +} + +export interface RpcModule { + walletKindInfo(): Promise; + + getLatestBlockWalletInfo(): Promise; + + changeServerProcess(serverUri: string): Promise; + + setCryptoDefaultProvider(): Promise; + + // TODO: Add remaining methods that are missing here: + // ... +} + +// Type assertion for intellisense +const NativeRPCModule = RPCModule as RpcModule; + +export default NativeRPCModule; diff --git a/app/RPCModule/index.d.ts b/app/RPCModule/index.d.ts new file mode 100644 index 000000000..915e1f764 --- /dev/null +++ b/app/RPCModule/index.d.ts @@ -0,0 +1,2 @@ +export { default } from './RPCModule'; +export * from './RPCModule'; diff --git a/ios/RPCModule.swift b/ios/RPCModule.swift index f7a9f02ed..fbac94bbc 100644 --- a/ios/RPCModule.swift +++ b/ios/RPCModule.swift @@ -192,33 +192,33 @@ class RPCModule: NSObject { func saveWalletInternal() throws { do { - let walletEncodedString = try saveToB64() - if !walletEncodedString.lowercased().hasPrefix(Constants.ErrorPrefix.rawValue) { + guard let walletEncodedString = try saveToB64() else { + NSLog("[Native] No need to save the wallet.") + return + } + + // Optional: sanity check base64 (Rust should already guarantee this) + let isValidB64 = checkB64(base64Data: walletEncodedString) + guard isValidB64 else { + let err = "Error: [Native] Couldn't save the wallet. The encoded content is incorrect." + NSLog(err) + throw FileError.saveFileError(err) + } + + // Approximate decoded size: 3/4 of encoded length let size = (walletEncodedString.count * 3) / 4 NSLog("[Native] file size: \(size) bytes") + if size > 0 { - // check if the content is correct. Stored Encoded. - let correct = checkB64(base64Data: walletEncodedString) - if correct == "true" { - try self.saveWalletFile(walletEncodedString) - } else { - let err = "Error: [Native] Couldn't save the wallet. The Encoded content is incorrect: \(walletEncodedString)" - NSLog(err) - throw FileError.saveFileError(err) - } + try self.saveWalletFile(walletEncodedString) } else { - NSLog("[Native] No need to save the wallet.") + NSLog("[Native] No need to save the wallet (empty buffer).") } - } else { - let err = "Error: [Native] Couldn't save the wallet. \(walletEncodedString)" + } catch { + let err = "Error: [Native] Couldn't save the wallet. \(error)" NSLog(err) throw FileError.saveFileError(err) } - } catch { - let err = "Error: [Native] Couldn't save the wallet. \(error.localizedDescription)" - NSLog(err) - throw FileError.saveFileError(err) - } } func saveWalletBackupInternal() throws { @@ -386,7 +386,7 @@ class RPCModule: NSObject { let walletEncodedData = try self.readWalletUtf8String() // check if the content is correct. Stored Encoded. let correct = checkB64(base64Data: backupEncodedData) - if correct == "true" { + if correct { try self.saveWalletFile(backupEncodedData) try self.saveWalletBackupFile(walletEncodedData) DispatchQueue.main.async { @@ -468,7 +468,7 @@ class RPCModule: NSObject { if let serveruri = dict["serveruri"] as? String, let resolve = dict["resolve"] as? RCTPromiseResolveBlock { do { - let resp = try getLatestBlockServer(serverUri: serveruri) + let resp = try getLatestBlockHeightServer(serverUri: serveruri) let respStr = String(resp) DispatchQueue.main.async { resolve(respStr) @@ -504,10 +504,9 @@ class RPCModule: NSObject { func fnGetLatestBlockWalletInfo(_ dict: [AnyHashable: Any]) { if let resolve = dict["resolve"] as? RCTPromiseResolveBlock { do { - let resp = try getLatestBlockWallet() - let respStr = String(resp) + let latestBlock = try getLatestBlockWallet() DispatchQueue.main.async { - resolve(respStr) + resolve(latestBlock) } } catch { let err = "Error: [Native] Get wallet latest block. \(error.localizedDescription)" @@ -598,9 +597,8 @@ class RPCModule: NSObject { if let resolve = dict["resolve"] as? RCTPromiseResolveBlock { do { let resp = try getValueTransfers() - let respStr = String(resp) DispatchQueue.main.async { - resolve(respStr) + resolve(resp) } } catch { let err = "Error: [Native] Get value transfers. \(error.localizedDescription)" @@ -626,24 +624,31 @@ class RPCModule: NSObject { } func fnSetCryptoDefaultProvider(_ dict: [AnyHashable: Any]) { - if let resolve = dict["resolve"] as? RCTPromiseResolveBlock { - do { - let resp = try setCryptoDefaultProviderToRing() - let respStr = String(resp) - DispatchQueue.main.async { - resolve(respStr) - } - } catch { - let err = "Error: [Native] Setting the crypto provider to ring by default. \(error.localizedDescription)" - NSLog(err) - DispatchQueue.main.async { - resolve(err) - } - } - } else { - let err = "Error: [Native] Setting the crypto provider to ring by default. Command arguments problem." - NSLog(err) + guard + let resolve = dict["resolve"] as? RCTPromiseResolveBlock, + let reject = dict["reject"] as? RCTPromiseRejectBlock + else { + let err = "Error: [Native] Setting the crypto provider to ring by default. Command arguments problem." + NSLog(err) + return + } + + do { + try setCryptoDefaultProviderToRing() + + DispatchQueue.main.async { + resolve(NSNull()) } + } catch { + let nsError = error as NSError + let errMsg = "Error: [Native] Setting the crypto provider to ring by default. \(error.localizedDescription)" + NSLog(errMsg) + + // Use the reject channel for errors + DispatchQueue.main.async { + reject("SET_CRYPTO_DEFAULT_PROVIDER_FAILED", errMsg, nsError) + } + } } @objc(setCryptoDefaultProvider:reject:) @@ -911,30 +916,33 @@ class RPCModule: NSObject { } func fnChangeServerProcess(_ dict: [AnyHashable: Any]) { - if let serveruri = dict["serveruri"] as? String, - let resolve = dict["resolve"] as? RCTPromiseResolveBlock { - do { - let resp = try changeServer(serverUri: serveruri) - let respStr = String(resp) - DispatchQueue.main.async { - resolve(respStr) - } - } catch { - let err = "Error: [Native] change server. \(error.localizedDescription)" - NSLog(err) - DispatchQueue.main.async { - resolve(err) - } - } - } else { - let err = "Error: [Native] change server. Command arguments problem." - NSLog(err) - if let resolve = dict["resolve"] as? RCTPromiseResolveBlock { - DispatchQueue.main.async { - resolve(err) - } - } + guard + let serveruri = dict["serveruri"] as? String, + let resolve = dict["resolve"] as? RCTPromiseResolveBlock + else { + let err = "Error: [Native] change server. Command arguments problem." + NSLog(err) + if let resolve = dict["resolve"] as? RCTPromiseResolveBlock { + resolve(err) + } + return + } + + do { + // New Rust API: returns (), just throws on error + try changeServer(serverUri: serveruri) + + DispatchQueue.main.async { + // Pick whatever you want here: "true", "ok", "server set", etc. + resolve("server set") } + } catch { + let err = "Error: [Native] change server. \(error.localizedDescription)" + NSLog(err) + DispatchQueue.main.async { + resolve(err) + } + } } @objc(changeServerProcess:resolve:reject:) @@ -946,26 +954,50 @@ class RPCModule: NSObject { } } } - + func fnWalletKindInfo(_ dict: [AnyHashable: Any]) { - if let resolve = dict["resolve"] as? RCTPromiseResolveBlock { - do { - let resp = try walletKind() - let respStr = String(resp) - DispatchQueue.main.async { - resolve(respStr) - } - } catch { - let err = "Error: [Native] wallet kind. \(error.localizedDescription)" - NSLog(err) - DispatchQueue.main.async { - resolve(err) - } - } - } else { - let err = "Error: [Native] wallet kind. Command arguments problem." - NSLog(err) + guard let resolve = dict["resolve"] as? RCTPromiseResolveBlock else { + let err = "Error: [Native] wallet kind. Command arguments problem." + NSLog(err) + return + } + + do { + let info = try walletKind() + + let kindString: String + switch info.kind { + case .seedOrMnemonic: + kindString = "seed_or_mnemonic" + case .unifiedSpendingKey: + kindString = "unified_spending_key" + case .unifiedFullViewingKey: + kindString = "unified_full_viewing_key" + case .noKeys: + kindString = "no_keys" + @unknown default: + kindString = "unknown" + } + + let result: [String: Any] = [ + "kind": kindString, + "pools": [ + "transparent": info.pools.transparent, + "sapling": info.pools.sapling, + "orchard": info.pools.orchard, + ] + ] + + DispatchQueue.main.async { + resolve(result) } + } catch { + let err = "Error: [Native] wallet kind. \(error.localizedDescription)" + NSLog(err) + DispatchQueue.main.async { + resolve(err) + } + } } @objc(walletKindInfo:reject:) diff --git a/rust/lib/src/error.rs b/rust/lib/src/error.rs index e58c0d185..6c40e3337 100644 --- a/rust/lib/src/error.rs +++ b/rust/lib/src/error.rs @@ -2,11 +2,17 @@ use crate::panic_handler::FromPanic; #[derive(uniffi::Error, Debug, thiserror::Error)] pub enum ZingolibError { - #[error("Error: Lightclient is not initialized")] - LightclientNotInitialized, + #[error("lightclient error")] + Lightclient(#[from] LightClientError), - #[error("Error: Lightclient lock poisoned")] - LightclientLockPoisoned, + #[error("failed to install default crypto provider")] + CryptoProviderInstall, + + #[error("invalid server uri: {0}")] + InvalidServerUri(String), + + #[error("lightwalletd query failed: {0}")] + Lightwalletd(String), #[error("panic: {0}")] Panic(String), @@ -18,6 +24,30 @@ impl FromPanic for ZingolibError { } } +#[derive(uniffi::Error, Debug, thiserror::Error)] +pub enum LightClientError { + #[error("lightclient not initialized")] + NotInitialized, + + #[error("lightclient lock poisoned")] + LockPoisoned, + + #[error("lightclient save failed")] + SaveError, + + #[error("value_transfers query failed: {0}")] + ValueTransfers(String), + + #[error("panic: {0}")] + Panic(String), +} + +impl FromPanic for LightClientError { + fn from_panic(msg: String) -> Self { + LightClientError::Panic(msg) + } +} + #[derive(Debug, thiserror::Error, uniffi::Error)] pub enum InitError { #[error("invalid input: {0}")] diff --git a/rust/lib/src/lib.rs b/rust/lib/src/lib.rs index 7f423a7c3..6e1a6629b 100644 --- a/rust/lib/src/lib.rs +++ b/rust/lib/src/lib.rs @@ -1,8 +1,11 @@ uniffi::setup_scaffolding!(); +#[warn(unused)] +#[warn(missing_docs)] pub mod error; pub mod lightclient; pub mod panic_handler; +pub mod types; #[macro_use] extern crate lazy_static; @@ -12,7 +15,9 @@ extern crate android_logger; mod tests { use base64::Engine; - use crate::error::{ConfigError, InitError, SeedError, UfvkError, ZingolibError}; + use crate::error::{ + ConfigError, InitError, LightClientError, SeedError, UfvkError, ZingolibError, + }; use crate::panic_handler::with_panic_guard; use crate::{ @@ -153,10 +158,10 @@ mod tests { drain_last_panic(); let result: Result<(), ZingolibError> = - with_panic_guard(|| Err(ZingolibError::LightclientNotInitialized)); + with_panic_guard(|| Err(ZingolibError::Lightclient(LightClientError::NotInitialized))); match result { - Err(ZingolibError::LightclientNotInitialized) => {} + Err(ZingolibError::Lightclient(LightClientError::NotInitialized)) => {} other => panic!("Expected LightclientNotInitialized, got {other:?}"), } @@ -272,9 +277,9 @@ mod tests { #[test] fn check_b64_reports_true_for_valid_and_false_for_invalid_data() { let encoded = base64::engine::general_purpose::STANDARD.encode(b"hello world"); - assert_eq!(check_b64(encoded), "true"); + assert_eq!(check_b64(encoded), true); let invalid = "not base64!!"; - assert_eq!(check_b64(invalid.to_string()), "false"); + assert_eq!(check_b64(invalid.to_string()), false); } } diff --git a/rust/lib/src/lightclient.rs b/rust/lib/src/lightclient.rs index 5490878d0..25e570255 100644 --- a/rust/lib/src/lightclient.rs +++ b/rust/lib/src/lightclient.rs @@ -23,8 +23,9 @@ use zingolib::utils::conversion::{address_from_str, txid_from_hex_encoded_str}; use zingolib::wallet::keys::WalletAddressRef; use zingolib::wallet::keys::unified::{ReceiverSelection, UnifiedKeyStore}; -use crate::error::{SeedError, UfvkError, ZingolibError}; +use crate::error::{LightClientError, SeedError, UfvkError, ZingolibError}; use crate::lightclient::util::{construct_uri_load_config, reset_lightclient, store_client}; +use crate::types::{ValueTransferInfo, WalletHeight, WalletKind, WalletKindInfo, WalletPools}; use crate::{error::InitError, panic_handler::with_panic_guard}; use base64::engine::general_purpose::STANDARD; use pepper_sync::config::PerformanceLevel; @@ -232,36 +233,39 @@ pub fn init_from_b64( } #[uniffi::export] -pub fn save_to_b64() -> Result { +pub fn save_to_b64() -> Result, LightClientError> { with_panic_guard(|| { - // Return the wallet as a base64 encoded string let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - // we need to use STANDARD because swift is expecting the encoded String with padding - // I tried with STANDARD_NO_PAD and the decoding return `nil`. - Ok(RT.block_on(async move { - let mut wallet = lightclient.wallet.write().await; - match wallet.save() { - Ok(Some(wallet_bytes)) => STANDARD.encode(wallet_bytes), - // TODO: check this is better than a custom error when save is not required (empty buffer) - Ok(None) => "".to_string(), - Err(e) => format!("Error: {e}"), + .map_err(|_| LightClientError::LockPoisoned)?; + + let Some(lightclient) = &mut *guard else { + return Err(LightClientError::NotInitialized); + }; + + RT.block_on(async move { + let mut wallet = lightclient.wallet.write().await; + + match wallet.save() { + Ok(Some(wallet_bytes)) => Ok(Some(STANDARD.encode(wallet_bytes))), + Ok(None) => Ok(None), + Err(e) => { + // If you already have a suitable variant, use that instead. + // Example if you add one: + // Err(ZingolibError::WalletSave(e.to_string())) + Err(LightClientError::SaveError) } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } + } + }) }) } +/// Checks if a base64 encoded string is valid +/// +/// FIXME: This should be removed in favor of byte streaming into a buffer #[uniffi::export] -pub fn check_b64(base64_data: String) -> String { - match STANDARD.decode(&base64_data) { - Ok(_) => "true".to_string(), - Err(_) => "false".to_string(), - } +pub fn check_b64(base64_data: String) -> bool { + STANDARD.decode(&base64_data).is_ok() } #[uniffi::export] @@ -275,72 +279,84 @@ pub fn get_zennies_for_zingo_donation_address() -> Result } #[uniffi::export] -pub fn set_crypto_default_provider_to_ring() -> Result { +pub fn set_crypto_default_provider_to_ring() -> Result<(), ZingolibError> { with_panic_guard(|| { - Ok(CryptoProvider::get_default().map_or_else( - || match default_provider().install_default() { - Ok(_) => "true".to_string(), - Err(_) => "Error: Failed to install crypto provider".to_string(), - }, - |_| "true".to_string(), - )) + if CryptoProvider::get_default().is_some() { + Ok(()) + } else { + default_provider() + .install_default() + .map_err(|_| ZingolibError::CryptoProviderInstall) + } }) } #[uniffi::export] -pub fn get_latest_block_server(server_uri: String) -> Result { +pub fn get_latest_block_height_server(server_uri: String) -> Result { with_panic_guard(|| { - let lightwalletd_uri: http::Uri = match server_uri.parse() { - Ok(uri) => uri, - Err(e) => { - return Ok(format!("Error: failed to parse uri. {e}")); - } - }; - Ok( - match RT.block_on(async move { - zingolib::grpc_connector::get_latest_block(lightwalletd_uri).await - }) { - Ok(block_id) => block_id.height.to_string(), - Err(e) => format!("Error: {e}"), - }, - ) + let lightwalletd_uri: http::Uri = server_uri + .parse() + .map_err(|_| ZingolibError::InvalidServerUri(server_uri.clone()))?; + + let block_id = RT + .block_on( + async move { zingolib::grpc_connector::get_latest_block(lightwalletd_uri).await }, + ) + .map_err(|e| ZingolibError::Lightwalletd(e.to_string()))?; + + Ok(block_id.height as u32) }) } #[uniffi::export] -pub fn get_latest_block_wallet() -> Result { +pub fn get_latest_block_wallet() -> Result, ZingolibError> { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.write().await; - object! { "height" => json::JsonValue::from(wallet.sync_state.wallet_height().map(u32::from).unwrap_or(0))}.pretty(2) - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } + .map_err(|_| LightClientError::LockPoisoned)?; + + let Some(lightclient) = &mut *guard else { + return Err(LightClientError::NotInitialized.into()); + }; + let height: Option = RT.block_on(async move { + let wallet = lightclient.wallet.read().await; + wallet + .sync_state + .wallet_height() + .map(|height| WalletHeight { + height: height.into(), + }) + }); + + Ok(height) }) } #[uniffi::export] -pub fn get_value_transfers() -> Result { +pub fn get_value_transfers() -> Result, ZingolibError> { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { + .map_err(|_| LightClientError::LockPoisoned)?; + + let Some(lightclient) = &mut *guard else { + return Err(LightClientError::NotInitialized.into()); + }; + + let transfers = RT + .block_on(async move { let wallet = lightclient.wallet.read().await; - match wallet.value_transfers(true).await { - Ok(value_transfers) => json::JsonValue::from(value_transfers).pretty(2), - Err(e) => format!("Error: {e}"), - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } + wallet.value_transfers(true).await + }) + .map_err(|e| LightClientError::ValueTransfers(e.to_string()))?; + + // ValueTransfers already implements IntoIterator for &ValueTransfers + let vts: Vec = (&transfers) + .into_iter() + .map(ValueTransferInfo::from) + .collect(); + + Ok(vts) }) } @@ -349,9 +365,10 @@ pub fn poll_sync() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; + if let Some(lightclient) = &mut *guard { - Ok(match lightclient.poll_sync() { + let resp = match lightclient.poll_sync() { PollReport::NoHandle => "Sync task has not been launched.".to_string(), PollReport::NotReady => "Sync task is not complete.".to_string(), PollReport::Ready(result) => match result { @@ -361,9 +378,11 @@ pub fn poll_sync() -> Result { } Err(e) => format!("Error: {e}"), }, - }) + }; + + Ok(resp) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -373,21 +392,25 @@ pub fn run_sync() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; + if let Some(lightclient) = &mut *guard { if lightclient.sync_mode() == SyncMode::Paused { + // same semantics as before: this is assumed infallible and will panic if violated lightclient.resume_sync().expect("sync should be paused"); Ok("Resuming sync task...".to_string()) } else { - Ok(RT.block_on(async { + let msg = RT.block_on(async { match lightclient.sync().await { Ok(_) => "Launching sync task...".to_string(), Err(e) => format!("Error: {e}"), } - })) + }); + + Ok(msg) } } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -397,14 +420,17 @@ pub fn pause_sync() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; + if let Some(lightclient) = &mut *guard { - Ok(match lightclient.pause_sync() { + let msg = match lightclient.pause_sync() { Ok(_) => "Pausing sync task...".to_string(), Err(e) => format!("Error: {e}"), - }) + }; + + Ok(msg) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -414,17 +440,20 @@ pub fn status_sync() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; + if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async { + let json = RT.block_on(async { let wallet = lightclient.wallet.read().await; match pepper_sync::sync_status(&*wallet).await { Ok(status) => json::JsonValue::from(status).pretty(2), Err(e) => format!("Error: {e}"), } - })) + }); + + Ok(json) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -434,16 +463,19 @@ pub fn run_rescan() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; + if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { + let msg = RT.block_on(async move { match lightclient.rescan().await { Ok(_) => "Launching rescan...".to_string(), Err(e) => format!("Error: {e}"), } - })) + }); + + Ok(msg) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -453,11 +485,13 @@ pub fn info_server() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; + if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { lightclient.do_info().await })) + let json = RT.block_on(async move { lightclient.do_info().await }); + Ok(json) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -474,7 +508,7 @@ impl ToString for UfvkInfo { } } -// TODO: rename "get_seed_phrase" or "get_mnemonic_phrase" +// TODO: rename to "get_seed_phrase" or "get_mnemonic_phrase" // or if other recovery info is being used could rename "get_recovery_info" ? #[uniffi::export] pub fn get_seed() -> Result { @@ -530,79 +564,91 @@ pub fn get_ufvk() -> Result { } #[uniffi::export] -pub fn change_server(server_uri: String) -> Result { +pub fn change_server(server_uri: String) -> Result<(), ZingolibError> { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - if server_uri.is_empty() { - lightclient.set_server(http::Uri::default()); - Ok("server set (default)".to_string()) - } else { - match http::Uri::from_str(&server_uri) { - Ok(uri) => { - lightclient.set_server(uri); - Ok("server set".to_string()) - } - Err(_) => Ok("Error: invalid server uri".to_string()), - } - } - } else { - Err(ZingolibError::LightclientNotInitialized) + .map_err(|_| LightClientError::LockPoisoned)?; + + let Some(lightclient) = &mut *guard else { + return Err(LightClientError::NotInitialized.into()); + }; + + let trimmed = server_uri.trim(); + + // Empty string -> reset to default URI + if trimmed.is_empty() { + lightclient.set_server(http::Uri::default()); + return Ok(()); } + + // Non-empty -> must parse as a valid URI + let uri = http::Uri::from_str(trimmed) + .map_err(|_| ZingolibError::InvalidServerUri(server_uri.clone()))?; + + lightclient.set_server(uri); + Ok(()) }) } #[uniffi::export] -pub fn wallet_kind() -> Result { +pub fn wallet_kind() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; - if let Some(lightclient) = &mut *guard { - Ok(RT.block_on(async move { - let wallet = lightclient.wallet.read().await; - if wallet.mnemonic().is_some() { - object! {"kind" => "Loaded from seed or mnemonic phrase)", - "transparent" => true, - "sapling" => true, - "orchard" => true, - } - .pretty(2) - } else { - match wallet - .unified_key_store - .get(&AccountId::ZERO) - .expect("account 0 must always exist") - { - UnifiedKeyStore::Spend(_) => object! { - "kind" => "Loaded from unified spending key", - "transparent" => true, - "sapling" => true, - "orchard" => true, - } - .pretty(2), - UnifiedKeyStore::View(ufvk) => object! { - "kind" => "Loaded from unified full viewing key", - "transparent" => ufvk.transparent().is_some(), - "sapling" => ufvk.sapling().is_some(), - "orchard" => ufvk.orchard().is_some(), - } - .pretty(2), - UnifiedKeyStore::Empty => object! { - "kind" => "No keys found", - "transparent" => false, - "sapling" => false, - "orchard" => false, - } - .pretty(2), - } - } - })) - } else { - Err(ZingolibError::LightclientNotInitialized) - } + .map_err(|_| LightClientError::LockPoisoned)?; + + let Some(lightclient) = &mut *guard else { + return Err(LightClientError::NotInitialized.into()); + }; + + let info = RT.block_on(async move { + let wallet = lightclient.wallet.read().await; + + // All pools can be derived + if wallet.mnemonic().is_some() { + return WalletKindInfo { + kind: WalletKind::SeedOrMnemonic, + pools: WalletPools { + transparent: true, + sapling: true, + orchard: true, + }, + }; + } + + // Inspect the unified key store + match wallet.unified_key_store.get(&AccountId::ZERO) { + Some(UnifiedKeyStore::Spend(_)) => WalletKindInfo { + kind: WalletKind::UnifiedSpendingKey, + pools: WalletPools { + transparent: true, + sapling: true, + orchard: true, + }, + }, + + Some(UnifiedKeyStore::View(ufvk)) => WalletKindInfo { + kind: WalletKind::UnifiedFullViewingKey, + pools: WalletPools { + transparent: ufvk.transparent().is_some(), + sapling: ufvk.sapling().is_some(), + orchard: ufvk.orchard().is_some(), + }, + }, + + Some(UnifiedKeyStore::Empty) | None => WalletKindInfo { + kind: WalletKind::NoKeys, + pools: WalletPools { + transparent: false, + sapling: false, + orchard: false, + }, + }, + } + }); + + Ok(info) }) } @@ -752,7 +798,7 @@ pub fn get_messages(address: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { match lightclient @@ -764,7 +810,7 @@ pub fn get_messages(address: String) -> Result { } })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -774,7 +820,7 @@ pub fn get_balance() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { match lightclient.account_balance(AccountId::ZERO).await { @@ -783,7 +829,7 @@ pub fn get_balance() -> Result { } })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -793,7 +839,7 @@ pub fn get_total_memobytes_to_address() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { match lightclient.do_total_memobytes_to_address().await { @@ -802,7 +848,7 @@ pub fn get_total_memobytes_to_address() -> Result { } })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -812,7 +858,7 @@ pub fn get_total_value_to_address() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { match lightclient.do_total_value_to_address().await { @@ -821,7 +867,7 @@ pub fn get_total_value_to_address() -> Result { } })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -831,7 +877,7 @@ pub fn get_total_spends_to_address() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { match lightclient.do_total_spends_to_address().await { @@ -840,7 +886,7 @@ pub fn get_total_spends_to_address() -> Result { } })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -850,7 +896,7 @@ pub fn zec_price(tor: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { let Ok(tor_bool) = tor.parse() else { @@ -876,7 +922,7 @@ pub fn zec_price(tor: String) -> Result { } })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -886,7 +932,7 @@ pub fn resend_transaction(txid: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { let txid = match txid_from_hex_encoded_str(&txid) { Ok(txid) => txid, @@ -899,7 +945,7 @@ pub fn resend_transaction(txid: String) -> Result { } })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -909,7 +955,7 @@ pub fn remove_transaction(txid: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { let txid = match txid_from_hex_encoded_str(&txid) { Ok(txid) => txid, @@ -923,7 +969,7 @@ pub fn remove_transaction(txid: String) -> Result { } })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -937,7 +983,7 @@ pub fn get_spendable_balance_with_address( with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { let Ok(address) = address_from_str(&address) else { return Ok("Error: unknown address format".to_string()); @@ -955,7 +1001,7 @@ pub fn get_spendable_balance_with_address( } })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -965,7 +1011,7 @@ pub fn get_spendable_balance_total() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { let wallet = lightclient.wallet.write().await; @@ -980,7 +1026,7 @@ pub fn get_spendable_balance_total() -> Result { .pretty(2) })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1000,7 +1046,7 @@ pub fn create_tor_client(data_dir: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { if lightclient.tor_client().is_some() { return Ok("Tor client already exists.".to_string()); @@ -1014,7 +1060,7 @@ pub fn create_tor_client(data_dir: String) -> Result { }, ) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1024,7 +1070,7 @@ pub fn remove_tor_client() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { if lightclient.tor_client().is_none() { return Ok("Tor client is not active.".to_string()); @@ -1034,7 +1080,7 @@ pub fn remove_tor_client() -> Result { }); Ok("Successfully removed tor client.".to_string()) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1044,11 +1090,11 @@ pub fn get_unified_addresses() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { lightclient.unified_addresses_json().await.pretty(2) })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1058,7 +1104,7 @@ pub fn get_transparent_addresses() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok( RT.block_on( @@ -1066,7 +1112,7 @@ pub fn get_transparent_addresses() -> Result { ), ) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1076,7 +1122,7 @@ pub fn create_new_unified_address(receivers: String) -> Result Result Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { let mut wallet = lightclient.wallet.write().await; @@ -1127,7 +1173,7 @@ pub fn create_new_transparent_address() -> Result { } })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1137,7 +1183,7 @@ pub fn check_my_address(address: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { let wallet = lightclient.wallet.read().await; @@ -1203,7 +1249,7 @@ pub fn check_my_address(address: String) -> Result { } })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1213,14 +1259,14 @@ pub fn get_wallet_save_required() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { let wallet = lightclient.wallet.read().await; object! { "save_required" => wallet.save_required }.pretty(2) })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1230,7 +1276,7 @@ pub fn set_config_wallet_to_test() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { let mut wallet = lightclient.wallet.write().await; @@ -1240,7 +1286,7 @@ pub fn set_config_wallet_to_test() -> Result { "Successfully set config wallet to test. (1 - Medium)".to_string() })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1253,7 +1299,7 @@ pub fn set_config_wallet_to_prod( with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { let performancetype = match performance_level.as_str() { @@ -1271,7 +1317,7 @@ pub fn set_config_wallet_to_prod( "Successfully set config wallet to prod.".to_string() })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1281,7 +1327,7 @@ pub fn get_config_wallet_performance() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { let wallet = lightclient.wallet.read().await; @@ -1294,7 +1340,7 @@ pub fn get_config_wallet_performance() -> Result { object! { "performance_level" => performance_level }.pretty(2) })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1304,7 +1350,7 @@ pub fn get_wallet_version() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { let wallet = lightclient.wallet.read().await; @@ -1317,7 +1363,7 @@ pub fn get_wallet_version() -> Result { .pretty(2) })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1344,7 +1390,7 @@ pub fn send(send_json: String) -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { let json_args = match json::parse(&send_json) { @@ -1406,7 +1452,7 @@ pub fn send(send_json: String) -> Result { .pretty(2) })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1416,7 +1462,7 @@ pub fn shield() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { match lightclient.propose_shield(AccountId::ZERO).await { @@ -1446,7 +1492,7 @@ pub fn shield() -> Result { .pretty(2) })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } @@ -1456,7 +1502,7 @@ pub fn confirm() -> Result { with_panic_guard(|| { let mut guard = LIGHTCLIENT .write() - .map_err(|_| ZingolibError::LightclientLockPoisoned)?; + .map_err(|_| LightClientError::LockPoisoned)?; if let Some(lightclient) = &mut *guard { Ok(RT.block_on(async move { match lightclient @@ -1472,7 +1518,7 @@ pub fn confirm() -> Result { .pretty(2) })) } else { - Err(ZingolibError::LightclientNotInitialized) + Err(LightClientError::NotInitialized.into()) } }) } diff --git a/rust/lib/src/lightclient/util.rs b/rust/lib/src/lightclient/util.rs index a9bce3e8e..0fe1f006d 100644 --- a/rust/lib/src/lightclient/util.rs +++ b/rust/lib/src/lightclient/util.rs @@ -95,8 +95,14 @@ pub(crate) fn construct_uri_load_config( Ok((config, lightwalletd_uri)) } +/// Initialize logging for mobile clients. +/// +/// - On Android: installs an `android_logger` logger (idempotent). +/// - On other platforms: this is a no-op. +/// +/// Safe to call multiple times. #[uniffi::export] -pub fn init_logging() -> Result { +pub fn init_logging() -> Result<(), ZingolibError> { with_panic_guard(|| { // this is only for Android #[cfg(target_os = "android")] @@ -107,6 +113,6 @@ pub fn init_logging() -> Result { .build(), ), ); - Ok("OK".to_string()) + Ok(()) }) } diff --git a/rust/lib/src/types.rs b/rust/lib/src/types.rs new file mode 100644 index 000000000..56cc235c7 --- /dev/null +++ b/rust/lib/src/types.rs @@ -0,0 +1,82 @@ +use zcash_protocol::consensus::BlockHeight; +use zingolib::wallet::summary::data::ValueTransfer; + +#[derive(Debug, Clone, uniffi::Record)] +pub struct WalletHeight { + pub height: u32, +} + +impl From for WalletHeight { + fn from(block_height: BlockHeight) -> Self { + WalletHeight { + height: block_height.into(), + } + } +} + +#[derive(Debug, Clone, uniffi::Record)] +pub struct ValueTransferInfo { + pub txid: String, + pub datetime: String, + pub status: String, + pub blockheight: u64, + + // For these, start with String and tighten types later + pub transaction_fee: Option, + pub zec_price: Option, + pub kind: String, + pub value: String, + + pub recipient_address: Option, + pub pool_received: Option, + pub memos: Vec, +} + +impl From<&ValueTransfer> for ValueTransferInfo { + fn from(vt: &ValueTransfer) -> Self { + ValueTransferInfo { + txid: vt.txid.to_string(), + datetime: vt.datetime.to_string(), + status: vt.status.to_string(), + blockheight: u64::from(vt.blockheight), + + transaction_fee: vt.transaction_fee, + zec_price: vt.zec_price, + kind: vt.kind.to_string(), + value: vt.value.to_string(), + + recipient_address: vt.recipient_address.clone(), + pool_received: vt.pool_received.clone(), + + memos: vt.memos.iter().map(|m| m.to_string()).collect(), + } + } +} + +#[derive(Debug, Clone, uniffi::Enum)] +pub enum WalletKind { + /// Wallet has a mnemonic (seed phrase) + SeedOrMnemonic, + + /// Wallet was loaded from a unified spending key + UnifiedSpendingKey, + + /// Wallet was loaded from a unified full viewing key + UnifiedFullViewingKey, + + /// No keys found for the given account + NoKeys, +} + +#[derive(Debug, Clone, uniffi::Record)] +pub struct WalletPools { + pub transparent: bool, + pub sapling: bool, + pub orchard: bool, +} + +#[derive(Debug, Clone, uniffi::Record)] +pub struct WalletKindInfo { + pub kind: WalletKind, + pub pools: WalletPools, +} From 7e3ab6d12ff0158467e10d21aa3aa46696e271a8 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Thu, 20 Nov 2025 01:14:45 -0300 Subject: [PATCH 10/13] chore(`ffi`): Kotlin fixes --- .../org/ZingoLabs/ZingoDelegator/RPCModule.kt | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/RPCModule.kt b/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/RPCModule.kt index e31ee6a05..9e2969277 100644 --- a/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/RPCModule.kt +++ b/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/RPCModule.kt @@ -65,38 +65,39 @@ class RPCModule internal constructor(private val reactContext: ReactApplicationC } fun saveWalletFile(): Boolean { - try { + return try { uniffi.zingo.initLogging() - // Get the encoded wallet file - val b64encoded: String = uniffi.zingo.saveToB64() - if (b64encoded.lowercase().startsWith(ErrorPrefix.value)) { - // with error don't save the file. Obviously. - Log.e("MAIN", "Error: [Native] Couldn't save the wallet. $b64encoded") - return false + val b64encoded = uniffi.zingo.saveToB64() + + // No data to save + if (b64encoded.isNullOrEmpty()) { + Log.i("MAIN", "[Native] No need to save the wallet.") + return true } - // Log.i("MAIN", b64encoded) - val correct = uniffi.zingo.checkB64(b64encoded) - if (correct == "false") { - Log.e("MAIN", "Error: [Native] Couldn't save the wallet. The Encoded content is incorrect: $b64encoded") + val correct: Boolean = uniffi.zingo.checkB64(b64encoded) + if (!correct) { + Log.e( + "MAIN", + "Error: [Native] Couldn't save the wallet. The encoded content is incorrect." + ) return false } - // check if the content is correct. Stored Decoded. val fileBytes = Base64.decode(b64encoded, Base64.NO_WRAP) Log.i("MAIN", "[Native] file size: ${fileBytes.size} bytes") - if (fileBytes.size > 0) { + if (fileBytes.isNotEmpty()) { writeFile(WalletFileName.value, fileBytes) - return true } else { - Log.e("MAIN", "[Native] No need to save the wallet.") - return true + Log.i("MAIN", "[Native] No need to save the wallet (empty file).") } + + true } catch (e: Exception) { - Log.e("MAIN", "Error: [Native] Unexpected error. Couldn't save the wallet. $e") - return false + Log.e("MAIN", "Error: [Native] Unexpected error. Couldn't save the wallet.", e) + false } } @@ -548,7 +549,7 @@ class RPCModule internal constructor(private val reactContext: ReactApplicationC CoroutineScope(Dispatchers.IO).launch { try { uniffi.zingo.initLogging() - val resp = uniffi.zingo.getLatestBlockServer(serveruri) + val resp = uniffi.zingo.getLatestBlockHeightServer(serveruri) withContext(Dispatchers.Main) { promise.resolve(resp) From 60afa2052d8527fe51fa3a7955041565936cff8c Mon Sep 17 00:00:00 2001 From: dorianvp Date: Fri, 21 Nov 2025 08:04:48 -0300 Subject: [PATCH 11/13] chore(`ffi`): Add module description for error.rs --- rust/lib/src/error.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rust/lib/src/error.rs b/rust/lib/src/error.rs index 6c40e3337..e8bf70a09 100644 --- a/rust/lib/src/error.rs +++ b/rust/lib/src/error.rs @@ -1,3 +1,5 @@ +//! Error types for Zingolib bindings + use crate::panic_handler::FromPanic; #[derive(uniffi::Error, Debug, thiserror::Error)] From a16fe755fc30670726dedbaf39b1fc44f04c8e78 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Fri, 21 Nov 2025 10:13:47 -0300 Subject: [PATCH 12/13] chore(`ffi`): correct naming for so file --- .github/workflows/android-build.yaml | 4 ++-- .../inactive/android-ubuntu-foss-script-artifact.yaml | 8 ++++---- .gitignore | 2 +- .../java/org/ZingoLabs/ZingoDelegator/MainApplication.kt | 2 +- rust/build_fdroid.sh | 8 ++++---- rust/buildphases/local/exportbuiltartifacts.sh | 8 ++++---- rust/ios/build.sh | 2 +- rust/ios/buildsimulator.sh | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/android-build.yaml b/.github/workflows/android-build.yaml index 536c43302..1753d60a0 100644 --- a/.github/workflows/android-build.yaml +++ b/.github/workflows/android-build.yaml @@ -174,13 +174,13 @@ jobs: - name: Rename the rust lib for uniffi compatibility working-directory: ./rust/target/${{ env.TARGET }}/release - run: mv ./libzingo.so ./libuniffi_zingo.so + run: mv ./libzingo.so ./libzingo.so - name: Upload native rust uses: actions/upload-artifact@v4 with: name: native-android-uniffi-${{ env.ARCH }}-${{ env.CACHE-KEY }} - path: rust/target/${{ env.TARGET }}/release/libuniffi_zingo.so + path: rust/target/${{ env.TARGET }}/release/libzingo.so cache-native-android-uniffi: name: Cache native rust diff --git a/.github/workflows/inactive/android-ubuntu-foss-script-artifact.yaml b/.github/workflows/inactive/android-ubuntu-foss-script-artifact.yaml index 7b076432f..5d1e9ed5d 100644 --- a/.github/workflows/inactive/android-ubuntu-foss-script-artifact.yaml +++ b/.github/workflows/inactive/android-ubuntu-foss-script-artifact.yaml @@ -69,10 +69,10 @@ jobs: sudo mkdir -p ../../android/app/src/main/jniLibs/x86_64 sudo mkdir -p ../../android/app/build/generated/source/uniffi/debug/java/uniffi/zingo sudo mkdir -p ../../android/app/build/generated/source/uniffi/release/java/uniffi/zingo - sudo cp /opt/jniLibs/x86_64/libuniffi_zingo.so ../../android/app/src/main/jniLibs/x86_64/libuniffi_zingo.so - sudo cp /opt/jniLibs/x86/libuniffi_zingo.so ../../android/app/src/main/jniLibs/x86/libuniffi_zingo.so - sudo cp /opt/jniLibs/armeabi-v7a/libuniffi_zingo.so ../../android/app/src/main/jniLibs/armeabi-v7a/libuniffi_zingo.so - sudo cp /opt/jniLibs/arm64-v8a/libuniffi_zingo.so ../../android/app/src/main/jniLibs/arm64-v8a/libuniffi_zingo.so + sudo cp /opt/jniLibs/x86_64/libzingo.so ../../android/app/src/main/jniLibs/x86_64/libzingo.so + sudo cp /opt/jniLibs/x86/libzingo.so ../../android/app/src/main/jniLibs/x86/libzingo.so + sudo cp /opt/jniLibs/armeabi-v7a/libzingo.so ../../android/app/src/main/jniLibs/armeabi-v7a/libzingo.so + sudo cp /opt/jniLibs/arm64-v8a/libzingo.so ../../android/app/src/main/jniLibs/arm64-v8a/libzingo.so sudo cp /opt/jniLibs/kotlin/zingo.kt ../../android/app/build/generated/source/uniffi/debug/java/uniffi/zingo/zingo.kt sudo cp /opt/jniLibs/kotlin/zingo.kt ../../android/app/build/generated/source/uniffi/release/java/uniffi/zingo/zingo.kt diff --git a/.gitignore b/.gitignore index a29f40a58..c3084f5f9 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ ios/vendor/ # Exclude compiled libs android/app/src/main/jniLibs android/app/release/ -ios/libuniffi_zingo.a +ios/libzingo.a ios/zingo.swift ios/zingoFFI.h ios/zingoFFI.modulemap diff --git a/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/MainApplication.kt b/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/MainApplication.kt index e1a6e42fc..e53c172d9 100644 --- a/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/MainApplication.kt +++ b/android/app/src/main/java/org/ZingoLabs/ZingoDelegator/MainApplication.kt @@ -56,7 +56,7 @@ class MainApplication : Application(), ReactApplication { } init { - System.loadLibrary("uniffi_zingo") + System.loadLibrary("zingo") } } } diff --git a/rust/build_fdroid.sh b/rust/build_fdroid.sh index bbb82a90d..9cab8a23b 100644 --- a/rust/build_fdroid.sh +++ b/rust/build_fdroid.sh @@ -184,10 +184,10 @@ mkdir -p /opt/jniLibs/x86 \ && mkdir -p /opt/jniLibs/x86_64 \ && mkdir -p /opt/jniLibs/kotlin -cp ../target/x86_64-linux-android/release/libzingo.so /opt/jniLibs/x86_64/libuniffi_zingo.so -cp ../target/i686-linux-android/release/libzingo.so /opt/jniLibs/x86/libuniffi_zingo.so -cp ../target/armv7-linux-androideabi/release/libzingo.so /opt/jniLibs/armeabi-v7a/libuniffi_zingo.so -cp ../target/aarch64-linux-android/release/libzingo.so /opt/jniLibs/arm64-v8a/libuniffi_zingo.so +cp ../target/x86_64-linux-android/release/libzingo.so /opt/jniLibs/x86_64/libzingo.so +cp ../target/i686-linux-android/release/libzingo.so /opt/jniLibs/x86/libzingo.so +cp ../target/armv7-linux-androideabi/release/libzingo.so /opt/jniLibs/armeabi-v7a/libzingo.so +cp ../target/aarch64-linux-android/release/libzingo.so /opt/jniLibs/arm64-v8a/libzingo.so cp ./src/uniffi/zingo/zingo.kt /opt/jniLibs/kotlin/zingo.kt yes | rustup self uninstall diff --git a/rust/buildphases/local/exportbuiltartifacts.sh b/rust/buildphases/local/exportbuiltartifacts.sh index 4be98b1c7..749b5d8d2 100755 --- a/rust/buildphases/local/exportbuiltartifacts.sh +++ b/rust/buildphases/local/exportbuiltartifacts.sh @@ -13,16 +13,16 @@ id=$(docker create devlocal/build_android) docker cp \ $id:/opt/zingo/rust/target/x86_64-linux-android/release/libzingo.so \ - ../android/app/src/main/jniLibs/x86_64/libuniffi_zingo.so + ../android/app/src/main/jniLibs/x86_64/libzingo.so docker cp \ $id:/opt/zingo/rust/target/i686-linux-android/release/libzingo.so \ - ../android/app/src/main/jniLibs/x86/libuniffi_zingo.so + ../android/app/src/main/jniLibs/x86/libzingo.so docker cp \ $id:/opt/zingo/rust/target/armv7-linux-androideabi/release/libzingo.so \ - ../android/app/src/main/jniLibs/armeabi-v7a/libuniffi_zingo.so + ../android/app/src/main/jniLibs/armeabi-v7a/libzingo.so docker cp \ $id:/opt/zingo/rust/target/aarch64-linux-android/release/libzingo.so \ - ../android/app/src/main/jniLibs/arm64-v8a/libuniffi_zingo.so + ../android/app/src/main/jniLibs/arm64-v8a/libzingo.so docker cp \ $id:/opt/zingo/rust/lib/src/uniffi/zingo/zingo.kt \ diff --git a/rust/ios/build.sh b/rust/ios/build.sh index 99218accc..40ad81ae3 100755 --- a/rust/ios/build.sh +++ b/rust/ios/build.sh @@ -11,4 +11,4 @@ cp ./Generated/zingo.swift ../../ios cp ./Generated/zingoFFI.h ../../ios cp ./Generated/zingoFFI.modulemap ../../ios -cp ../target/universal/release/libzingo.a ../../ios/libuniffi_zingo.a \ No newline at end of file +cp ../target/universal/release/libzingo.a ../../ios/libzingo.a \ No newline at end of file diff --git a/rust/ios/buildsimulator.sh b/rust/ios/buildsimulator.sh index d813fe1b8..1bd953b36 100755 --- a/rust/ios/buildsimulator.sh +++ b/rust/ios/buildsimulator.sh @@ -27,4 +27,4 @@ cp ./Generated/zingo.swift ../../ios cp ./Generated/zingoFFI.h ../../ios cp ./Generated/zingoFFI.modulemap ../../ios -cp ../target/universal/release/libzingo.a ../../ios/libuniffi_zingo.a +cp ../target/universal/release/libzingo.a ../../ios/libzingo.a From 218a18f890314cc241fdd314cb5bf7d9186a67f8 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Fri, 21 Nov 2025 11:59:47 -0300 Subject: [PATCH 13/13] chore(`ffi`): extend types into TS --- app/LoadedApp/LoadedApp.tsx | 1353 ++++++++++++++-------- app/LoadingApp/LoadingApp.tsx | 657 ++++++++--- app/LoadingApp/components/ImportUfvk.tsx | 185 ++- app/RPCModule/RPCModule.ts | 216 +++- 4 files changed, 1729 insertions(+), 682 deletions(-) diff --git a/app/LoadedApp/LoadedApp.tsx b/app/LoadedApp/LoadedApp.tsx index 52c8b29ae..ec57d0c10 100644 --- a/app/LoadedApp/LoadedApp.tsx +++ b/app/LoadedApp/LoadedApp.tsx @@ -11,17 +11,33 @@ import { Platform, ActivityIndicator, } from 'react-native'; -import { BottomTabBarButtonProps, createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { + BottomTabBarButtonProps, + createBottomTabNavigator, +} from '@react-navigation/bottom-tabs'; import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; -import { faDownload, faCog, faRefresh, faPaperPlane, faClockRotateLeft, faComments } from '@fortawesome/free-solid-svg-icons'; +import { + faDownload, + faCog, + faRefresh, + faPaperPlane, + faClockRotateLeft, + faComments, +} from '@fortawesome/free-solid-svg-icons'; import { useTheme } from '@react-navigation/native'; import { I18n } from 'i18n-js'; import * as RNLocalize from 'react-native-localize'; import { isEqual } from 'lodash'; import { StackScreenProps } from '@react-navigation/stack'; import { LoadingAppNavigationState, AppDrawerParamList } from '../types'; -import NetInfo, { NetInfoSubscription, NetInfoState } from '@react-native-community/netinfo/src/index'; -import { activateKeepAwake, deactivateKeepAwake } from '@sayem314/react-native-keep-awake'; +import NetInfo, { + NetInfoSubscription, + NetInfoState, +} from '@react-native-community/netinfo/src/index'; +import { + activateKeepAwake, + deactivateKeepAwake, +} from '@sayem314/react-native-keep-awake'; import RPC from '../rpc'; import RPCModule from '../RPCModule'; @@ -77,7 +93,10 @@ import { RPCSeedType } from '../rpc/types/RPCSeedType'; import { Launching } from '../LoadingApp'; import simpleBiometrics from '../simpleBiometrics'; import ShowAddressAlertAsync from '../../components/Send/components/ShowAddressAlertAsync'; -import { createUpdateRecoveryWalletInfo, removeRecoveryWalletInfo } from '../recoveryWalletInfov10'; +import { + createUpdateRecoveryWalletInfo, + removeRecoveryWalletInfo, +} from '../recoveryWalletInfov10'; import History from '../../components/History'; import Send from '../../components/Send'; @@ -107,7 +126,9 @@ const Rescan = React.lazy(() => import('../../components/Rescan')); const Pools = React.lazy(() => import('../../components/Pools')); const Insight = React.lazy(() => import('../../components/Insight')); const ShowUfvk = React.lazy(() => import('../../components/Ufvk/ShowUfvk')); -const ComputingTxContent = React.lazy(() => import('./components/ComputingTxContent')); +const ComputingTxContent = React.lazy( + () => import('./components/ComputingTxContent'), +); const en = require('../translations/en.json'); const es = require('../translations/es.json'); @@ -122,7 +143,10 @@ const Stack = createNativeStackNavigator(); //const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); type LoadedAppProps = { - navigation: StackScreenProps['navigation']; + navigation: StackScreenProps< + AppStackParamList, + RouteEnum.LoadedApp + >['navigation']; route: StackScreenProps['route']; toggleTheme: (mode: ModeEnum) => void; }; @@ -137,16 +161,27 @@ export default function LoadedApp(props: LoadedAppProps) { const [loading, setLoading] = useState(true); const [language, setLanguage] = useState(LanguageEnum.en); - const [currency, setCurrency] = useState(CurrencyEnum.noCurrency); - const [lightWalletServer, setLightWalletServer] = useState(SERVER_DEFAULT_0); - const [selectLightWalletServer, setSelectLightWalletServer] = useState(SelectServerEnum.custom); - const [validatorServer, setValidatorServer] = useState(SERVER_DEFAULT_0); - const [selectValidatorServer, setSelectValidatorServer] = useState(SelectServerEnum.custom); + const [currency, setCurrency] = useState( + CurrencyEnum.noCurrency, + ); + const [lightWalletServer, setLightWalletServer] = + useState(SERVER_DEFAULT_0); + const [selectLightWalletServer, setSelectLightWalletServer] = + useState(SelectServerEnum.custom); + const [validatorServer, setValidatorServer] = + useState(SERVER_DEFAULT_0); + const [selectValidatorServer, setSelectValidatorServer] = + useState(SelectServerEnum.custom); const [sendAll, setSendAll] = useState(false); const [donation, setDonation] = useState(false); const [privacy, setPrivacy] = useState(false); const [mode, setMode] = useState(ModeEnum.advanced); // by default advanced - const [background, setBackground] = useState({ batches: 0, message: '', date: 0, dateEnd: 0 }); + const [background, setBackground] = useState({ + batches: 0, + message: '', + date: 0, + dateEnd: 0, + }); const [security, setSecurity] = useState({ startApp: true, // activate only this foregroundApp: false, @@ -159,9 +194,12 @@ export default function LoadedApp(props: LoadedAppProps) { }); const [rescanMenu, setRescanMenu] = useState(false); // by default the App store the seed phrase & birthday on KeyChain/KeyStore (Device). - const [recoveryWalletInfoOnDevice, setRecoveryWalletInfoOnDevice] = useState(true); - const [performanceLevel, setPerformanceLevel] = useState(RPCPerformanceLevelEnum.Medium); - const [zenniesDonationAddress, setZenniesDonationAddress] = useState(''); + const [recoveryWalletInfoOnDevice, setRecoveryWalletInfoOnDevice] = + useState(true); + const [performanceLevel, setPerformanceLevel] = + useState(RPCPerformanceLevelEnum.Medium); + const [zenniesDonationAddress, setZenniesDonationAddress] = + useState(''); const file = useMemo( () => ({ en: en, @@ -174,30 +212,54 @@ export default function LoadedApp(props: LoadedAppProps) { ); const i18n = useMemo(() => new I18n(file), [file]); - const translate: (key: string) => TranslateType = (key: string) => i18n.t(key); - - const readOnly = !!props.route.params && props.route.params.readOnly !== undefined ? props.route.params.readOnly : false; - const orchardPool = !!props.route.params && props.route.params.orchardPool !== undefined ? props.route.params.orchardPool : false; - const saplingPool = !!props.route.params && props.route.params.saplingPool !== undefined ? props.route.params.saplingPool : false; - const transparentPool = !!props.route.params && props.route.params.transparentPool !== undefined ? props.route.params.transparentPool : false; - const firstLaunchingMessage = !!props.route.params && props.route.params.firstLaunchingMessage !== undefined ? props.route.params.firstLaunchingMessage : LaunchingModeEnum.opening; + const translate: (key: string) => TranslateType = (key: string) => + i18n.t(key); + + const readOnly = + !!props.route.params && props.route.params.readOnly !== undefined + ? props.route.params.readOnly + : false; + const orchardPool = + !!props.route.params && props.route.params.orchardPool !== undefined + ? props.route.params.orchardPool + : false; + const saplingPool = + !!props.route.params && props.route.params.saplingPool !== undefined + ? props.route.params.saplingPool + : false; + const transparentPool = + !!props.route.params && props.route.params.transparentPool !== undefined + ? props.route.params.transparentPool + : false; + const firstLaunchingMessage = + !!props.route.params && + props.route.params.firstLaunchingMessage !== undefined + ? props.route.params.firstLaunchingMessage + : LaunchingModeEnum.opening; useEffect(() => { (async () => { // fallback if no available language fits const fallback = { languageTag: LanguageEnum.en, isRTL: false }; - const { languageTag, isRTL } = RNLocalize.findBestLanguageTag(Object.keys(file)) || fallback; + const { languageTag, isRTL } = + RNLocalize.findBestLanguageTag(Object.keys(file)) || fallback; // update layout direction I18nManager.forceRTL(isRTL); // If the App is mounting this component, // I know I have to reset the firstInstall prop in settings. - await SettingsFileImpl.writeSettings(SettingsNameEnum.firstInstall, false); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.firstInstall, + false, + ); // If the App is mounting this component, I know I have to update the version prop in settings. - await SettingsFileImpl.writeSettings(SettingsNameEnum.version, translate('version') as string); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.version, + translate('version') as string, + ); //I have to check what language is in the settings const settings = await SettingsFileImpl.readSettings(); @@ -206,7 +268,10 @@ export default function LoadedApp(props: LoadedAppProps) { // for testing //await delay(5000); - if (settings.mode === ModeEnum.basic || settings.mode === ModeEnum.advanced) { + if ( + settings.mode === ModeEnum.basic || + settings.mode === ModeEnum.advanced + ) { setMode(settings.mode); props.toggleTheme(settings.mode); } else { @@ -238,18 +303,26 @@ export default function LoadedApp(props: LoadedAppProps) { await SettingsFileImpl.writeSettings(SettingsNameEnum.language, lang); //console.log('apploaded NO settings', languageTag); } - if (settings.currency === CurrencyEnum.noCurrency || - settings.currency === CurrencyEnum.USDCurrency || - settings.currency === CurrencyEnum.USDTORCurrency) { + if ( + settings.currency === CurrencyEnum.noCurrency || + settings.currency === CurrencyEnum.USDCurrency || + settings.currency === CurrencyEnum.USDTORCurrency + ) { setCurrency(settings.currency); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.currency, currency); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.currency, + currency, + ); } // lightwallet server if (settings.lightWalletserver) { setLightWalletServer(settings.lightWalletserver); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.lightWalletServer, lightWalletServer); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.lightWalletServer, + lightWalletServer, + ); } if ( settings.selectLightWalletServer === SelectServerEnum.auto || @@ -259,13 +332,19 @@ export default function LoadedApp(props: LoadedAppProps) { ) { setSelectLightWalletServer(settings.selectLightWalletServer); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.selectLightWalletServer, selectLightWalletServer); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.selectLightWalletServer, + selectLightWalletServer, + ); } // validator server if (settings.validatorServer) { setValidatorServer(settings.validatorServer); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.validatorServer, validatorServer); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.validatorServer, + validatorServer, + ); } if ( settings.selectValidatorServer === SelectServerEnum.auto || @@ -275,7 +354,10 @@ export default function LoadedApp(props: LoadedAppProps) { ) { setSelectValidatorServer(settings.selectValidatorServer); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.selectValidatorServer, selectValidatorServer); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.selectValidatorServer, + selectValidatorServer, + ); } if (settings.sendAll === true || settings.sendAll === false) { setSendAll(settings.sendAll); @@ -285,7 +367,10 @@ export default function LoadedApp(props: LoadedAppProps) { if (settings.donation === true || settings.donation === false) { setDonation(settings.donation); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.donation, donation); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.donation, + donation, + ); } if (settings.privacy === true || settings.privacy === false) { setPrivacy(settings.privacy); @@ -295,17 +380,29 @@ export default function LoadedApp(props: LoadedAppProps) { if (settings.security) { setSecurity(settings.security); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.security, security); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.security, + security, + ); } if (settings.rescanMenu === true || settings.rescanMenu === false) { setRescanMenu(settings.rescanMenu); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.rescanMenu, rescanMenu); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.rescanMenu, + rescanMenu, + ); } - if (settings.recoveryWalletInfoOnDevice === true || settings.recoveryWalletInfoOnDevice === false) { + if ( + settings.recoveryWalletInfoOnDevice === true || + settings.recoveryWalletInfoOnDevice === false + ) { setRecoveryWalletInfoOnDevice(settings.recoveryWalletInfoOnDevice); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.recoveryWalletInfoOnDevice, recoveryWalletInfoOnDevice); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.recoveryWalletInfoOnDevice, + recoveryWalletInfoOnDevice, + ); } if ( settings.performanceLevel === RPCPerformanceLevelEnum.High || @@ -315,14 +412,19 @@ export default function LoadedApp(props: LoadedAppProps) { ) { setPerformanceLevel(settings.performanceLevel); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.performanceLevel, performanceLevel); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.performanceLevel, + performanceLevel, + ); } // reading background task info const backgroundJson = await BackgroundFileImpl.readBackground(); setBackground(backgroundJson); - const zenniesAddress = await Utils.getZenniesDonationAddress(lightWalletServer.chainName); + const zenniesAddress = await Utils.getZenniesDonationAddress( + lightWalletServer.chainName, + ); setZenniesDonationAddress(zenniesAddress); setLoading(false); @@ -334,7 +436,11 @@ export default function LoadedApp(props: LoadedAppProps) { if (loading) { return ( - + ); } else { return ( @@ -383,14 +489,18 @@ const Loading: React.FC = ({ backgroundColor, spinColor }) => { alignItems: 'center', backgroundColor: backgroundColor, height: '100%', - }}> + }} + > ); }; type LoadedAppClassProps = { - navigationApp: StackScreenProps['navigation']; + navigationApp: StackScreenProps< + AppStackParamList, + RouteEnum.LoadedApp + >['navigation']; route: StackScreenProps['route']; toggleTheme: (mode: ModeEnum) => void; translate: (key: string) => TranslateType; @@ -420,14 +530,23 @@ type LoadedAppClassProps = { type LoadedAppClassState = AppStateLoaded & AppContextLoaded; -const TabPressable: React.FC = ({ colors, ...props }) => { - return ; +const TabPressable: React.FC< + BottomTabBarButtonProps & { colors: ThemeType } +> = ({ colors, ...props }) => { + return ( + + ); }; -const renderTabPressable = (colors: ThemeType) => (props: BottomTabBarButtonProps) => - ; +const renderTabPressable = + (colors: ThemeType) => (props: BottomTabBarButtonProps) => ( + + ); -export class LoadedAppClass extends Component { +export class LoadedAppClass extends Component< + LoadedAppClassProps, + LoadedAppClassState +> { rpc: RPC; appstate: NativeEventSubscription; linking: EmitterSubscription; @@ -495,7 +614,10 @@ export class LoadedAppClass extends Component { - //console.log('LOADED', 'prior', this.state.appStateStatus, 'next', nextAppState); - // let's catch the prior value - const priorAppState = this.state.appStateStatus; - if (Platform.OS === GlobalConst.platformOSios) { + this.appstate = AppState.addEventListener( + EventListenerEnum.change, + async nextAppState => { + //console.log('LOADED', 'prior', this.state.appStateStatus, 'next', nextAppState); + // let's catch the prior value + const priorAppState = this.state.appStateStatus; + if (Platform.OS === GlobalConst.platformOSios) { + if ( + (priorAppState === AppStateStatusEnum.inactive && + nextAppState === AppStateStatusEnum.active) || + (priorAppState === AppStateStatusEnum.active && + nextAppState === AppStateStatusEnum.inactive) + ) { + //console.log('LOADED SAVED IOS do nothing', nextAppState); + this.setState({ appStateStatus: nextAppState }); + return; + } + if ( + priorAppState === AppStateStatusEnum.inactive && + nextAppState === AppStateStatusEnum.background + ) { + console.log('App LOADED IOS is gone to the background!'); + this.setState({ appStateStatus: nextAppState }); + // setting value for background task Android + await AsyncStorage.setItem(GlobalConst.background, GlobalConst.yes); + //console.log('&&&&& background yes in storage &&&&&'); + await this.rpc.clearTimers(); + //console.log('clear timers IOS'); + this.setSyncingStatus({} as RPCSyncStatusType); + //console.log('clear sync status state'); + //console.log('LOADED SAVED IOS background', nextAppState); + // We need to save the wallet file here because + // sometimes the App can lose the last synced chunk + await RPCModule.doSave(); + return; + } + } + if (Platform.OS === GlobalConst.platformOSandroid) { + if (priorAppState !== nextAppState) { + //console.log('LOADED SAVED Android', nextAppState); + this.setState({ appStateStatus: nextAppState }); + } + } if ( - (priorAppState === AppStateStatusEnum.inactive && nextAppState === AppStateStatusEnum.active) || - (priorAppState === AppStateStatusEnum.active && nextAppState === AppStateStatusEnum.inactive) + (priorAppState === AppStateStatusEnum.inactive || + priorAppState === AppStateStatusEnum.background) && + nextAppState === AppStateStatusEnum.active ) { - //console.log('LOADED SAVED IOS do nothing', nextAppState); - this.setState({ appStateStatus: nextAppState }); - return; - } - if (priorAppState === AppStateStatusEnum.inactive && nextAppState === AppStateStatusEnum.background) { - console.log('App LOADED IOS is gone to the background!'); - this.setState({ appStateStatus: nextAppState }); + //console.log('App LOADED Android & IOS has come to the foreground!'); + if (Platform.OS === GlobalConst.platformOSios) { + //console.log('LOADED SAVED IOS foreground', nextAppState); + this.setState({ appStateStatus: nextAppState }); + } + // (PIN or TouchID or FaceID) + const resultBio = this.state.security.foregroundApp + ? await simpleBiometrics({ translate: this.state.translate }) + : true; + // can be: + // - true -> the user do pass the authentication + // - false -> the user do NOT pass the authentication + // - undefined -> no biometric authentication available -> Passcode -> Nothing. + //console.log('BIOMETRIC FOREGROUND --------> ', resultBio); + if (resultBio === false) { + this.navigateToLoadingApp({ + startingApp: true, + biometricsFailed: true, + }); + } else { + // reading background task info + await this.fetchBackgroundSyncing(); + // setting value for background task Android + await AsyncStorage.setItem(GlobalConst.background, GlobalConst.no); + //console.log('&&&&& background no in storage &&&&&'); + // needs this because when the App go from back to fore + // it have to re-launch all the tasks. + await this.rpc.clearTimers(); + await this.rpc.configure(); + //console.log('configure start timers Android & IOS'); + if ( + this.state.backgroundError && + (this.state.backgroundError.title || + this.state.backgroundError.error) + ) { + Alert.alert( + this.state.backgroundError.title, + this.state.backgroundError.error, + ); + this.setBackgroundError('', ''); + } + } + } else if ( + priorAppState === AppStateStatusEnum.active && + (nextAppState === AppStateStatusEnum.inactive || + nextAppState === AppStateStatusEnum.background) + ) { + console.log('App LOADED is gone to the background!'); // setting value for background task Android await AsyncStorage.setItem(GlobalConst.background, GlobalConst.yes); //console.log('&&&&& background yes in storage &&&&&'); await this.rpc.clearTimers(); - //console.log('clear timers IOS'); + //console.log('clear timers'); this.setSyncingStatus({} as RPCSyncStatusType); //console.log('clear sync status state'); - //console.log('LOADED SAVED IOS background', nextAppState); // We need to save the wallet file here because // sometimes the App can lose the last synced chunk await RPCModule.doSave(); - return; - } - } - if (Platform.OS === GlobalConst.platformOSandroid) { - if (priorAppState !== nextAppState) { - //console.log('LOADED SAVED Android', nextAppState); - this.setState({ appStateStatus: nextAppState }); - } - } - if ( - (priorAppState === AppStateStatusEnum.inactive || priorAppState === AppStateStatusEnum.background) && - nextAppState === AppStateStatusEnum.active - ) { - //console.log('App LOADED Android & IOS has come to the foreground!'); - if (Platform.OS === GlobalConst.platformOSios) { - //console.log('LOADED SAVED IOS foreground', nextAppState); - this.setState({ appStateStatus: nextAppState }); - } - // (PIN or TouchID or FaceID) - const resultBio = this.state.security.foregroundApp - ? await simpleBiometrics({ translate: this.state.translate }) - : true; - // can be: - // - true -> the user do pass the authentication - // - false -> the user do NOT pass the authentication - // - undefined -> no biometric authentication available -> Passcode -> Nothing. - //console.log('BIOMETRIC FOREGROUND --------> ', resultBio); - if (resultBio === false) { - this.navigateToLoadingApp({ startingApp: true, biometricsFailed: true }); - } else { - // reading background task info - await this.fetchBackgroundSyncing(); - // setting value for background task Android - await AsyncStorage.setItem(GlobalConst.background, GlobalConst.no); - //console.log('&&&&& background no in storage &&&&&'); - // needs this because when the App go from back to fore - // it have to re-launch all the tasks. - await this.rpc.clearTimers(); - await this.rpc.configure(); - //console.log('configure start timers Android & IOS'); - if (this.state.backgroundError && (this.state.backgroundError.title || this.state.backgroundError.error)) { - Alert.alert(this.state.backgroundError.title, this.state.backgroundError.error); - this.setBackgroundError('', ''); - } - } - } else if ( - priorAppState === AppStateStatusEnum.active && - (nextAppState === AppStateStatusEnum.inactive || nextAppState === AppStateStatusEnum.background) - ) { - console.log('App LOADED is gone to the background!'); - // setting value for background task Android - await AsyncStorage.setItem(GlobalConst.background, GlobalConst.yes); - //console.log('&&&&& background yes in storage &&&&&'); - await this.rpc.clearTimers(); - //console.log('clear timers'); - this.setSyncingStatus({} as RPCSyncStatusType); - //console.log('clear sync status state'); - // We need to save the wallet file here because - // sometimes the App can lose the last synced chunk - await RPCModule.doSave(); - if (Platform.OS === GlobalConst.platformOSios) { - //console.log('LOADED SAVED IOS background', nextAppState); - this.setState({ appStateStatus: nextAppState }); - } - } else { - if (Platform.OS === GlobalConst.platformOSios) { - if (priorAppState !== nextAppState) { - //console.log('LOADED SAVED IOS', nextAppState); + if (Platform.OS === GlobalConst.platformOSios) { + //console.log('LOADED SAVED IOS background', nextAppState); this.setState({ appStateStatus: nextAppState }); } + } else { + if (Platform.OS === GlobalConst.platformOSios) { + if (priorAppState !== nextAppState) { + //console.log('LOADED SAVED IOS', nextAppState); + this.setState({ appStateStatus: nextAppState }); + } + } } - } - }); + }, + ); const initialUrl = await Linking.getInitialURL(); console.log('INITIAL URI', initialUrl); @@ -666,51 +809,61 @@ export class LoadedAppClass extends Component { - console.log('EVENT LISTENER URI', url); - if (url !== null) { - this.readUrl(url); - } - - this.state.navigationHome?.navigate(RouteEnum.HomeStack, { - screen: RouteEnum.Send, - }); - }); + this.linking = Linking.addEventListener( + EventListenerEnum.url, + async ({ url }) => { + console.log('EVENT LISTENER URI', url); + if (url !== null) { + this.readUrl(url); + } - this.unsubscribeNetInfo = NetInfo.addEventListener(async (state: NetInfoState) => { - const { isConnected, type, isConnectionExpensive } = this.state.netInfo; - if ( - isConnected !== state.isConnected || - type !== state.type || - isConnectionExpensive !== state.details?.isConnectionExpensive - ) { - //console.log('fetch net info'); - this.setState({ - netInfo: { - isConnected: state.isConnected, - type: state.type, - isConnectionExpensive: state.details && state.details.isConnectionExpensive, - }, + this.state.navigationHome?.navigate(RouteEnum.HomeStack, { + screen: RouteEnum.Send, }); - if (isConnected !== state.isConnected) { - if (!state.isConnected) { - //console.log('EVENT Loaded: No internet connection.'); - } else { - //console.log('EVENT Loaded: YES internet connection.'); - // restart the interval process again... - await this.rpc.clearTimers(); - await this.rpc.configure(); + }, + ); + + this.unsubscribeNetInfo = NetInfo.addEventListener( + async (state: NetInfoState) => { + const { isConnected, type, isConnectionExpensive } = this.state.netInfo; + if ( + isConnected !== state.isConnected || + type !== state.type || + isConnectionExpensive !== state.details?.isConnectionExpensive + ) { + //console.log('fetch net info'); + this.setState({ + netInfo: { + isConnected: state.isConnected, + type: state.type, + isConnectionExpensive: + state.details && state.details.isConnectionExpensive, + }, + }); + if (isConnected !== state.isConnected) { + if (!state.isConnected) { + //console.log('EVENT Loaded: No internet connection.'); + } else { + //console.log('EVENT Loaded: YES internet connection.'); + // restart the interval process again... + await this.rpc.clearTimers(); + await this.rpc.configure(); + } } } - } - }); + }, + ); }; componentWillUnmount = async () => { await this.rpc.clearTimers(); - this.appstate && typeof this.appstate.remove === 'function' && this.appstate.remove(); + this.appstate && + typeof this.appstate.remove === 'function' && + this.appstate.remove(); this.linking && typeof this.linking === 'function' && this.linking.remove(); - this.unsubscribeNetInfo && typeof this.unsubscribeNetInfo === 'function' && this.unsubscribeNetInfo(); + this.unsubscribeNetInfo && + typeof this.unsubscribeNetInfo === 'function' && + this.unsubscribeNetInfo(); }; keepAwake = (keep: boolean): void => { @@ -726,7 +879,11 @@ export class LoadedAppClass extends Component { - const backgroundJson: BackgroundType = await BackgroundFileImpl.readBackground(); + const backgroundJson: BackgroundType = + await BackgroundFileImpl.readBackground(); if (!isEqual(this.state.background, backgroundJson)) { //console.log('fetch background sync info'); this.setState({ background: backgroundJson }); @@ -816,8 +976,12 @@ export class LoadedAppClass extends Component { - const basicFirstViewSeed = (await SettingsFileImpl.readSettings()).basicFirstViewSeed; + setValueTransfersList = async ( + valueTransfers: ValueTransferType[], + valueTransfersTotal: number, + ) => { + const basicFirstViewSeed = (await SettingsFileImpl.readSettings()) + .basicFirstViewSeed; // only for basic mode if (this.state.mode === ModeEnum.basic) { // only if the user doesn't see the seed the first time @@ -829,8 +993,8 @@ export class LoadedAppClass extends Component 0 ? valueTransfers.filter((vt: ValueTransferType) => vt.confirmations >= 0 && vt.confirmations < GlobalConst.minConfirmations).length : 0; + valueTransfersTotal > 0 + ? valueTransfers.filter( + (vt: ValueTransferType) => + vt.confirmations >= 0 && + vt.confirmations < GlobalConst.minConfirmations, + ).length + : 0; // if a ValueTransfer go from 3 confirmations to > 3 -> Show a message about a ValueTransfer is confirmed this.state.valueTransfers && this.state.valueTransfersTotal !== null && @@ -854,7 +1030,9 @@ export class LoadedAppClass extends Component { const vtNew = valueTransfers.filter( (vt: ValueTransferType) => - vt.txid === vtOld.txid && vt.address === vtOld.address && vt.poolType === vtOld.poolType, + vt.txid === vtOld.txid && + vt.address === vtOld.address && + vt.poolType === vtOld.poolType, ); //console.log('old', vtOld); //console.log('new', vtNew); @@ -862,7 +1040,10 @@ export class LoadedAppClass extends Component 0 && vtNew[0].confirmations > 0) { let message: string = ''; let title: string = ''; - if (vtNew[0].kind === ValueTransferKindEnum.Received && vtNew[0].amount > 0) { + if ( + vtNew[0].kind === ValueTransferKindEnum.Received && + vtNew[0].amount > 0 + ) { message = (this.state.translate('loadedapp.incoming-funds') as string) + (this.state.translate('history.received') as string) + @@ -870,10 +1051,18 @@ export class LoadedAppClass extends Component 0) { + title = this.state.translate( + 'loadedapp.receive-menu', + ) as string; + } else if ( + vtNew[0].kind === ValueTransferKindEnum.MemoToSelf && + vtNew[0].fee && + vtNew[0].fee > 0 + ) { message = - (this.state.translate('loadedapp.valuetransfer-confirmed') as string) + + (this.state.translate( + 'loadedapp.valuetransfer-confirmed', + ) as string) + (this.state.translate('history.memotoself') as string) + (vtNew[0].fee ? ((' ' + this.state.translate('send.fee')) as string) + @@ -883,9 +1072,15 @@ export class LoadedAppClass extends Component 0) { + } else if ( + vtNew[0].kind === ValueTransferKindEnum.SendToSelf && + vtNew[0].fee && + vtNew[0].fee > 0 + ) { message = - (this.state.translate('loadedapp.valuetransfer-confirmed') as string) + + (this.state.translate( + 'loadedapp.valuetransfer-confirmed', + ) as string) + (this.state.translate('history.sendtoself') as string) + (vtNew[0].fee ? ((' ' + this.state.translate('send.fee')) as string) + @@ -895,7 +1090,10 @@ export class LoadedAppClass extends Component 0) { + } else if ( + vtNew[0].kind === ValueTransferKindEnum.Rejection && + vtNew[0].amount > 0 + ) { // not so sure about this `kind`... // I guess the wallet is receiving some refund from a TEX sent. message = @@ -905,8 +1103,13 @@ export class LoadedAppClass extends Component 0) { + title = this.state.translate( + 'loadedapp.receive-menu', + ) as string; + } else if ( + vtNew[0].kind === ValueTransferKindEnum.Shield && + vtNew[0].amount > 0 + ) { message = (this.state.translate('loadedapp.incoming-funds') as string) + (this.state.translate('history.shield') as string) + @@ -914,8 +1117,13 @@ export class LoadedAppClass extends Component 0) { + title = this.state.translate( + 'loadedapp.receive-menu', + ) as string; + } else if ( + vtNew[0].kind === ValueTransferKindEnum.Sent && + vtNew[0].amount > 0 + ) { message = (this.state.translate('loadedapp.payment-made') as string) + (this.state.translate('history.sent') as string) + @@ -933,7 +1141,7 @@ export class LoadedAppClass extends Component { - if (!isEqual(this.state.messages, messages) || this.state.messagesTotal !== messagesTotal) { + if ( + !isEqual(this.state.messages, messages) || + this.state.messagesTotal !== messagesTotal + ) { //console.log('fetch messages'); //const start = Date.now(); this.setState({ messages, messagesTotal }); @@ -980,7 +1191,9 @@ export class LoadedAppClass extends Component { + setAllAddresses = ( + addresses: (UnifiedAddressClass | TransparentAddressClass)[], + ) => { if (!isEqual(this.state.addresses, addresses)) { //console.log('fetch addresses'); //const start = Date.now(); @@ -989,8 +1202,12 @@ export class LoadedAppClass extends Component 0) { // the last Unified Address created. - const defaultUAArray = addresses.filter((a: UnifiedAddressClass | TransparentAddressClass) => a.addressKind === AddressKindEnum.u); - const defaultUA: string = defaultUAArray[defaultUAArray.length - 1].address; + const defaultUAArray = addresses.filter( + (a: UnifiedAddressClass | TransparentAddressClass) => + a.addressKind === AddressKindEnum.u, + ); + const defaultUA: string = + defaultUAArray[defaultUAArray.length - 1].address; if (this.state.defaultUnifiedAddress !== defaultUA) { this.setState({ defaultUnifiedAddress: defaultUA }); } @@ -1038,7 +1255,10 @@ export class LoadedAppClass extends Component => { + sendTransaction = async ( + sendPageState: SendPageStateClass, + ): Promise => { try { // Construct a sendJson from the sendPage state const { lightWalletserver, donation, defaultUnifiedAddress } = this.state; - const sendJson = await Utils.getSendManyJSON(sendPageState, defaultUnifiedAddress, lightWalletserver, donation); + const sendJson = await Utils.getSendManyJSON( + sendPageState, + defaultUnifiedAddress, + lightWalletserver, + donation, + ); //const start = Date.now(); const txid = await this.rpc.sendTransaction(sendJson); //console.log('&&&&&&&&&&&&&& send tx', Date.now() - start); @@ -1130,34 +1357,34 @@ export class LoadedAppClass extends Component await this.onClickOKChangeWallet({ screen: 3, startingApp: false }), + onPress: async () => + await this.onClickOKChangeWallet({ + screen: 3, + startingApp: false, + }), }, { text: translate('cancel') as string, style: 'cancel' }, ], @@ -1197,7 +1428,10 @@ export class LoadedAppClass extends Component { @@ -1214,7 +1448,9 @@ export class LoadedAppClass extends Component we need tor client Just in case. - if (this.state.currency === CurrencyEnum.USDTORCurrency || this.state.currency === CurrencyEnum.USDCurrency) { + if ( + this.state.currency === CurrencyEnum.USDTORCurrency || + this.state.currency === CurrencyEnum.USDCurrency + ) { const resp: string = await RPCModule.createTorClientProcess(); if (resp && resp.toLowerCase().startsWith(GlobalConst.error)) { this.setLastError(`Create tor client error: ${resp}`); @@ -1309,12 +1555,12 @@ export class LoadedAppClass extends Component => { - await SettingsFileImpl.writeSettings(SettingsNameEnum.selectLightWalletServer, value); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.selectLightWalletServer, + value, + ); this.setState({ selectLightWalletServer: value as SelectServerEnum, }); @@ -1425,8 +1677,13 @@ export class LoadedAppClass extends Component => { - await SettingsFileImpl.writeSettings(SettingsNameEnum.recoveryWalletInfoOnDevice, value); + setRecoveryWalletInfoOnDeviceOption = async ( + value: boolean, + ): Promise => { + await SettingsFileImpl.writeSettings( + SettingsNameEnum.recoveryWalletInfoOnDevice, + value, + ); this.setState({ recoveryWalletInfoOnDevice: value as boolean, }); @@ -1439,8 +1696,13 @@ export class LoadedAppClass extends Component => { - await SettingsFileImpl.writeSettings(SettingsNameEnum.performanceLevel, value); + setPerformanceLevelOption = async ( + value: RPCPerformanceLevelEnum, + ): Promise => { + await SettingsFileImpl.writeSettings( + SettingsNameEnum.performanceLevel, + value, + ); this.setState({ performanceLevel: value as RPCPerformanceLevelEnum, }); @@ -1448,10 +1710,16 @@ export class LoadedAppClass extends Component { - if (this.state.newLightWalletServer && this.state.newSelectLightWalletServer) { - const beforeServer = this.state.lightWalletserver; - - const resultStrServerPromise = await RPCModule.changeServerProcess(this.state.newLightWalletServer.uri); - const timeoutServerPromise = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error('Promise changeserver Timeout 30 seconds')); - }, 30 * 1000); - }); + const { newLightWalletServer, newSelectLightWalletServer } = this.state; + + if (!newLightWalletServer || !newSelectLightWalletServer) { + return; + } + const previousServer = this.state.lightWalletserver; - const resultStrServer: string = await Promise.race([resultStrServerPromise, timeoutServerPromise]); - //console.log(resultStrServer); + const changeServerPromise = RPCModule.changeServerProcess( + newLightWalletServer.uri, + ); - if (resultStrServer && resultStrServer.toLowerCase().startsWith(GlobalConst.error)) { - //console.log(`Error change server ${value} - ${resultStr}`); - this.addLastSnackbar({ - message: `${this.state.translate('loadedapp.changeservernew-error')} ${resultStrServer}`, - screenName: [this.screenName], - }); - return; - } else { - //console.log(`change server ok ${value}`); - } + const timeoutServerPromise: Promise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Promise changeserver Timeout 30 seconds')); + }, 30 * 1000); + }); - await SettingsFileImpl.writeSettings(SettingsNameEnum.lightWalletServer, this.state.newLightWalletServer); - await SettingsFileImpl.writeSettings(SettingsNameEnum.selectLightWalletServer, this.state.newSelectLightWalletServer); - this.setState({ - lightWalletserver: this.state.newLightWalletServer, - selectLightWalletServer: this.state.selectLightWalletServer, - newLightWalletServer: {} as ServerType, - newSelectLightWalletServer: null, + // 3) Race them, and handle errors via try/catch + try { + // Option 2 flavored: map success to a constant value if you *really* + // want a string, but in practice you don't need it. + await Promise.race([ + changeServerPromise, // Promise + timeoutServerPromise, // Promise + ]); + // if we get here, server changed successfully within 30s + } catch (err) { + const msg = + err instanceof Error ? err.message : String(err ?? 'Unknown error'); + + this.addLastSnackbar({ + message: `${this.state.translate('loadedapp.changeservernew-error')} ${msg}`, + screenName: [this.screenName], }); - await this.rpc.fetchInfoAndServerHeight(); + return; // abort flow on error + } - let resultStr2 = ''; - // if the server was testnet or regtest -> no need backup the wallet. - if (beforeServer.chainName === ChainNameEnum.mainChainName) { - // backup - resultStr2 = (await this.rpc.changeWallet()) as string; - } else { - // no backup - resultStr2 = (await this.rpc.changeWalletNoBackup()) as string; - } + await SettingsFileImpl.writeSettings( + SettingsNameEnum.lightWalletServer, + newLightWalletServer, + ); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.selectLightWalletServer, + newSelectLightWalletServer, + ); + this.setState({ + lightWalletserver: this.state.newLightWalletServer, + selectLightWalletServer: this.state.selectLightWalletServer, + newLightWalletServer: {} as ServerType, + newSelectLightWalletServer: null, + }); - //console.log("jc change", resultStr); - if (resultStr2 && resultStr2.toLowerCase().startsWith(GlobalConst.error)) { - //console.log(`Error change wallet. ${resultStr}`); - createAlert( - this.setBackgroundError, - this.addLastSnackbar, - [this.screenName], - this.state.translate('loadedapp.changingwallet-label') as string, - resultStr2, - false, - this.state.translate, - sendEmail, - this.state.zingolibVersion, - ); - //return; - } + await this.rpc.fetchInfoAndServerHeight(); - // no need to restart the tasks because is about to restart the app. - this.navigateToLoadingApp({ startingApp: false }); + let resultStr2 = ''; + // if the server was testnet or regtest -> no need backup the wallet. + if (previousServer.chainName === ChainNameEnum.mainChainName) { + // backup + resultStr2 = (await this.rpc.changeWallet()) as string; + } else { + // no backup + resultStr2 = (await this.rpc.changeWalletNoBackup()) as string; } + + //console.log("jc change", resultStr); + if (resultStr2 && resultStr2.toLowerCase().startsWith(GlobalConst.error)) { + //console.log(`Error change wallet. ${resultStr}`); + createAlert( + this.setBackgroundError, + this.addLastSnackbar, + [this.screenName], + this.state.translate('loadedapp.changingwallet-label') as string, + resultStr2, + false, + this.state.translate, + sendEmail, + this.state.zingolibVersion, + ); + //return; + } + + // no need to restart the tasks because is about to restart the app. + this.navigateToLoadingApp({ startingApp: false }); }; setBackgroundError = (title: string, error: string) => { @@ -1606,7 +1893,10 @@ export class LoadedAppClass extends Component { const newSnackbars = this.state.snackbars; // if the last one is the same don't do anything. - if (newSnackbars.length > 0 && newSnackbars[newSnackbars.length - 1].message === snackbar.message) { + if ( + newSnackbars.length > 0 && + newSnackbars[newSnackbars.length - 1].message === snackbar.message + ) { return; } newSnackbars.push(snackbar); @@ -1618,7 +1908,9 @@ export class LoadedAppClass extends Component { - const newSnackbars = this.state.snackbars.filter((s: SnackbarType) => s.screenName.includes(screenName)); + const newSnackbars = this.state.snackbars.filter((s: SnackbarType) => + s.screenName.includes(screenName), + ); newSnackbars.shift(); this.setState({ snackbars: newSnackbars }); }; @@ -1635,7 +1927,9 @@ export class LoadedAppClass extends Component { + setNavigationHome = ( + navigationHome: DrawerContentComponentProps['navigation'], + ) => { if (!this.state.navigationHome) { this.setState({ navigationHome, @@ -1712,7 +2006,10 @@ export class LoadedAppClass extends Component { + const fnTabBarIcon = ( + route: { name: string; key: string }, + focused: boolean, + ) => { var iconName; if (route.name === RouteEnum.History) { @@ -1721,11 +2018,12 @@ export class LoadedAppClass extends Component 0 && totalBalance.confirmedOrchardBalance === 0) || - (totalBalance.totalSaplingBalance > 0 && totalBalance.confirmedSaplingBalance === 0) || - (totalBalance.totalTransparentBalance > 0 && totalBalance.confirmedTransparentBalance === 0) - ) && + ((totalBalance.totalOrchardBalance > 0 && + totalBalance.confirmedOrchardBalance === 0) || + (totalBalance.totalSaplingBalance > 0 && + totalBalance.confirmedSaplingBalance === 0) || + (totalBalance.totalTransparentBalance > 0 && + totalBalance.confirmedTransparentBalance === 0)) && somePending ) { iconName = faRefresh; @@ -1742,7 +2040,11 @@ export class LoadedAppClass extends Component - + ); }; @@ -1762,132 +2064,200 @@ export class LoadedAppClass extends Component - + {props => { useEffect(() => { this.setNavigationHome(props.navigation); }); return ( - <> - {mode === ModeEnum.advanced || - (valueTransfersTotal !== null && valueTransfersTotal > 0) || - (!readOnly && !!totalBalance && totalBalance.confirmedOrchardBalance + totalBalance.confirmedSaplingBalance > 0) ? ( - ({ - tabBarIcon: ({ focused }) => fnTabBarIcon(route, focused), - tabBarIconStyle: { - alignSelf: 'center', - marginBottom: 2, - }, - tabBarLabel: route.name === RouteEnum.History - ? (translate('loadedapp.history-menu') as string) - : route.name === RouteEnum.Send - ? (translate('loadedapp.send-menu') as string) - : route.name === RouteEnum.Receive - ? (translate('loadedapp.receive-menu') as string) - : route.name === RouteEnum.Messages - ? (translate('loadedapp.messages-menu') as string) - : '', - tabBarLabelPosition: 'below-icon', - tabBarLabelStyle: { - alignSelf: 'center', - fontSize: 14, - }, - tabBarItemStyle: { - height: 60, - }, - tabBarActiveTintColor: colors.background, - tabBarActiveBackgroundColor: colors.primaryDisabled, - tabBarInactiveTintColor: colors.money, - tabBarInactiveBackgroundColor: colors.sideMenuBackground, - tabBarStyle: { - borderTopWidth: 1, - height: 60, - }, - headerShown: false, - tabBarButton: renderTabPressable(colors), - })}> - - {propsTab => ( - props.navigation.toggleDrawer() /* header */} - setShieldingAmount={this.setShieldingAmount /* header */} - setScrollToTop={this.setScrollToTop /* header & history */} - scrollToTop={scrollToTop /* history */} - setScrollToBottom={this.setScrollToBottom /* header & messages */} + <> + {mode === ModeEnum.advanced || + (valueTransfersTotal !== null && + valueTransfersTotal > 0) || + (!readOnly && + !!totalBalance && + totalBalance.confirmedOrchardBalance + + totalBalance.confirmedSaplingBalance > + 0) ? ( + ({ + tabBarIcon: ({ focused }) => + fnTabBarIcon(route, focused), + tabBarIconStyle: { + alignSelf: 'center', + marginBottom: 2, + }, + tabBarLabel: + route.name === RouteEnum.History + ? (translate( + 'loadedapp.history-menu', + ) as string) + : route.name === RouteEnum.Send + ? (translate('loadedapp.send-menu') as string) + : route.name === RouteEnum.Receive + ? (translate( + 'loadedapp.receive-menu', + ) as string) + : route.name === RouteEnum.Messages + ? (translate( + 'loadedapp.messages-menu', + ) as string) + : '', + tabBarLabelPosition: 'below-icon', + tabBarLabelStyle: { + alignSelf: 'center', + fontSize: 14, + }, + tabBarItemStyle: { + height: 60, + }, + tabBarActiveTintColor: colors.background, + tabBarActiveBackgroundColor: colors.primaryDisabled, + tabBarInactiveTintColor: colors.money, + tabBarInactiveBackgroundColor: + colors.sideMenuBackground, + tabBarStyle: { + borderTopWidth: 1, + height: 60, + }, + headerShown: false, + tabBarButton: renderTabPressable(colors), + })} + > + + {propsTab => ( + + props.navigation.toggleDrawer() /* header */ + } + setShieldingAmount={ + this.setShieldingAmount /* header */ + } + setScrollToTop={ + this.setScrollToTop /* header & history */ + } + scrollToTop={scrollToTop /* history */} + setScrollToBottom={ + this.setScrollToBottom /* header & messages */ + } + /> + )} + + {!readOnly && + selectLightWalletServer !== + SelectServerEnum.offline && + (mode === ModeEnum.advanced || + (!!totalBalance && + totalBalance.confirmedOrchardBalance + + totalBalance.confirmedSaplingBalance > + 0) || + (!!totalBalance && + ((totalBalance.totalOrchardBalance > 0 && + totalBalance.confirmedOrchardBalance === 0) || + (totalBalance.totalSaplingBalance > 0 && + totalBalance.confirmedSaplingBalance === + 0)) && + somePending)) && ( + + {propsTab => ( + + props.navigation.toggleDrawer() /* header */ + } + setShieldingAmount={ + this.setShieldingAmount /* header */ + } + setScrollToTop={ + this.setScrollToTop /* header & send */ + } + setScrollToBottom={ + this.setScrollToBottom /* header & send */ + } + sendTransaction={ + this.sendTransaction /* send */ + } + setServerOption={ + this.setServerOption /* send */ + } + clearToAddr={this.clearToAddr /* send */} + setSecurityOption={ + this.setSecurityOption /* send */ + } + /> + )} + + )} + + {propsTab => ( + + props.navigation.toggleDrawer() /* header */ + } + alone={false /* receive */} + setSecurityOption={this.setSecurityOption} + /> + )} + + + ) : ( + <> + {addresses === null ? ( + + ) : ( + + + {propsTab => ( + + props.navigation.toggleDrawer() /* header */ + } + alone={true /* receive */} + setSecurityOption={this.setSecurityOption} + /> + )} + + )} - - {!readOnly && - selectLightWalletServer !== SelectServerEnum.offline && - (mode === ModeEnum.advanced || - (!!totalBalance && totalBalance.confirmedOrchardBalance + totalBalance.confirmedSaplingBalance > 0) || - (!!totalBalance && - ( - (totalBalance.totalOrchardBalance > 0 && totalBalance.confirmedOrchardBalance === 0) || - (totalBalance.totalSaplingBalance > 0 && totalBalance.confirmedSaplingBalance === 0) - ) && - somePending)) && ( - - {propsTab => ( - props.navigation.toggleDrawer() /* header */} - setShieldingAmount={this.setShieldingAmount /* header */} - setScrollToTop={this.setScrollToTop /* header & send */} - setScrollToBottom={this.setScrollToBottom /* header & send */} - sendTransaction={this.sendTransaction /* send */} - setServerOption={this.setServerOption /* send */} - clearToAddr={this.clearToAddr /* send */} - setSecurityOption={this.setSecurityOption /* send */} - /> - )} - - )} - - {propsTab => ( - props.navigation.toggleDrawer() /* header */} - alone={false /* receive */} - setSecurityOption={this.setSecurityOption} - /> - )} - - - ) : ( - <> - {addresses === null ? ( - - ) : ( - - - {propsTab => ( - props.navigation.toggleDrawer() /* header */} - alone={true /* receive */} - setSecurityOption={this.setSecurityOption} - /> - )} - - - )} - - )} - - );}} + + )} + + ); + }} - {props => - ( + props.navigation.toggleDrawer() /* header */} + toggleMenuDrawer={ + () => props.navigation.toggleDrawer() /* header */ + } /> - } + )} @@ -1911,41 +2285,66 @@ export class LoadedAppClass extends Component {() => { return ( - - - + + + ); }} {props => { - const action = !!props.route.params && props.route.params.action !== undefined ? props.route.params.action : UfvkActionEnum.view; - if (action === UfvkActionEnum.view ) { + const action = + !!props.route.params && + props.route.params.action !== undefined + ? props.route.params.action + : UfvkActionEnum.view; + if (action === UfvkActionEnum.view) { return ( - {}} onClickCancel={() => {}} /> ); } else if (action === UfvkActionEnum.change) { return ( - await this.onClickOKChangeWallet({ startingApp: false })} + + await this.onClickOKChangeWallet({ + startingApp: false, + }) + } onClickCancel={() => {}} /> ); } else if (action === UfvkActionEnum.backup) { return ( - await this.onClickOKRestoreBackup()} + + await this.onClickOKRestoreBackup() + } onClickCancel={() => {}} /> ); } else if (action === UfvkActionEnum.server) { return ( - await this.onClickOKServerWallet()} + + await this.onClickOKServerWallet() + } onClickCancel={async () => { // restart all the tasks again, nothing happen. await this.rpc.clearTimers(); @@ -1958,10 +2357,15 @@ export class LoadedAppClass extends Component {props => { - const action = !!props.route.params && props.route.params.action !== undefined ? props.route.params.action : SeedActionEnum.view; - if (action === SeedActionEnum.view ) { + const action = + !!props.route.params && + props.route.params.action !== undefined + ? props.route.params.action + : SeedActionEnum.view; + if (action === SeedActionEnum.view) { return ( - {}} onClickCancel={() => {}} keepAwake={this.keepAwake} @@ -1970,22 +2374,33 @@ export class LoadedAppClass extends Component await this.onClickOKChangeWallet({ startingApp: false })} + + await this.onClickOKChangeWallet({ + startingApp: false, + }) + } onClickCancel={() => {}} /> ); } else if (action === SeedActionEnum.backup) { return ( - await this.onClickOKRestoreBackup()} + + await this.onClickOKRestoreBackup() + } onClickCancel={() => {}} /> ); } else if (action === SeedActionEnum.server) { return ( - await this.onClickOKServerWallet()} + + await this.onClickOKServerWallet() + } onClickCancel={async () => { // restart all the tasks again, nothing happen. await this.rpc.clearTimers(); @@ -1996,32 +2411,62 @@ export class LoadedAppClass extends Component - + {() => { return ( - - - + + + ); }} - - + + {() => { return ( - - - + + + ); }} - + diff --git a/app/LoadingApp/LoadingApp.tsx b/app/LoadingApp/LoadingApp.tsx index 6553dd47d..6a6457cb1 100644 --- a/app/LoadingApp/LoadingApp.tsx +++ b/app/LoadingApp/LoadingApp.tsx @@ -14,9 +14,12 @@ import { useTheme } from '@react-navigation/native'; import { I18n } from 'i18n-js'; import * as RNLocalize from 'react-native-localize'; import { StackScreenProps } from '@react-navigation/stack'; -import NetInfo, { NetInfoSubscription, NetInfoState } from '@react-native-community/netinfo/src/index'; +import NetInfo, { + NetInfoSubscription, + NetInfoState, +} from '@react-native-community/netinfo/src/index'; -import RPCModule from '../RPCModule'; +import RPCModule, { WalletKind } from '../RPCModule'; import { AppStateLoading, BackgroundType, @@ -88,7 +91,10 @@ const tr = require('../translations/tr.json'); //const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); type LoadingAppProps = { - navigation: StackScreenProps['navigation']; + navigation: StackScreenProps< + AppStackParamList, + RouteEnum.LoadingApp + >['navigation']; route: StackScreenProps['route']; toggleTheme: (mode: ModeEnum) => void; }; @@ -103,17 +109,29 @@ export default function LoadingApp(props: LoadingAppProps) { const [loading, setLoading] = useState(true); const [language, setLanguage] = useState(LanguageEnum.en); - const [currency, setCurrency] = useState(CurrencyEnum.noCurrency); // by default none because of cTAZ - const [lightWalletServer, setLightWalletServer] = useState(SERVER_DEFAULT_0); - const [selectLightWalletServer, setSelectLightWalletServer] = useState(SelectServerEnum.custom); - const [validatorServer, setValidatorServer] = useState(SERVER_DEFAULT_0); - const [selectValidatorServer, setSelectValidatorServer] = useState(SelectServerEnum.custom); + const [currency, setCurrency] = useState( + CurrencyEnum.noCurrency, + ); // by default none because of cTAZ + const [lightWalletServer, setLightWalletServer] = + useState(SERVER_DEFAULT_0); + const [selectLightWalletServer, setSelectLightWalletServer] = + useState(SelectServerEnum.custom); + const [validatorServer, setValidatorServer] = + useState(SERVER_DEFAULT_0); + const [selectValidatorServer, setSelectValidatorServer] = + useState(SelectServerEnum.custom); const [sendAll, setSendAll] = useState(false); const [donation, setDonation] = useState(false); const [privacy, setPrivacy] = useState(false); const [mode, setMode] = useState(ModeEnum.advanced); // by default advanced - const [background, setBackground] = useState({ batches: 0, message: '', date: 0, dateEnd: 0 }); - const [firstLaunchingMessage, setFirstLaunchingMessage] = useState(LaunchingModeEnum.opening); + const [background, setBackground] = useState({ + batches: 0, + message: '', + date: 0, + dateEnd: 0, + }); + const [firstLaunchingMessage, setFirstLaunchingMessage] = + useState(LaunchingModeEnum.opening); const [security, setSecurity] = useState({ startApp: true, // activate only this foregroundApp: false, @@ -126,8 +144,10 @@ export default function LoadingApp(props: LoadingAppProps) { }); const [rescanMenu, setRescanMenu] = useState(false); // by default the App store the seed phrase & birthday on KeyChain/KeyStore (Device). - const [recoveryWalletInfoOnDevice, setRecoveryWalletInfoOnDevice] = useState(true); - const [performanceLevel, setPerformanceLevel] = useState(RPCPerformanceLevelEnum.Medium); + const [recoveryWalletInfoOnDevice, setRecoveryWalletInfoOnDevice] = + useState(true); + const [performanceLevel, setPerformanceLevel] = + useState(RPCPerformanceLevelEnum.Medium); const file = useMemo( () => ({ en: en, @@ -140,14 +160,16 @@ export default function LoadingApp(props: LoadingAppProps) { ); const i18n = useMemo(() => new I18n(file), [file]); - const translate: (key: string) => TranslateType = (key: string) => i18n.t(key); + const translate: (key: string) => TranslateType = (key: string) => + i18n.t(key); useEffect(() => { (async () => { // fallback if no available language fits const fallback = { languageTag: LanguageEnum.en, isRTL: false }; - const { languageTag, isRTL } = RNLocalize.findBestLanguageTag(Object.keys(file)) || fallback; + const { languageTag, isRTL } = + RNLocalize.findBestLanguageTag(Object.keys(file)) || fallback; // update layout direction I18nManager.forceRTL(isRTL); @@ -161,12 +183,18 @@ export default function LoadingApp(props: LoadingAppProps) { if (settings.version === null) { // this is a fresh install setFirstLaunchingMessage(LaunchingModeEnum.installing); - } else if (settings.version === '' || settings.version !== (translate('version') as string)) { + } else if ( + settings.version === '' || + settings.version !== (translate('version') as string) + ) { // this is an update setFirstLaunchingMessage(LaunchingModeEnum.updating); } - if (settings.mode === ModeEnum.basic || settings.mode === ModeEnum.advanced) { + if ( + settings.mode === ModeEnum.basic || + settings.mode === ModeEnum.advanced + ) { setMode(settings.mode); props.toggleTheme(settings.mode); } else { @@ -205,13 +233,19 @@ export default function LoadingApp(props: LoadingAppProps) { ) { setCurrency(settings.currency); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.currency, currency); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.currency, + currency, + ); } // lightwallet server if (settings.lightWalletserver) { setLightWalletServer(settings.lightWalletserver); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.lightWalletServer, lightWalletServer); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.lightWalletServer, + lightWalletServer, + ); } // using only custom & offline. if ( @@ -222,13 +256,19 @@ export default function LoadingApp(props: LoadingAppProps) { ) { setSelectLightWalletServer(settings.selectLightWalletServer); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.selectLightWalletServer, selectLightWalletServer); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.selectLightWalletServer, + selectLightWalletServer, + ); } // validator server if (settings.validatorServer) { setValidatorServer(settings.validatorServer); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.validatorServer, validatorServer); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.validatorServer, + validatorServer, + ); } if ( settings.selectValidatorServer === SelectServerEnum.auto || @@ -238,7 +278,10 @@ export default function LoadingApp(props: LoadingAppProps) { ) { setSelectValidatorServer(settings.selectValidatorServer); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.selectValidatorServer, selectValidatorServer); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.selectValidatorServer, + selectValidatorServer, + ); } if (settings.sendAll === true || settings.sendAll === false) { setSendAll(settings.sendAll); @@ -248,7 +291,10 @@ export default function LoadingApp(props: LoadingAppProps) { if (settings.donation === true || settings.donation === false) { setDonation(settings.donation); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.donation, donation); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.donation, + donation, + ); } if (settings.privacy === true || settings.privacy === false) { setPrivacy(settings.privacy); @@ -258,17 +304,29 @@ export default function LoadingApp(props: LoadingAppProps) { if (settings.security) { setSecurity(settings.security); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.security, security); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.security, + security, + ); } if (settings.rescanMenu === true || settings.rescanMenu === false) { setRescanMenu(settings.rescanMenu); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.rescanMenu, rescanMenu); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.rescanMenu, + rescanMenu, + ); } - if (settings.recoveryWalletInfoOnDevice === true || settings.recoveryWalletInfoOnDevice === false) { + if ( + settings.recoveryWalletInfoOnDevice === true || + settings.recoveryWalletInfoOnDevice === false + ) { setRecoveryWalletInfoOnDevice(settings.recoveryWalletInfoOnDevice); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.recoveryWalletInfoOnDevice, recoveryWalletInfoOnDevice); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.recoveryWalletInfoOnDevice, + recoveryWalletInfoOnDevice, + ); } if ( settings.performanceLevel === RPCPerformanceLevelEnum.High || @@ -278,7 +336,10 @@ export default function LoadingApp(props: LoadingAppProps) { ) { setPerformanceLevel(settings.performanceLevel); } else { - await SettingsFileImpl.writeSettings(SettingsNameEnum.performanceLevel, performanceLevel); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.performanceLevel, + performanceLevel, + ); } // for testing @@ -296,7 +357,13 @@ export default function LoadingApp(props: LoadingAppProps) { //console.log('render loadingApp - 2', translate('version')); if (loading) { - return ; + return ( + + ); } else { return ( ['navigation']; + navigationApp: StackScreenProps< + AppStackParamList, + RouteEnum.LoadingApp + >['navigation']; route: StackScreenProps['route']; toggleTheme: (mode: ModeEnum) => void; translate: (key: string) => TranslateType; @@ -351,7 +421,10 @@ type LoadingAppClassProps = { type LoadingAppClassState = AppStateLoading & AppContextLoading; -export class LoadingAppClass extends Component { +export class LoadingAppClass extends Component< + LoadingAppClassProps, + LoadingAppClassState +> { dim: EmitterSubscription; appstate: NativeEventSubscription; unsubscribeNetInfo: NetInfoSubscription; @@ -397,7 +470,10 @@ export class LoadingAppClass extends Component { this.addLastSnackbar({ - message: this.state.translate('loadedapp.selectingserver') as string, + message: this.state.translate( + 'loadedapp.selectingserver', + ) as string, duration: SnackbarDurationEnum.longer, screenName: [this.screenName], }); @@ -493,7 +577,10 @@ export class LoadingAppClass extends Component create a new wallet & go directly to history screen. // no seed screen. - if (!netInfoState.isConnected || this.state.selectLightWalletServer === SelectServerEnum.offline) { + if ( + !netInfoState.isConnected || + this.state.selectLightWalletServer === SelectServerEnum.offline + ) { this.setState({ screen: 1, walletExists: false, @@ -636,13 +741,22 @@ export class LoadingAppClass extends Component go to the initial menu. - await SettingsFileImpl.writeSettings(SettingsNameEnum.basicFirstViewSeed, true); + await SettingsFileImpl.writeSettings( + SettingsNameEnum.basicFirstViewSeed, + true, + ); this.setState(state => ({ screen: state.screen === 3 ? 3 : 1, walletExists: false, @@ -651,84 +765,107 @@ export class LoadingAppClass extends Component { - //console.log('LOADING', 'prior', this.state.appStateStatus, 'next', nextAppState); - // let's catch the prior value - const priorAppState = this.state.appStateStatus; - this.setState({ appStateStatus: nextAppState }); - if ( - (priorAppState === AppStateStatusEnum.inactive || priorAppState === AppStateStatusEnum.background) && - nextAppState === AppStateStatusEnum.active - ) { - //console.log('App LOADING has come to the foreground!'); - // reading background task info - this.fetchBackgroundSyncing(); - // setting value for background task Android - await AsyncStorage.setItem(GlobalConst.background, GlobalConst.no); - //console.log('&&&&& background no in storage &&&&&'); - if (this.state.backgroundError && (this.state.backgroundError.title || this.state.backgroundError.error)) { - Alert.alert(this.state.backgroundError.title, this.state.backgroundError.error); - this.setBackgroundError('', ''); + this.appstate = AppState.addEventListener( + EventListenerEnum.change, + async nextAppState => { + //console.log('LOADING', 'prior', this.state.appStateStatus, 'next', nextAppState); + // let's catch the prior value + const priorAppState = this.state.appStateStatus; + this.setState({ appStateStatus: nextAppState }); + if ( + (priorAppState === AppStateStatusEnum.inactive || + priorAppState === AppStateStatusEnum.background) && + nextAppState === AppStateStatusEnum.active + ) { + //console.log('App LOADING has come to the foreground!'); + // reading background task info + this.fetchBackgroundSyncing(); + // setting value for background task Android + await AsyncStorage.setItem(GlobalConst.background, GlobalConst.no); + //console.log('&&&&& background no in storage &&&&&'); + if ( + this.state.backgroundError && + (this.state.backgroundError.title || + this.state.backgroundError.error) + ) { + Alert.alert( + this.state.backgroundError.title, + this.state.backgroundError.error, + ); + this.setBackgroundError('', ''); + } } - } - if ( - (nextAppState === AppStateStatusEnum.inactive || nextAppState === AppStateStatusEnum.background) && - priorAppState === AppStateStatusEnum.active - ) { - console.log('App LOADING is gone to the background!'); - // setting value for background task Android - await AsyncStorage.setItem(GlobalConst.background, GlobalConst.yes); - //console.log('&&&&& background yes in storage &&&&&'); - } - }); + if ( + (nextAppState === AppStateStatusEnum.inactive || + nextAppState === AppStateStatusEnum.background) && + priorAppState === AppStateStatusEnum.active + ) { + console.log('App LOADING is gone to the background!'); + // setting value for background task Android + await AsyncStorage.setItem(GlobalConst.background, GlobalConst.yes); + //console.log('&&&&& background yes in storage &&&&&'); + } + }, + ); - this.unsubscribeNetInfo = NetInfo.addEventListener((state: NetInfoState) => { - const { screen } = this.state; - const { isConnected, type, isConnectionExpensive } = this.state.netInfo; - if ( - isConnected !== state.isConnected || - type !== state.type || - isConnectionExpensive !== state.details?.isConnectionExpensive - ) { - this.setState({ - netInfo: { - isConnected: state.isConnected, - type: state.type, - isConnectionExpensive: state.details && state.details.isConnectionExpensive, - }, - screen: screen === 3 ? 3 : screen !== 0 ? 1 : 0, - //actionButtonsDisabled: true, - }); - if (isConnected !== state.isConnected) { - if (!state.isConnected) { - //console.log('EVENT Loading: No internet connection.'); - this.setState({ - customServerShow: false, - }); - } else { - //console.log('EVENT Loading: YESSSSS internet connection.'); - // if it is offline & there is no wallet file - // the screen is going to be empty - // show the custom server component - if (this.state.selectLightWalletServer === SelectServerEnum.offline && !this.state.walletExists) { - this.setState({ - customServerShow: true, - }); - } - if (screen !== 0) { + this.unsubscribeNetInfo = NetInfo.addEventListener( + (state: NetInfoState) => { + const { screen } = this.state; + const { isConnected, type, isConnectionExpensive } = this.state.netInfo; + if ( + isConnected !== state.isConnected || + type !== state.type || + isConnectionExpensive !== state.details?.isConnectionExpensive + ) { + this.setState({ + netInfo: { + isConnected: state.isConnected, + type: state.type, + isConnectionExpensive: + state.details && state.details.isConnectionExpensive, + }, + screen: screen === 3 ? 3 : screen !== 0 ? 1 : 0, + //actionButtonsDisabled: true, + }); + if (isConnected !== state.isConnected) { + if (!state.isConnected) { + //console.log('EVENT Loading: No internet connection.'); this.setState({ - screen: screen === 3 ? 3 : screen !== 0 ? 1 : 0, + customServerShow: false, }); + } else { + //console.log('EVENT Loading: YESSSSS internet connection.'); + // if it is offline & there is no wallet file + // the screen is going to be empty + // show the custom server component + if ( + this.state.selectLightWalletServer === + SelectServerEnum.offline && + !this.state.walletExists + ) { + this.setState({ + customServerShow: true, + }); + } + if (screen !== 0) { + this.setState({ + screen: screen === 3 ? 3 : screen !== 0 ? 1 : 0, + }); + } } } } - } - }); + }, + ); // if it is offline & there is no wallet file // the screen is going to be empty // show the custom server component - if (netInfoState.isConnected && this.state.selectLightWalletServer === SelectServerEnum.offline && !this.state.walletExists) { + if ( + netInfoState.isConnected && + this.state.selectLightWalletServer === SelectServerEnum.offline && + !this.state.walletExists + ) { this.setState({ customServerShow: true, }); @@ -737,8 +874,12 @@ export class LoadingAppClass extends Component { this.dim && typeof this.dim.remove === 'function' && this.dim.remove(); - this.appstate && typeof this.appstate.remove === 'function' && this.appstate.remove(); - this.unsubscribeNetInfo && typeof this.unsubscribeNetInfo === 'function' && this.unsubscribeNetInfo(); + this.appstate && + typeof this.appstate.remove === 'function' && + this.appstate.remove(); + this.unsubscribeNetInfo && + typeof this.unsubscribeNetInfo === 'function' && + this.unsubscribeNetInfo(); }; selectTheBestServer = async (aDifferentOne: boolean): Promise => { @@ -747,7 +888,8 @@ export class LoadingAppClass extends Component !s.obsolete && s.uri !== (aDifferentOne ? actualServer.uri : ''), + (s: ServerUrisType) => + !s.obsolete && s.uri !== (aDifferentOne ? actualServer.uri : ''), ), ); let fasterServer: ServerType = {} as ServerType; @@ -766,19 +908,30 @@ export class LoadingAppClass extends Component Promise = async (server: ServerType) => { + checkServer: (s: ServerType) => Promise = async ( + server: ServerType, + ) => { const s = { uri: server.uri, chainName: server.chainName, @@ -804,10 +959,19 @@ export class LoadingAppClass extends Component { + walletErrorHandle = async ( + result: string, + title: string, + screen: number, + start: boolean, + ) => { // first check the actual server // if the server is not working properly sometimes can take more than one minute to fail. - if (start && this.state.netInfo.isConnected && this.state.selectLightWalletServer !== SelectServerEnum.offline) { + if ( + start && + this.state.netInfo.isConnected && + this.state.selectLightWalletServer !== SelectServerEnum.offline + ) { this.addLastSnackbar({ message: this.state.translate('restarting') as string, duration: SnackbarDurationEnum.long, @@ -816,7 +980,10 @@ export class LoadingAppClass extends Component show the error. // if Offline mode -> show the error. - if (!this.state.netInfo.isConnected || this.state.selectLightWalletServer === SelectServerEnum.offline) { + if ( + !this.state.netInfo.isConnected || + this.state.selectLightWalletServer === SelectServerEnum.offline + ) { createAlert( this.setBackgroundError, this.addLastSnackbar, @@ -828,9 +995,15 @@ export class LoadingAppClass extends Component this error is something not related with the server availability createAlert( @@ -844,14 +1017,20 @@ export class LoadingAppClass extends Component { - const backgroundJson: BackgroundType = await BackgroundFileImpl.readBackground(); + const backgroundJson: BackgroundType = + await BackgroundFileImpl.readBackground(); this.setState({ background: backgroundJson }); }; @@ -946,25 +1140,40 @@ export class LoadingAppClass extends Component { + navigateToLoadedApp = ( + readOnly: boolean, + orchardPool: boolean, + saplingPool: boolean, + transparentPool: boolean, + firstLaunchingMessage: LaunchingModeEnum, + ) => { this.props.navigationApp.reset({ index: 0, routes: [ { name: RouteEnum.LoadedApp, - params: { readOnly, orchardPool, saplingPool, transparentPool, firstLaunchingMessage }, + params: { + readOnly, + orchardPool, + saplingPool, + transparentPool, + firstLaunchingMessage, + }, }, ], }); }; createNewWallet = async (goSeedScreen: boolean = true): Promise => { - if (!this.state.netInfo.isConnected || this.state.selectLightWalletServer === SelectServerEnum.offline) { - this.addLastSnackbar({ message: this.state.translate('loadedapp.connection-error') as string, screenName: [this.screenName] }); + if ( + !this.state.netInfo.isConnected || + this.state.selectLightWalletServer === SelectServerEnum.offline + ) { + this.addLastSnackbar({ + message: this.state.translate('loadedapp.connection-error') as string, + screenName: [this.screenName], + }); return; } this.setState({ actionButtonsDisabled: true }); @@ -1055,7 +1291,10 @@ export class LoadingAppClass extends Component { const newSnackbars = this.state.snackbars; // if the last one is the same don't do anything. - if (newSnackbars.length > 0 && newSnackbars[newSnackbars.length - 1].message === snackbar.message) { + if ( + newSnackbars.length > 0 && + newSnackbars[newSnackbars.length - 1].message === snackbar.message + ) { return; } newSnackbars.push(snackbar); @@ -1324,7 +1603,10 @@ export class LoadingAppClass extends Component { Alert.alert( this.props.translate('loadedapp.walletseed-basic') as string, - (security ? '' : ((this.props.translate('loadingapp.recoverkeysinstall') + '\n\n') as string)) + txt, + (security + ? '' + : ((this.props.translate('loadingapp.recoverkeysinstall') + + '\n\n') as string)) + txt, [ { text: this.props.translate('copy') as string, @@ -1337,7 +1619,10 @@ export class LoadingAppClass extends Component zingolib version - ', Date.now() - start); + console.log( + '=========================================== > zingolib version - ', + Date.now() - start, + ); } if (zingolibStr) { if (zingolibStr.toLowerCase().startsWith(GlobalConst.error)) { @@ -1385,7 +1673,6 @@ export class LoadingAppClass extends Component { - this.setState({ biometricsFailed: false }, () => this.componentDidMount()); + this.setState({ biometricsFailed: false }, () => + this.componentDidMount(), + ); }} /> )} @@ -1491,9 +1780,26 @@ export class LoadingAppClass extends Component this.navigateToLoadedApp(readOnly, orchardPool, saplingPool, transparentPool, firstLaunchingMessage)}> + onRequestClose={() => + this.navigateToLoadedApp( + readOnly, + orchardPool, + saplingPool, + transparentPool, + firstLaunchingMessage, + ) + } + > this.navigateToLoadedApp(readOnly, orchardPool, saplingPool, transparentPool, firstLaunchingMessage)} + onClickOK={() => + this.navigateToLoadedApp( + readOnly, + orchardPool, + saplingPool, + transparentPool, + firstLaunchingMessage, + ) + } /> )} @@ -1502,7 +1808,8 @@ export class LoadingAppClass extends Component this.setState({ screen: 1 })}> + onRequestClose={() => this.setState({ screen: 1 })} + > this.doRestore(s, b)} onClickCancel={() => this.setState({ screen: 1 })} diff --git a/app/LoadingApp/components/ImportUfvk.tsx b/app/LoadingApp/components/ImportUfvk.tsx index 02815675c..69b2c372f 100644 --- a/app/LoadingApp/components/ImportUfvk.tsx +++ b/app/LoadingApp/components/ImportUfvk.tsx @@ -22,7 +22,12 @@ import { ThemeType } from '../../types'; import { ContextAppLoading } from '../../context'; import Header from '../../../components/Header'; import RPCModule from '../../RPCModule'; -import { ButtonTypeEnum, GlobalConst, ScreenEnum, SelectServerEnum } from '../../AppState'; +import { + ButtonTypeEnum, + GlobalConst, + ScreenEnum, + SelectServerEnum, +} from '../../AppState'; import Snackbars from '../../../components/Components/Snackbars'; import { ToastProvider } from 'react-native-toastier'; @@ -30,10 +35,22 @@ type ImportUfvkProps = { onClickCancel: () => void; onClickOK: (keyText: string, birthday: number) => void; }; -const ImportUfvk: React.FunctionComponent = ({ onClickCancel, onClickOK }) => { +const ImportUfvk: React.FunctionComponent = ({ + onClickCancel, + onClickOK, +}) => { const context = useContext(ContextAppLoading); - const { translate, netInfo, lightWalletserver, mode, addLastSnackbar, selectLightWalletServer, snackbars, removeFirstSnackbar } = context; - const { colors } = useTheme() as ThemeType; + const { + translate, + netInfo, + lightWalletserver, + mode, + addLastSnackbar, + selectLightWalletServer, + snackbars, + removeFirstSnackbar, + } = context; + const { colors } = useTheme() as ThemeType; const screenName = ScreenEnum.ImportUfvk; const [seedufvkText, setSeedufvkText] = useState(''); @@ -42,17 +59,36 @@ const ImportUfvk: React.FunctionComponent = ({ onClickCancel, o const [latestBlock, setLatestBlock] = useState(0); useEffect(() => { - if (!netInfo.isConnected || selectLightWalletServer !== SelectServerEnum.offline) { - (async () => { - const resp: string = await RPCModule.getLatestBlockServerInfo(lightWalletserver.uri); - //console.log(resp); - if (resp && !resp.toLowerCase().startsWith(GlobalConst.error)) { - setLatestBlock(Number(resp)); - } else { - //console.log('error latest block', resp); - } - })(); + if ( + !netInfo.isConnected || + selectLightWalletServer === SelectServerEnum.offline + ) { + return; } + + let cancelled = false; + + const fetchLatestBlock = async () => { + try { + const height = await RPCModule.getLatestBlockServerInfo( + lightWalletserver.uri, + ); + + if (!cancelled) { + setLatestBlock(height); + } + } catch { + if (!cancelled) { + // TODO: What should we do here? + } + } + }; + + fetchLatestBlock(); + + return () => { + cancelled = true; + }; }, [lightWalletserver, selectLightWalletServer, netInfo.isConnected]); useEffect(() => { @@ -62,13 +98,20 @@ const ImportUfvk: React.FunctionComponent = ({ onClickCancel, o seedufvkText.toLowerCase().startsWith(GlobalConst.utestview) ) { // if it is a ufvk - const seedufvkTextArray: string[] = seedufvkText.replaceAll('\n', ' ').trim().replaceAll(' ', ' ').split(' '); + const seedufvkTextArray: string[] = seedufvkText + .replaceAll('\n', ' ') + .trim() + .replaceAll(' ', ' ') + .split(' '); //console.log(seedufvkTextArray); // if the ufvk have 2 -> means it is a copy/paste from the stored ufvk in the device. if (seedufvkTextArray.length === 2) { // if the last word is a number -> move it to the birthday field - const lastWord: string = seedufvkTextArray[seedufvkTextArray.length - 1]; - const possibleBirthday: number | null = isNaN(Number(lastWord)) ? null : Number(lastWord); + const lastWord: string = + seedufvkTextArray[seedufvkTextArray.length - 1]; + const possibleBirthday: number | null = isNaN(Number(lastWord)) + ? null + : Number(lastWord); if (possibleBirthday && !birthday) { setBirthday(possibleBirthday.toString()); setSeedufvkText(seedufvkTextArray.slice(0, 1).join(' ')); @@ -76,13 +119,20 @@ const ImportUfvk: React.FunctionComponent = ({ onClickCancel, o } } else { // if it is a seed - const seedufvkTextArray: string[] = seedufvkText.replaceAll('\n', ' ').trim().replaceAll(' ', ' ').split(' '); + const seedufvkTextArray: string[] = seedufvkText + .replaceAll('\n', ' ') + .trim() + .replaceAll(' ', ' ') + .split(' '); //console.log(seedufvkTextArray); // if the seed have 25 -> means it is a copy/paste from the stored seed in the device. if (seedufvkTextArray.length === 25) { // if the last word is a number -> move it to the birthday field - const lastWord: string = seedufvkTextArray[seedufvkTextArray.length - 1]; - const possibleBirthday: number | null = isNaN(Number(lastWord)) ? null : Number(lastWord); + const lastWord: string = + seedufvkTextArray[seedufvkTextArray.length - 1]; + const possibleBirthday: number | null = isNaN(Number(lastWord)) + ? null + : Number(lastWord); if (possibleBirthday && !birthday) { setBirthday(possibleBirthday.toString()); setSeedufvkText(seedufvkTextArray.slice(0, 24).join(' ')); @@ -95,8 +145,14 @@ const ImportUfvk: React.FunctionComponent = ({ onClickCancel, o }, [seedufvkText]); const okButton = async () => { - if (!netInfo.isConnected || selectLightWalletServer === SelectServerEnum.offline) { - addLastSnackbar({ message: translate('loadedapp.connection-error') as string, screenName: [screenName] }); + if ( + !netInfo.isConnected || + selectLightWalletServer === SelectServerEnum.offline + ) { + addLastSnackbar({ + message: translate('loadedapp.connection-error') as string, + screenName: [screenName], + }); return; } onClickOK(seedufvkText.trimEnd().trimStart(), Number(birthday)); @@ -109,7 +165,7 @@ const ImportUfvk: React.FunctionComponent = ({ onClickCancel, o // setSeedufvkText(a); // }); //} else { - setQrcodeModalVisible(true); + setQrcodeModalVisible(true); //} }; @@ -122,8 +178,12 @@ const ImportUfvk: React.FunctionComponent = ({ onClickCancel, o /> = ({ onClickCancel, o style={{ flex: 1, backgroundColor: colors.background, - }}> + }} + > setQrcodeModalVisible(false)}> - setQrcodeModalVisible(false)} /> + onRequestClose={() => setQrcodeModalVisible(false)} + > + setQrcodeModalVisible(false)} + />
= ({ onClickCancel, o flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-start', - }}> - + }} + > + {translate('import.key-label') as string} = ({ onClickCancel, o maxHeight: '40%', flexDirection: 'row', justifyContent: 'space-between', - }}> + }} + > = ({ onClickCancel, o width: 'auto', flex: 1, justifyContent: 'center', - }}> + }} + > = ({ onClickCancel, o { setSeedufvkText(''); - }}> - + }} + > + )} { showQrcodeModalVisible(); - }}> - + }} + > + @@ -224,7 +305,10 @@ const ImportUfvk: React.FunctionComponent = ({ onClickCancel, o {translate('import.birthday') as string} {selectLightWalletServer !== SelectServerEnum.offline && ( - {translate('seed.birthday-no-readonly') + ' (1, ' + (latestBlock ? latestBlock.toString() : '--') + ')'} + {translate('seed.birthday-no-readonly') + + ' (1, ' + + (latestBlock ? latestBlock.toString() : '--') + + ')'} )} = ({ onClickCancel, o maxHeight: 48, minWidth: '20%', minHeight: 48, - }}> + }} + > = ({ onClickCancel, o setBirthday(''); } else if ( Number(text) <= 0 || - (Number(text) > latestBlock && selectLightWalletServer !== SelectServerEnum.offline) + (Number(text) > latestBlock && + selectLightWalletServer !== SelectServerEnum.offline) ) { setBirthday(''); } else { - setBirthday(Number(text.replace('.', '').replace(',', '')).toFixed(0)); + setBirthday( + Number(text.replace('.', '').replace(',', '')).toFixed( + 0, + ), + ); } }} - editable={latestBlock ? true : selectLightWalletServer !== SelectServerEnum.offline ? false : true} + editable={ + latestBlock + ? true + : selectLightWalletServer !== SelectServerEnum.offline + ? false + : true + } keyboardType="numeric" /> - {translate('import.text') as string} + + {translate('import.text') as string} + = ({ onClickCancel, o justifyContent: 'center', alignItems: 'center', marginVertical: 5, - }}> + }} + >