Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/printversion.ps1
Original file line number Diff line number Diff line change
@@ -1 +1 @@
echo "VERSION=2.0.16-153" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "VERSION=2.0.17-154" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
2 changes: 1 addition & 1 deletion bin/printversion.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/bash
VERSION="2.0.16-153"
VERSION="2.0.17-154"
echo "VERSION=$VERSION" >> $GITHUB_ENV
1 change: 1 addition & 0 deletions flatpak/co.zingo.pc.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
</content_rating>

<releases>
<release version="2.0.17" date="2026-05-21"/>
<release version="2.0.16" date="2026-05-19"/>
<release version="2.0.15" date="2026-05-18">
<description>
Expand Down
16 changes: 8 additions & 8 deletions native/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion native/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,16 @@ windows = { version = "0.61", features = ["Security_Credentials_UI"] }
cc = "1"

[profile.release]
debug = 1
debug = 1
# panic strategy: intentionally left at the default ("unwind").
#
# Switching to `panic = "abort"` would shrink the MAS binary slightly and is
# tempting from a security-hardening angle, but it is incompatible with the
# `with_panic_guard` infrastructure used throughout this crate. That guard
# wraps every FFI entry point in `panic::catch_unwind`, converting transient
# panics (zingolib internal bugs, lock poisoning, etc.) into a recoverable
# `ZingolibError::Panic` returned to JS. With `abort`, those panics would
# instead terminate the wallet process. The unwind/resilience trade-off was
# made deliberately at the architecture level — do not flip this flag without
# first removing `with_panic_guard` everywhere and propagating errors via
# `Result` only.
145 changes: 35 additions & 110 deletions native/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ use zingolib::config::{ChainType, ZingoConfig, construct_lightwalletd_uri};
use zingolib::data::PollReport;
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::keys::unified::{ReceiverSelection, UnifiedKeyStore};
use zingolib::wallet::{LightWallet, WalletBase, WalletSettings};
use zingolib::data::receivers::Receivers;
use zcash_address::ZcashAddress;
Expand Down Expand Up @@ -101,14 +98,12 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("get_spendable_balance_with_address", get_spendable_balance_with_address)?;
cx.export_function("get_spendable_balance_total", get_spendable_balance_total)?;
cx.export_function("set_option_wallet", set_option_wallet)?;
cx.export_function("get_option_wallet", get_option_wallet)?;
cx.export_function("create_tor_client", create_tor_client)?;
cx.export_function("remove_tor_client", remove_tor_client)?;
cx.export_function("get_unified_addresses", get_unified_addresses)?;
cx.export_function("get_transparent_addresses", get_transparent_addresses)?;
cx.export_function("create_new_unified_address", create_new_unified_address)?;
cx.export_function("create_new_transparent_address", create_new_transparent_address)?;
cx.export_function("check_my_addressv", check_my_address)?;
cx.export_function("get_wallet_save_required", get_wallet_save_required)?;
cx.export_function("set_config_wallet_to_test", set_config_wallet_to_test)?;
cx.export_function("set_config_wallet_to_prod", set_config_wallet_to_prod)?;
Expand Down Expand Up @@ -446,7 +441,11 @@ fn construct_uri_load_config(
transparent_address_discovery: TransparentAddressDiscovery::minimal(),
performance_level: performancetype,
},
min_confirmations: NonZeroU32::try_from(min_confirmations as u32).unwrap(),
// `min_confirmations` comes from the renderer through IPC. Reject 0 (and
// anything that casts to 0 — negatives, NaN, fractional values <1) instead
// of unwrapping, which would panic and abort the wallet process.
min_confirmations: NonZeroU32::try_from(min_confirmations as u32)
.map_err(|_| "Error: min_confirmations must be >= 1".to_string())?,
},
NonZeroU32::try_from(1).expect("hard-coded integer"),
wallet_name,
Expand Down Expand Up @@ -770,6 +769,8 @@ fn check_save_error(mut cx: FunctionContext) -> JsResult<JsPromise> {
Ok(promise)
}

// FFI-exposed but currently has no JS caller. Reserved for an upcoming UI
// feature; review input validation here when the JS caller is wired up.
fn get_developer_donation_address(mut cx: FunctionContext) -> JsResult<JsString> {
let res: Result<String, ZingolibError> = with_panic_guard(|| {
let resp = zingolib::config::DEVELOPER_DONATION_ADDRESS.to_string();
Expand All @@ -783,6 +784,8 @@ fn get_developer_donation_address(mut cx: FunctionContext) -> JsResult<JsString>
}
}

// FFI-exposed but currently has no JS caller. Reserved for an upcoming UI
// feature; review input validation here when the JS caller is wired up.
fn get_zennies_for_zingo_donation_address(mut cx: FunctionContext) -> JsResult<JsString> {
let res: Result<String, ZingolibError> = with_panic_guard(|| {
let resp = zingolib::config::ZENNIES_FOR_ZINGO_DONATION_ADDRESS.to_string();
Expand Down Expand Up @@ -1042,6 +1045,8 @@ fn run_sync(mut cx: FunctionContext) -> JsResult<JsPromise> {
Ok(promise)
}

// FFI-exposed but currently has no JS caller. Reserved for an upcoming UI
// feature; review input validation here when the JS caller is wired up.
fn pause_sync(mut cx: FunctionContext) -> JsResult<JsPromise> {
let promise = cx
.task(move || -> Result<String, ZingolibError> {
Expand Down Expand Up @@ -1342,6 +1347,10 @@ fn parse_address(mut cx: FunctionContext) -> JsResult<JsPromise> {
Ok(promise)
}

// Validates a Unified Full Viewing Key for the renderer. Called from
// AddNewWallet.doRestoreUfvkWallet before init_from_ufvk to give the user a
// specific error (invalid key / wrong network) instead of the generic failure
// that would otherwise come back from init_from_ufvk after disk writes.
fn parse_ufvk(mut cx: FunctionContext) -> JsResult<JsPromise> {
let ufvk = cx.argument::<JsString>(0)?.value(&mut cx);

Expand Down Expand Up @@ -1719,21 +1728,8 @@ fn set_option_wallet(mut cx: FunctionContext) -> JsResult<JsPromise> {
Ok(promise)
}

fn get_option_wallet(mut cx: FunctionContext) -> JsResult<JsPromise> {
let promise = cx
.task(move || -> Result<String, ZingolibError> {
with_panic_guard(|| {
Ok("Error: unimplemented".to_string())
})
})
.promise(move |mut cx, result| match result {
Ok(msg) => Ok(cx.string(msg)),
Err(err) => cx.throw_error(err.to_string()),
});

Ok(promise)
}

// FFI-exposed but currently has no JS caller. Reserved for an upcoming UI
// feature; review input validation here when the JS caller is wired up.
fn create_tor_client(mut cx: FunctionContext) -> JsResult<JsPromise> {
let data_dir = cx.argument::<JsString>(0)?.value(&mut cx);

Expand Down Expand Up @@ -1764,6 +1760,8 @@ fn create_tor_client(mut cx: FunctionContext) -> JsResult<JsPromise> {
Ok(promise)
}

// FFI-exposed but currently has no JS caller. Reserved for an upcoming UI
// feature; review input validation here when the JS caller is wired up.
fn remove_tor_client(mut cx: FunctionContext) -> JsResult<JsPromise> {
let promise = cx
.task(move || -> Result<String, ZingolibError> {
Expand Down Expand Up @@ -1906,93 +1904,6 @@ fn create_new_transparent_address(mut cx: FunctionContext) -> JsResult<JsPromise
Ok(promise)
}

fn check_my_address(mut cx: FunctionContext) -> JsResult<JsPromise> {
let address = cx.argument::<JsString>(0)?.value(&mut cx);

let promise = cx
.task(move || -> Result<String, 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 {
match lightclient
.wallet
.read()
.await
.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)
}
})
})
.promise(move |mut cx, result| match result {
Ok(msg) => Ok(cx.string(msg)),
Err(err) => cx.throw_error(err.to_string()),
});

Ok(promise)
}

fn get_wallet_save_required(mut cx: FunctionContext) -> JsResult<JsPromise> {
let promise = cx
.task(move || -> Result<String, ZingolibError> {
Expand All @@ -2016,6 +1927,8 @@ fn get_wallet_save_required(mut cx: FunctionContext) -> JsResult<JsPromise> {
Ok(promise)
}

// FFI-exposed but currently has no JS caller. Reserved for an upcoming UI
// feature; review input validation here when the JS caller is wired up.
fn set_config_wallet_to_test(mut cx: FunctionContext) -> JsResult<JsPromise> {
let promise = cx
.task(move || -> Result<String, ZingolibError> {
Expand All @@ -2042,6 +1955,8 @@ fn set_config_wallet_to_test(mut cx: FunctionContext) -> JsResult<JsPromise> {
Ok(promise)
}

// FFI-exposed but currently has no JS caller. Reserved for an upcoming UI
// feature; review input validation here when the JS caller is wired up.
fn set_config_wallet_to_prod(mut cx: FunctionContext) -> JsResult<JsPromise> {
let performance_level = cx.argument::<JsString>(0)?.value(&mut cx);
let min_confirmations = cx.argument::<JsNumber>(1)?.value(&mut cx);
Expand All @@ -2059,8 +1974,16 @@ fn set_config_wallet_to_prod(mut cx: FunctionContext) -> JsResult<JsPromise> {
"Low" => PerformanceLevel::Low,
_ => return "Error: Not a valid performance level!".to_string(),
};
// `min_confirmations` comes from the renderer through IPC. Reject 0
// (and anything that casts to 0) instead of unwrapping — panic in this
// async block would unwind into Neon's panic guard but corrupt wallet
// state mid-write since we already hold the write lock.
let min_conf_nonzero = match NonZeroU32::try_from(min_confirmations as u32) {
Ok(n) => n,
Err(_) => return "Error: min_confirmations must be >= 1".to_string(),
};
let mut wallet = lightclient.wallet.write().await;
wallet.wallet_settings.min_confirmations = NonZeroU32::try_from(min_confirmations as u32).unwrap();
wallet.wallet_settings.min_confirmations = min_conf_nonzero;
wallet.wallet_settings.sync_config.performance_level = performancetype;
wallet.save_required = true;
"Successfully set config wallet to prod.".to_string()
Expand All @@ -2078,6 +2001,8 @@ fn set_config_wallet_to_prod(mut cx: FunctionContext) -> JsResult<JsPromise> {
Ok(promise)
}

// FFI-exposed but currently has no JS caller. Reserved for an upcoming UI
// feature; review input validation here when the JS caller is wired up.
fn get_config_wallet_performance(mut cx: FunctionContext) -> JsResult<JsPromise> {
let promise = cx
.task(move || -> Result<String, ZingolibError> {
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zingo-pc",
"productName": "Zingo PC",
"version": "2.0.16",
"version": "2.0.17",
"private": true,
"description": "Zingo PC",
"license": "MIT",
Expand Down Expand Up @@ -40,7 +40,7 @@
"react-textarea-autosize": "^8.5.9",
"typeface-roboto": "^1.1.13",
"url-parse": "^1.5.10",
"uuid": "^11.1.0",
"uuid": "^11.1.1",
"zcashname-sdk": "^0.8.7"
},
"scripts": {
Expand Down Expand Up @@ -218,7 +218,7 @@
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "./configs/entitlements.mac.inherit.plist",
"bundleVersion": "153",
"bundleVersion": "154",
"extendInfo": {
"ITSAppUsesNonExemptEncryption": false
},
Expand All @@ -240,7 +240,7 @@
"entitlements": "./configs/entitlements.mas.plist",
"entitlementsInherit": "./configs/entitlements.mas.inherit.plist",
"provisioningProfile": "./configs/Zingo_PC_Profile.provisionprofile",
"bundleVersion": "153",
"bundleVersion": "154",
"extendInfo": {
"ITSAppUsesNonExemptEncryption": false
},
Expand Down
Loading
Loading