zcash-android-wallet-sdk/backend-lib/src/main/rust/lib.rs

1870 lines
67 KiB
Rust

use std::convert::{Infallible, TryFrom, TryInto};
use std::error::Error;
use std::num::NonZeroU32;
use std::panic;
use std::path::Path;
use std::ptr;
use anyhow::anyhow;
use jni::objects::{JByteArray, JObject, JObjectArray, JValue};
use jni::{
objects::{JClass, JString},
sys::{jboolean, jbyteArray, jint, jlong, jobject, jobjectArray, jstring, JNI_FALSE, JNI_TRUE},
JNIEnv,
};
use prost::Message;
use secrecy::{ExposeSecret, SecretVec};
use tracing::{debug, error};
use tracing_subscriber::prelude::*;
use tracing_subscriber::reload;
use zcash_address::{ToAddress, ZcashAddress};
use zcash_client_backend::{
address::{Address, UnifiedAddress},
data_api::{
chain::{scan_cached_blocks, CommitmentTreeRoot, ScanSummary},
scanning::{ScanPriority, ScanRange},
wallet::{
create_proposed_transactions, decrypt_and_store_transaction,
input_selection::GreedyInputSelector, propose_shielding, propose_transfer,
},
Account, AccountBalance, AccountBirthday, AccountSource, InputSource, SeedRelevance,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite,
},
encoding::AddressCodec,
fees::{standard::SingleOutputChangeStrategy, DustOutputPolicy},
keys::{DecodingError, Era, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
proto::{proposal::Proposal, service::TreeState},
wallet::{NoteId, OvkPolicy, WalletTransparentOutput},
zip321::{Payment, TransactionRequest},
ShieldedProtocol,
};
use zcash_client_sqlite::{
chain::{init::init_blockmeta_db, BlockMeta},
wallet::init::{init_wallet_db, WalletMigrationError},
AccountId, FsBlockDb, WalletDb,
};
use zcash_primitives::{
block::BlockHash,
consensus::{
BlockHeight, BranchId, Network,
Network::{MainNetwork, TestNetwork},
Parameters,
},
legacy::{Script, TransparentAddress},
memo::{Memo, MemoBytes},
merkle_tree::HashSer,
transaction::{
components::{amount::NonNegativeAmount, Amount, OutPoint, TxOut},
fees::StandardFeeRule,
Transaction, TxId,
},
zip32::{self, DiversifierIndex},
};
use zcash_proofs::prover::LocalTxProver;
use crate::utils::{catch_unwind, exception::unwrap_exc_or};
mod utils;
const ANCHOR_OFFSET_U32: u32 = 10;
const ANCHOR_OFFSET: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(ANCHOR_OFFSET_U32) };
// Do not generate Orchard receivers until we support receiving Orchard funds.
const DEFAULT_ADDRESS_REQUEST: UnifiedAddressRequest =
UnifiedAddressRequest::unsafe_new(true, true, true);
#[cfg(debug_assertions)]
fn print_debug_state() {
debug!("WARNING! Debugging enabled! This will likely slow things down 10X!");
}
#[cfg(not(debug_assertions))]
fn print_debug_state() {
debug!("Release enabled (congrats, this is NOT a debug build).");
}
fn wallet_db<P: Parameters>(
env: &mut JNIEnv,
params: P,
db_data: JString,
) -> anyhow::Result<WalletDb<rusqlite::Connection, P>> {
WalletDb::for_path(utils::java_string_to_rust(env, &db_data), params)
.map_err(|e| anyhow!("Error opening wallet database connection: {}", e))
}
fn block_db(env: &mut JNIEnv, fsblockdb_root: JString) -> anyhow::Result<FsBlockDb> {
FsBlockDb::for_path(utils::java_string_to_rust(env, &fsblockdb_root))
.map_err(|e| anyhow!("Error opening block source database connection: {:?}", e))
}
fn account_id_from_jint(account: jint) -> anyhow::Result<zip32::AccountId> {
u32::try_from(account)
.map_err(|_| ())
.and_then(|id| zip32::AccountId::try_from(id).map_err(|_| ()))
.map_err(|_| anyhow!("Invalid account ID"))
}
fn account_id_from_jni<'local, P: Parameters>(
db_data: &WalletDb<rusqlite::Connection, P>,
account_index: jint,
) -> anyhow::Result<AccountId> {
let requested_account_index = account_id_from_jint(account_index)?;
// Find the single account matching the given ZIP 32 account index.
let mut accounts = db_data
.get_account_ids()?
.into_iter()
.filter_map(|account_id| {
db_data
.get_account(account_id)
.map_err(|e| {
anyhow!(
"Database error encountered retrieving account {:?}: {}",
account_id,
e
)
})
.and_then(|acct_opt|
acct_opt.ok_or_else(||
anyhow!(
"Wallet data corrupted: unable to retrieve account data for account {:?}",
account_id
)
).map(|account| match account.source() {
AccountSource::Derived { account_index, .. } if account_index == requested_account_index => Some(account),
_ => None
})
)
.transpose()
});
match (accounts.next(), accounts.next()) {
(Some(account), None) => Ok(account?.id()),
(None, None) => Err(anyhow!("Account does not exist")),
(_, Some(_)) => Err(anyhow!("Account index matches more than one account")),
}
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initOnLoad<'local>(
_env: JNIEnv<'local>,
_: JClass<'local>,
) {
// Set up the Android tracing layer.
#[cfg(target_os = "android")]
let android_layer = paranoid_android::layer("cash.z.rust.logs")
.with_ansi(false)
.with_filter(tracing_subscriber::filter::LevelFilter::INFO);
// Generate Android trace events from `tracing` spans.
let (trace_event_layer, reload_handle) = reload::Layer::new(utils::trace::Layer::new(None));
// Install the `tracing` subscriber.
let registry = tracing_subscriber::registry();
#[cfg(target_os = "android")]
let registry = registry.with(android_layer);
registry.with(trace_event_layer).init();
// Log panics instead of writing them to stderr.
log_panics::init();
// Load optional NDK APIs. We do this via a reload so that we can capture any errors
// that occur while trying to dynamically load the NDK.
if let Err(e) = reload_handle.modify(|layer| match utils::target_ndk::load() {
Ok(api) => *layer = utils::trace::Layer::new(Some(api)),
Err(e) => error!("Could not open NDK library or load symbols: {}", e),
}) {
error!("Failed to reload tracing subscriber with NDK APIs: {}", e);
}
// Manually build the Rayon thread pool, so we can name the threads.
rayon::ThreadPoolBuilder::new()
.thread_name(|i| format!("zc-rayon-{}", i))
.build_global()
.expect("Only initialized once");
debug!("Rust backend has been initialized successfully");
print_debug_state();
}
/// Sets up the internal structure of the blockmeta database.
///
/// Returns 0 if successful, or -1 otherwise.
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initBlockMetaDb<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
fsblockdb_root: JString<'local>,
) -> jint {
let res = catch_unwind(&mut env, |env| {
let mut db_meta = block_db(env, fsblockdb_root)?;
match init_blockmeta_db(&mut db_meta) {
Ok(()) => Ok(0),
Err(e) => Err(anyhow!("Error while initializing block metadata DB: {}", e)),
}
});
unwrap_exc_or(&mut env, res, -1)
}
/// Sets up the internal structure of the data database.
///
/// If `seed` is `null`, database migrations will be attempted without it.
///
/// Returns:
/// - 0 if successful.
/// - 1 if the seed must be provided in order to execute the requested migrations.
/// - 2 if the provided seed is not relevant to any of the derived accounts in the wallet.
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initDataDb<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
seed: JByteArray<'local>,
network_id: jint,
) -> jint {
let res = catch_unwind(&mut env, |env| {
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)
.map_err(|e| anyhow!("Error while opening data DB: {}", e))?;
let seed = (!seed.is_null()).then(|| SecretVec::new(env.convert_byte_array(seed).unwrap()));
match init_wallet_db(&mut db_data, seed) {
Ok(()) => Ok(0),
Err(e)
if matches!(
e.source().and_then(|e| e.downcast_ref()),
Some(&WalletMigrationError::SeedRequired)
) =>
{
Ok(1)
}
Err(e)
if matches!(
e.source().and_then(|e| e.downcast_ref()),
Some(&WalletMigrationError::SeedNotRelevant)
) =>
{
Ok(2)
}
Err(e) => Err(anyhow!("Error while initializing data DB: {}", e)),
}
});
unwrap_exc_or(&mut env, res, -1)
}
fn encode_usk<'a>(
env: &mut JNIEnv<'a>,
account: zip32::AccountId,
usk: UnifiedSpendingKey,
) -> jni::errors::Result<JObject<'a>> {
let encoded = SecretVec::new(usk.to_bytes(Era::Orchard));
let bytes = env.byte_array_from_slice(encoded.expose_secret())?;
env.new_object(
"cash/z/ecc/android/sdk/internal/model/JniUnifiedSpendingKey",
"(I[B)V",
&[JValue::Int(u32::from(account) as i32), (&bytes).into()],
)
}
fn decode_usk(env: &JNIEnv, usk: JByteArray) -> anyhow::Result<UnifiedSpendingKey> {
let usk_bytes = SecretVec::new(env.convert_byte_array(usk).unwrap());
// The remainder of the function is safe.
UnifiedSpendingKey::from_bytes(Era::Orchard, usk_bytes.expose_secret()).map_err(|e| match e {
DecodingError::EraMismatch(era) => anyhow!(
"Spending key was from era {:?}, but {:?} was expected.",
era,
Era::Orchard
),
e => anyhow!(
"An error occurred decoding the provided unified spending key: {:?}",
e
),
})
}
/// Adds the next available account-level spend authority, given the current set of
/// [ZIP 316] account identifiers known, to the wallet database.
///
/// Returns the newly created [ZIP 316] account identifier, along with the binary encoding
/// of the [`UnifiedSpendingKey`] for the newly created account. The caller should store
/// the returned spending key in a secure fashion.
///
/// If `seed` was imported from a backup and this method is being used to restore a
/// previous wallet state, you should use this method to add all of the desired
/// accounts before scanning the chain from the seed's birthday height.
///
/// By convention, wallets should only allow a new account to be generated after funds
/// have been received by the currently-available account (in order to enable
/// automated account recovery).
///
/// [ZIP 316]: https://zips.z.cash/zip-0316
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createAccount<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
seed: JByteArray<'local>,
treestate: JByteArray<'local>,
recover_until: jlong,
network_id: jint,
) -> jobject {
use zcash_client_backend::data_api::BirthdayError;
let res = catch_unwind(&mut env, |env| {
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)?;
let seed = SecretVec::new(env.convert_byte_array(seed).unwrap());
let treestate = TreeState::decode(&env.convert_byte_array(treestate).unwrap()[..])
.map_err(|e| anyhow!("Invalid TreeState: {}", e))?;
let recover_until = recover_until.try_into().ok();
let birthday =
AccountBirthday::from_treestate(treestate, recover_until).map_err(|e| match e {
BirthdayError::HeightInvalid(e) => {
anyhow!("Invalid TreeState: Invalid height: {}", e)
}
BirthdayError::Decode(e) => {
anyhow!("Invalid TreeState: Invalid frontier encoding: {}", e)
}
})?;
let (account_id, usk) = db_data
.create_account(&seed, &birthday)
.map_err(|e| anyhow!("Error while initializing accounts: {}", e))?;
let account = db_data.get_account(account_id)?.expect("just created");
let account_index = match account.source() {
AccountSource::Derived { account_index, .. } => account_index,
AccountSource::Imported => unreachable!("just created"),
};
Ok(encode_usk(env, account_index, usk)?.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
/// Checks whether the given seed is relevant to any of the derived accounts in the wallet.
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_isSeedRelevantToAnyDerivedAccounts<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
seed: JByteArray<'local>,
network_id: jint,
) -> jboolean {
let res = catch_unwind(&mut env, |env| {
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(env, network, db_data)?;
let seed = SecretVec::new(env.convert_byte_array(seed).unwrap());
// Replicate the logic from `initWalletDb`.
Ok(match db_data.seed_relevance_to_derived_accounts(&seed)? {
SeedRelevance::Relevant { .. } | SeedRelevance::NoAccounts => JNI_TRUE,
SeedRelevance::NotRelevant | SeedRelevance::NoDerivedAccounts => JNI_FALSE,
})
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
/// Derives and returns a unified spending key from the given seed for the given account ID.
///
/// Returns the newly created [ZIP 316] account identifier, along with the binary encoding
/// of the [`UnifiedSpendingKey`] for the newly created account. The caller should store
/// the returned spending key in a secure fashion.
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_deriveSpendingKey<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
seed: JByteArray<'local>,
account: jint,
network_id: jint,
) -> jobject {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustDerivationTool.deriveSpendingKey").entered();
let network = parse_network(network_id as u32)?;
let seed = SecretVec::new(env.convert_byte_array(seed).unwrap());
let account = account_id_from_jint(account)?;
let usk = UnifiedSpendingKey::from_seed(&network, seed.expose_secret(), account)
.map_err(|e| anyhow!("error generating unified spending key from seed: {:?}", e))?;
Ok(encode_usk(env, account, usk)?.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_deriveUnifiedFullViewingKeysFromSeed<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
seed: JByteArray<'local>,
accounts: jint,
network_id: jint,
) -> jobjectArray {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustDerivationTool.deriveUnifiedFullViewingKeysFromSeed")
.entered();
let network = parse_network(network_id as u32)?;
let seed = env.convert_byte_array(seed).unwrap();
let accounts = if accounts > 0 {
accounts as u32
} else {
return Err(anyhow!("accounts argument must be greater than zero"));
};
let ufvks: Vec<_> = (0..accounts)
.map(|account| {
let account_id = zip32::AccountId::try_from(account)
.map_err(|_| anyhow!("Invalid account ID"))?;
UnifiedSpendingKey::from_seed(&network, &seed, account_id)
.map_err(|e| {
anyhow!("error generating unified spending key from seed: {:?}", e)
})
.map(|usk| usk.to_unified_full_viewing_key().encode(&network))
})
.collect::<Result<_, _>>()?;
Ok(utils::rust_vec_to_java(
env,
ufvks,
"java/lang/String",
|env, ufvk| env.new_string(ufvk),
|env| env.new_string(""),
)?
.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_deriveUnifiedAddressFromSeed<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
seed: JByteArray<'local>,
account_index: jint,
network_id: jint,
) -> jstring {
let res = panic::catch_unwind(|| {
let _span =
tracing::info_span!("RustDerivationTool.deriveUnifiedAddressFromSeed").entered();
let network = parse_network(network_id as u32)?;
let seed = env.convert_byte_array(seed).unwrap();
let account_id = account_id_from_jint(account_index)?;
let ufvk = UnifiedSpendingKey::from_seed(&network, &seed, account_id)
.map_err(|e| anyhow!("error generating unified spending key from seed: {:?}", e))
.map(|usk| usk.to_unified_full_viewing_key())?;
let (ua, _) = ufvk
.find_address(DiversifierIndex::new(), DEFAULT_ADDRESS_REQUEST)
.expect("At least one Unified Address should be derivable");
let address_str = ua.encode(&network);
let output = env
.new_string(address_str)
.expect("Couldn't create Java string!");
Ok(output.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_deriveUnifiedAddressFromViewingKey<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
ufvk_string: JString<'local>,
network_id: jint,
) -> jstring {
let res = catch_unwind(&mut env, |env| {
let _span =
tracing::info_span!("RustDerivationTool.deriveUnifiedAddressFromViewingKey").entered();
let network = parse_network(network_id as u32)?;
let ufvk_string = utils::java_string_to_rust(env, &ufvk_string);
let ufvk = match UnifiedFullViewingKey::decode(&network, &ufvk_string) {
Ok(ufvk) => ufvk,
Err(e) => {
return Err(anyhow!(
"Error while deriving viewing key from string input: {}",
e,
));
}
};
// Derive the default Unified Address (containing the default Sapling payment
// address that older SDKs used).
let (ua, _) = ufvk.default_address(DEFAULT_ADDRESS_REQUEST)?;
let address_str = ua.encode(&network);
let output = env
.new_string(address_str)
.expect("Couldn't create Java string!");
Ok(output.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_deriveUnifiedFullViewingKey<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
usk: JByteArray<'local>,
network_id: jint,
) -> jstring {
let res = panic::catch_unwind(|| {
let _span = tracing::info_span!("RustDerivationTool.deriveUnifiedFullViewingKey").entered();
let usk = decode_usk(&env, usk)?;
let network = parse_network(network_id as u32)?;
let ufvk = usk.to_unified_full_viewing_key();
let output = env
.new_string(ufvk.encode(&network))
.expect("Couldn't create Java string!");
Ok(output.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getCurrentAddress<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
account: jint,
network_id: jint,
) -> jstring {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.getCurrentAddress").entered();
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(env, network, db_data)?;
let account = account_id_from_jni(&db_data, account)?;
match db_data.get_current_address(account) {
Ok(Some(addr)) => {
let addr_str = addr.encode(&network);
let output = env
.new_string(addr_str)
.expect("Couldn't create Java string!");
Ok(output.into_raw())
}
Ok(None) => Err(anyhow!("{:?} is not known to the wallet", account)),
Err(e) => Err(anyhow!("Error while fetching address: {}", e)),
}
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
struct UnifiedAddressParser(UnifiedAddress);
impl zcash_address::TryFromRawAddress for UnifiedAddressParser {
type Error = anyhow::Error;
fn try_from_raw_unified(
data: zcash_address::unified::Address,
) -> Result<Self, zcash_address::ConversionError<Self::Error>> {
data.try_into()
.map(UnifiedAddressParser)
.map_err(|e| anyhow!("Invalid Unified Address: {}", e).into())
}
}
/// Returns the transparent receiver within the given Unified Address, if any.
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getTransparentReceiverForUnifiedAddress<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
ua: JString<'local>,
) -> jstring {
let res = catch_unwind(&mut env, |env| {
let _span =
tracing::info_span!("RustBackend.getTransparentReceiverForUnifiedAddress").entered();
let ua_str = utils::java_string_to_rust(env, &ua);
let (network, ua) = match ZcashAddress::try_from_encoded(&ua_str) {
Ok(addr) => addr
.convert::<(_, UnifiedAddressParser)>()
.map_err(|e| anyhow!("Not a Unified Address: {}", e)),
Err(e) => return Err(anyhow!("Invalid Zcash address: {}", e)),
}?;
if let Some(taddr) = ua.0.transparent() {
let taddr = match taddr {
TransparentAddress::PublicKeyHash(data) => {
ZcashAddress::from_transparent_p2pkh(network, *data)
}
TransparentAddress::ScriptHash(data) => {
ZcashAddress::from_transparent_p2sh(network, *data)
}
};
let output = env
.new_string(taddr.encode())
.expect("Couldn't create Java string!");
Ok(output.into_raw())
} else {
Err(anyhow!(
"Unified Address doesn't contain a transparent receiver"
))
}
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
/// Returns the Sapling receiver within the given Unified Address, if any.
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getSaplingReceiverForUnifiedAddress<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
ua: JString<'local>,
) -> jstring {
let res = catch_unwind(&mut env, |env| {
let _span =
tracing::info_span!("RustBackend.getSaplingReceiverForUnifiedAddress").entered();
let ua_str = utils::java_string_to_rust(env, &ua);
let (network, ua) = match ZcashAddress::try_from_encoded(&ua_str) {
Ok(addr) => addr
.convert::<(_, UnifiedAddressParser)>()
.map_err(|e| anyhow!("Not a Unified Address: {}", e)),
Err(e) => return Err(anyhow!("Invalid Zcash address: {}", e)),
}?;
if let Some(addr) = ua.0.sapling() {
let output = env
.new_string(ZcashAddress::from_sapling(network, addr.to_bytes()).encode())
.expect("Couldn't create Java string!");
Ok(output.into_raw())
} else {
Err(anyhow!(
"Unified Address doesn't contain a Sapling receiver"
))
}
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_isValidSpendingKey<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
usk: JByteArray<'local>,
) -> jboolean {
let res = panic::catch_unwind(|| {
let _span = tracing::info_span!("RustBackend.isValidSpendingKey").entered();
let _usk = decode_usk(&env, usk)?;
Ok(JNI_TRUE)
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_isValidSaplingAddress<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
addr: JString<'local>,
network_id: jint,
) -> jboolean {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.isValidSaplingAddress").entered();
let network = parse_network(network_id as u32)?;
let addr = utils::java_string_to_rust(env, &addr);
match Address::decode(&network, &addr) {
Some(addr) => match addr {
Address::Sapling(_) => Ok(JNI_TRUE),
Address::Transparent(_) | Address::Unified(_) => Ok(JNI_FALSE),
},
None => Err(anyhow!("Address is for the wrong network")),
}
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_isValidTransparentAddress<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
addr: JString<'local>,
network_id: jint,
) -> jboolean {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.isValidTransparentAddress").entered();
let network = parse_network(network_id as u32)?;
let addr = utils::java_string_to_rust(env, &addr);
match Address::decode(&network, &addr) {
Some(addr) => match addr {
Address::Sapling(_) | Address::Unified(_) => Ok(JNI_FALSE),
Address::Transparent(_) => Ok(JNI_TRUE),
},
None => Err(anyhow!("Address is for the wrong network")),
}
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_isValidUnifiedAddress<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
addr: JString<'local>,
network_id: jint,
) -> jboolean {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.isValidUnifiedAddress").entered();
let network = parse_network(network_id as u32)?;
let addr = utils::java_string_to_rust(env, &addr);
match Address::decode(&network, &addr) {
Some(addr) => match addr {
Address::Unified(_) => Ok(JNI_TRUE),
Address::Sapling(_) | Address::Transparent(_) => Ok(JNI_FALSE),
},
None => Err(anyhow!("Address is for the wrong network")),
}
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getTotalTransparentBalance<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
address: JString<'local>,
network_id: jint,
) -> jlong {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.getTotalTransparentBalance").entered();
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(env, network, db_data)?;
let addr = utils::java_string_to_rust(env, &address);
let taddr = TransparentAddress::decode(&network, &addr).unwrap();
let min_confirmations = NonZeroU32::new(1).unwrap();
let amount = (&db_data)
.get_target_and_anchor_heights(min_confirmations)
.map_err(|e| anyhow!("Error while fetching anchor height: {}", e))
.and_then(|opt_anchor| {
opt_anchor
.map(|(target, _)| target) // Include unconfirmed funds.
.ok_or(anyhow!("Anchor height not available; scan required."))
})
.and_then(|anchor| {
(&db_data)
.get_unspent_transparent_outputs(&taddr, anchor, &[])
.map_err(|e| anyhow!("Error while fetching verified balance: {}", e))
})?
.iter()
.map(|utxo| utxo.txout().value)
.sum::<Option<NonNegativeAmount>>()
.ok_or_else(|| anyhow!("Balance overflowed MAX_MONEY"))?;
Ok(Amount::from(amount).into())
});
unwrap_exc_or(&mut env, res, -1)
}
fn parse_protocol(code: i32) -> Option<ShieldedProtocol> {
match code {
2 => Some(ShieldedProtocol::Sapling),
3 => Some(ShieldedProtocol::Orchard),
_ => None,
}
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getMemoAsUtf8<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
txid_bytes: JByteArray<'local>,
pool_type: jint,
output_index: jint,
network_id: jint,
) -> jstring {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.getMemoAsUtf8").entered();
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(env, network, db_data)?;
let txid_bytes = env.convert_byte_array(txid_bytes)?;
let txid = TxId::read(&txid_bytes[..])?;
let protocol = parse_protocol(pool_type)
.ok_or(anyhow!("Shielded protocol not recognized: {}", pool_type))?;
let output_index = u16::try_from(output_index)?;
let memo = (&db_data)
.get_memo(NoteId::new(txid, protocol, output_index))
.map_err(|e| anyhow!("An error occurred retrieving the memo, {}", e))
.and_then(|memo| match memo {
Some(Memo::Empty) => Ok("".to_string()),
Some(Memo::Text(memo)) => Ok(memo.into()),
None => Err(anyhow!("Memo not available")),
_ => Err(anyhow!("This memo does not contain UTF-8 text")),
})?;
let output = env.new_string(memo).expect("Couldn't create Java string!");
Ok(output.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
fn encode_blockmeta<'a>(env: &mut JNIEnv<'a>, meta: BlockMeta) -> jni::errors::Result<JObject<'a>> {
let block_hash = env.byte_array_from_slice(&meta.block_hash.0)?;
env.new_object(
"cash/z/ecc/android/sdk/internal/model/JniBlockMeta",
"(J[BJJJ)V",
&[
JValue::Long(i64::from(u32::from(meta.height))),
(&block_hash).into(),
JValue::Long(i64::from(meta.block_time)),
JValue::Long(i64::from(meta.sapling_outputs_count)),
JValue::Long(i64::from(meta.orchard_actions_count)),
],
)
}
fn decode_blockmeta(env: &mut JNIEnv, obj: JObject) -> anyhow::Result<BlockMeta> {
fn long_as_u32(env: &mut JNIEnv, obj: &JObject, name: &str) -> anyhow::Result<u32> {
Ok(u32::try_from(env.get_field(obj, name, "J")?.j()?)?)
}
fn byte_array<const N: usize>(
env: &mut JNIEnv,
obj: &JObject,
name: &str,
) -> anyhow::Result<[u8; N]> {
let field = JByteArray::from(env.get_field(obj, name, "[B")?.l()?);
Ok(env.convert_byte_array(field)?[..].try_into()?)
}
Ok(BlockMeta {
height: BlockHeight::from_u32(long_as_u32(env, &obj, "height")?),
block_hash: BlockHash(byte_array(env, &obj, "hash")?),
block_time: long_as_u32(env, &obj, "time")?,
sapling_outputs_count: long_as_u32(env, &obj, "saplingOutputsCount")?,
orchard_actions_count: long_as_u32(env, &obj, "orchardOutputsCount")?,
})
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_writeBlockMetadata<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_cache: JString<'local>,
block_meta: JObjectArray<'local>,
) -> jboolean {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.writeBlockMetadata").entered();
let block_db = block_db(env, db_cache)?;
let block_meta = {
let count = env.get_array_length(&block_meta).unwrap();
(0..count)
.scan(env, |env, i| {
Some(
env.get_object_array_element(&block_meta, i)
.map_err(|e| e.into())
.and_then(|jobj| decode_blockmeta(env, jobj)),
)
})
.collect::<Result<Vec<_>, _>>()?
};
match block_db.write_block_metadata(&block_meta) {
Ok(()) => Ok(JNI_TRUE),
Err(e) => Err(anyhow!(
"Failed to write block metadata to FsBlockDb: {:?}",
e
)),
}
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getLatestCacheHeight<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
fsblockdb_root: JString<'local>,
) -> jlong {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.getLatestCacheHeight").entered();
let block_db = block_db(env, fsblockdb_root)?;
match block_db.get_max_cached_height() {
Ok(Some(block_height)) => Ok(i64::from(u32::from(block_height))),
// Use -1 to return null across the FFI.
Ok(None) => Ok(-1),
Err(e) => Err(anyhow!(
"Failed to read block metadata from FsBlockDb: {:?}",
e
)),
}
});
unwrap_exc_or(&mut env, res, -1)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_findBlockMetadata<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
fsblockdb_root: JString<'local>,
height: jlong,
) -> jobject {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.findBlockMetadata").entered();
let block_db = block_db(env, fsblockdb_root)?;
let height = BlockHeight::try_from(height)?;
match block_db.find_block(height) {
Ok(Some(meta)) => Ok(encode_blockmeta(env, meta)?.into_raw()),
Ok(None) => Ok(ptr::null_mut()),
Err(e) => Err(anyhow!(
"Failed to read block metadata from FsBlockDb: {:?}",
e
)),
}
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_rewindBlockMetadataToHeight<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
fsblockdb_root: JString<'local>,
height: jlong,
) {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.rewindBlockMetadataToHeight").entered();
let block_db = block_db(env, fsblockdb_root)?;
let height = BlockHeight::try_from(height)?;
block_db.truncate_to_height(height).map_err(|e| {
anyhow!(
"Error while rewinding block metadata DB to height {}: {}",
height,
e
)
})
});
unwrap_exc_or(&mut env, res, ())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getNearestRewindHeight<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
height: jlong,
network_id: jint,
) -> jlong {
#[allow(deprecated)]
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.getNearestRewindHeight").entered();
if height < 100 {
Ok(height)
} else {
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(env, network, db_data)?;
match db_data.get_min_unspent_height() {
Ok(Some(best_height)) => {
let first_unspent_note_height = u32::from(best_height);
Ok(std::cmp::min(
first_unspent_note_height as i64,
height as i64,
))
}
Ok(None) => Ok(height as i64),
Err(e) => Err(anyhow!(
"Error while getting nearest rewind height for {}: {}",
height,
e
)),
}
}
});
unwrap_exc_or(&mut env, res, -1) as jlong
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_rewindToHeight<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
height: jlong,
network_id: jint,
) -> jboolean {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.rewindToHeight").entered();
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)?;
let height = BlockHeight::try_from(height)?;
db_data
.truncate_to_height(height)
.map(|_| 1)
.map_err(|e| anyhow!("Error while rewinding data DB to height {}: {}", height, e))
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
fn decode_subtree_root<H>(
env: &mut JNIEnv,
obj: JObject,
node_parser: impl FnOnce(&[u8]) -> std::io::Result<H>,
) -> anyhow::Result<CommitmentTreeRoot<H>> {
fn long_as_u32(env: &mut JNIEnv, obj: &JObject, name: &str) -> anyhow::Result<u32> {
Ok(u32::try_from(env.get_field(obj, name, "J")?.j()?)?)
}
fn byte_array(env: &mut JNIEnv, obj: &JObject, name: &str) -> anyhow::Result<Vec<u8>> {
let field = JByteArray::from(env.get_field(obj, name, "[B")?.l()?);
Ok(env.convert_byte_array(field)?[..].try_into()?)
}
Ok(CommitmentTreeRoot::from_parts(
BlockHeight::from_u32(long_as_u32(env, &obj, "completingBlockHeight")?),
node_parser(&byte_array(env, &obj, "rootHash")?[..])?,
))
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_putSubtreeRoots<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
sapling_start_index: jlong,
sapling_roots: JObjectArray<'local>,
orchard_start_index: jlong,
orchard_roots: JObjectArray<'local>,
network_id: jint,
) -> jboolean {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.putSubtreeRoots").entered();
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)?;
fn parse_roots<H>(
env: &mut JNIEnv,
roots: JObjectArray,
node_parser: impl Fn(&[u8]) -> std::io::Result<H>,
) -> anyhow::Result<Vec<CommitmentTreeRoot<H>>> {
let count = env.get_array_length(&roots).unwrap();
(0..count)
.scan(env, |env, i| {
Some(
env.get_object_array_element(&roots, i)
.map_err(|e| e.into())
.and_then(|jobj| decode_subtree_root(env, jobj, &node_parser)),
)
})
.collect::<Result<Vec<_>, _>>()
}
let sapling_start_index = if sapling_start_index >= 0 {
sapling_start_index as u64
} else {
return Err(anyhow!("Sapling start index must be nonnegative."));
};
let sapling_roots = parse_roots(env, sapling_roots, |n| sapling::Node::read(n))?;
let orchard_start_index = if orchard_start_index >= 0 {
orchard_start_index as u64
} else {
return Err(anyhow!("Orchard start index must be nonnegative."));
};
let orchard_roots = parse_roots(env, orchard_roots, |n| {
orchard::tree::MerkleHashOrchard::read(n)
})?;
db_data
.put_sapling_subtree_roots(sapling_start_index, &sapling_roots)
.map_err(|e| anyhow!("Error while storing Sapling subtree roots: {}", e))?;
db_data
.put_orchard_subtree_roots(orchard_start_index, &orchard_roots)
.map_err(|e| anyhow!("Error while storing Orchard subtree roots: {}", e))?;
Ok(JNI_TRUE)
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_updateChainTip<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
height: jlong,
network_id: jint,
) -> jboolean {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.updateChainTip").entered();
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)?;
let height = BlockHeight::try_from(height)?;
db_data
.update_chain_tip(height)
.map(|()| JNI_TRUE)
.map_err(|e| anyhow!("Error while updating chain tip to height {}: {}", height, e))
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getFullyScannedHeight<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
network_id: jint,
) -> jlong {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.getFullyScannedHeight").entered();
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(env, network, db_data)?;
match db_data.block_fully_scanned() {
Ok(Some(metadata)) => Ok(i64::from(u32::from(metadata.block_height()))),
// Use -1 to return null across the FFI.
Ok(None) => Ok(-1),
Err(e) => Err(anyhow!(
"Failed to read block metadata from WalletDb: {:?}",
e
)),
}
});
unwrap_exc_or(&mut env, res, -1)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getMaxScannedHeight<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
network_id: jint,
) -> jlong {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.getMaxScannedHeight").entered();
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(env, network, db_data)?;
match db_data.block_max_scanned() {
Ok(Some(metadata)) => Ok(i64::from(u32::from(metadata.block_height()))),
// Use -1 to return null across the FFI.
Ok(None) => Ok(-1),
Err(e) => Err(anyhow!(
"Failed to read block metadata from WalletDb: {:?}",
e
)),
}
});
unwrap_exc_or(&mut env, res, -1)
}
const JNI_ACCOUNT_BALANCE: &str = "cash/z/ecc/android/sdk/internal/model/JniAccountBalance";
fn encode_account_balance<'a>(
env: &mut JNIEnv<'a>,
account: &zip32::AccountId,
balance: &AccountBalance,
) -> jni::errors::Result<JObject<'a>> {
let sapling_verified_balance = Amount::from(balance.sapling_balance().spendable_value());
let sapling_change_pending =
Amount::from(balance.sapling_balance().change_pending_confirmation());
let sapling_value_pending =
Amount::from(balance.sapling_balance().value_pending_spendability());
let orchard_verified_balance = Amount::from(balance.orchard_balance().spendable_value());
let orchard_change_pending =
Amount::from(balance.orchard_balance().change_pending_confirmation());
let orchard_value_pending =
Amount::from(balance.orchard_balance().value_pending_spendability());
let unshielded = Amount::from(balance.unshielded());
env.new_object(
JNI_ACCOUNT_BALANCE,
"(IJJJJJJJ)V",
&[
JValue::Int(u32::from(*account) as i32),
JValue::Long(sapling_verified_balance.into()),
JValue::Long(sapling_change_pending.into()),
JValue::Long(sapling_value_pending.into()),
JValue::Long(orchard_verified_balance.into()),
JValue::Long(orchard_change_pending.into()),
JValue::Long(orchard_value_pending.into()),
JValue::Long(unshielded.into()),
],
)
}
/// Returns a `JniWalletSummary` object, provided that `progress_numerator` is
/// nonnegative, `progress_denominator` is positive, and the represented ratio is in the
/// range 0.0 to 1.0 inclusive.
///
/// If these conditions are not met, this fails and leaves an `IllegalArgumentException`
/// pending.
fn encode_wallet_summary<'a, P: Parameters>(
env: &mut JNIEnv<'a>,
db_data: &WalletDb<rusqlite::Connection, P>,
summary: WalletSummary<AccountId>,
) -> anyhow::Result<JObject<'a>> {
let account_balances = utils::rust_vec_to_java(
env,
summary
.account_balances()
.into_iter()
.map(|(account_id, balance)| {
let account_index = match db_data
.get_account(*account_id)?
.expect("the account exists in the wallet")
.source()
{
AccountSource::Derived { account_index, .. } => account_index,
AccountSource::Imported => unreachable!("Imported accounts are unimplemented"),
};
Ok::<_, anyhow::Error>((account_index, balance))
})
.collect::<Result<_, _>>()?,
JNI_ACCOUNT_BALANCE,
|env, (account_index, balance)| encode_account_balance(env, &account_index, balance),
|env| encode_account_balance(env, &zip32::AccountId::ZERO, &AccountBalance::ZERO),
)?;
let (progress_numerator, progress_denominator) = summary
.scan_progress()
.map(|progress| (*progress.numerator(), *progress.denominator()))
.unwrap_or((0, 1));
Ok(env.new_object(
"cash/z/ecc/android/sdk/internal/model/JniWalletSummary",
&format!("([L{};JJJJJJ)V", JNI_ACCOUNT_BALANCE),
&[
(&account_balances).into(),
JValue::Long(i64::from(u32::from(summary.chain_tip_height()))),
JValue::Long(i64::from(u32::from(summary.fully_scanned_height()))),
JValue::Long(progress_numerator as i64),
JValue::Long(progress_denominator as i64),
JValue::Long(summary.next_sapling_subtree_index() as i64),
JValue::Long(summary.next_orchard_subtree_index() as i64),
],
)?)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getWalletSummary<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
network_id: jint,
) -> jobject {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.getWalletSummary").entered();
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(env, network, db_data)?;
match db_data
.get_wallet_summary(ANCHOR_OFFSET_U32)
.map_err(|e| anyhow!("Error while fetching scan progress: {}", e))?
.filter(|summary| {
summary
.scan_progress()
.map(|r| r.denominator() > &0)
.unwrap_or(false)
}) {
Some(summary) => Ok(encode_wallet_summary(env, &db_data, summary)?.into_raw()),
None => Ok(ptr::null_mut()),
}
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
fn encode_scan_range<'a>(
env: &mut JNIEnv<'a>,
scan_range: ScanRange,
) -> jni::errors::Result<JObject<'a>> {
let priority = match scan_range.priority() {
ScanPriority::Ignored => 0,
ScanPriority::Scanned => 10,
ScanPriority::Historic => 20,
ScanPriority::OpenAdjacent => 30,
ScanPriority::FoundNote => 40,
ScanPriority::ChainTip => 50,
ScanPriority::Verify => 60,
};
env.new_object(
"cash/z/ecc/android/sdk/internal/model/JniScanRange",
"(JJJ)V",
&[
JValue::Long(i64::from(u32::from(scan_range.block_range().start))),
JValue::Long(i64::from(u32::from(scan_range.block_range().end))),
JValue::Long(priority),
],
)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_suggestScanRanges<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
network_id: jint,
) -> jobjectArray {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.suggestScanRanges").entered();
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(env, network, db_data)?;
let ranges = db_data
.suggest_scan_ranges()
.map_err(|e| anyhow!("Error while fetching suggested scan ranges: {}", e))?;
Ok(utils::rust_vec_to_java(
env,
ranges,
"cash/z/ecc/android/sdk/internal/model/JniScanRange",
|env, scan_range| encode_scan_range(env, scan_range),
|env| {
encode_scan_range(
env,
ScanRange::from_parts((0.into())..(0.into()), ScanPriority::Scanned),
)
},
)?
.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
fn encode_scan_summary<'a>(
env: &mut JNIEnv<'a>,
scan_summary: ScanSummary,
) -> anyhow::Result<JObject<'a>> {
let scanned_range = scan_summary.scanned_range();
Ok(env.new_object(
"cash/z/ecc/android/sdk/internal/model/JniScanSummary",
"(JJJJ)V",
&[
i64::from(u32::from(scanned_range.start)).into(),
i64::from(u32::from(scanned_range.end)).into(),
i64::try_from(scan_summary.spent_sapling_note_count())?.into(),
i64::try_from(scan_summary.received_sapling_note_count())?.into(),
],
)?)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_scanBlocks<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_cache: JString<'local>,
db_data: JString<'local>,
from_height: jlong,
from_state: JByteArray<'local>,
limit: jlong,
network_id: jint,
) -> jobject {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.scanBlocks").entered();
let network = parse_network(network_id as u32)?;
let db_cache = block_db(env, db_cache)?;
let mut db_data = wallet_db(env, network, db_data)?;
let from_height = BlockHeight::try_from(from_height)?;
let from_state = TreeState::decode(&env.convert_byte_array(from_state).unwrap()[..])
.map_err(|e| anyhow!("Invalid TreeState: {}", e))?
.to_chain_state()?;
let limit = usize::try_from(limit)?;
match scan_cached_blocks(
&network,
&db_cache,
&mut db_data,
from_height,
&from_state,
limit,
) {
Ok(scan_summary) => Ok(encode_scan_summary(env, scan_summary)?.into_raw()),
Err(e) => Err(anyhow!(
"Rust error while scanning blocks (limit {:?}): {}",
limit,
e
)),
}
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_putUtxo<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
address: JString<'local>,
txid_bytes: JByteArray<'local>,
index: jint,
script: JByteArray<'local>,
value: jlong,
height: jint,
network_id: jint,
) -> jboolean {
// debug!("For height {} found consensus branch {:?}", height, branch);
debug!("preparing to store UTXO in db_data");
#[allow(deprecated)]
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.putUtxo").entered();
let network = parse_network(network_id as u32)?;
let txid_bytes = env.convert_byte_array(txid_bytes).unwrap();
let mut txid = [0u8; 32];
txid.copy_from_slice(&txid_bytes);
let script_pubkey = Script(env.convert_byte_array(script).unwrap());
let mut db_data = wallet_db(env, network, db_data)?;
let addr = utils::java_string_to_rust(env, &address);
let _address = TransparentAddress::decode(&network, &addr).unwrap();
let output = WalletTransparentOutput::from_parts(
OutPoint::new(txid, index as u32),
TxOut {
value: NonNegativeAmount::from_nonnegative_i64(value)
.map_err(|_| anyhow!("Invalid UTXO value"))?,
script_pubkey,
},
BlockHeight::from(height as u32),
)
.ok_or_else(|| anyhow!("UTXO is not P2PKH or P2SH"))?;
debug!("Storing UTXO in db_data");
match db_data.put_received_transparent_utxo(&output) {
Ok(_) => Ok(JNI_TRUE),
Err(e) => Err(anyhow!("Error while inserting UTXO: {}", e)),
}
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_decryptAndStoreTransaction<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
tx: JByteArray<'local>,
network_id: jint,
) -> jboolean {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.decryptAndStoreTransaction").entered();
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)?;
let tx_bytes = env.convert_byte_array(tx).unwrap();
// The consensus branch ID passed in here does not matter:
// - v4 and below cache it internally, but all we do with this transaction while
// it is in memory is decryption and serialization, neither of which use the
// consensus branch ID.
// - v5 and above transactions ignore the argument, and parse the correct value
// from their encoding.
let tx = Transaction::read(&tx_bytes[..], BranchId::Sapling)?;
match decrypt_and_store_transaction(&network, &mut db_data, &tx) {
Ok(()) => Ok(JNI_TRUE),
Err(e) => Err(anyhow!("Error while decrypting transaction: {}", e)),
}
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
fn zip317_helper<DbT>(
change_memo: Option<MemoBytes>,
use_zip317_fees: jboolean,
) -> GreedyInputSelector<DbT, SingleOutputChangeStrategy> {
let fee_rule = if use_zip317_fees == JNI_TRUE {
StandardFeeRule::Zip317
} else {
#[allow(deprecated)]
StandardFeeRule::PreZip313
};
GreedyInputSelector::new(
SingleOutputChangeStrategy::new(fee_rule, change_memo, ShieldedProtocol::Orchard),
DustOutputPolicy::default(),
)
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeTransfer<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
account: jint,
to: JString<'local>,
value: jlong,
memo: JByteArray<'local>,
network_id: jint,
use_zip317_fees: jboolean,
) -> jbyteArray {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.proposeTransfer").entered();
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)?;
let account = account_id_from_jni(&db_data, account)?;
let to = utils::java_string_to_rust(env, &to);
let value = NonNegativeAmount::from_nonnegative_i64(value)
.map_err(|_| anyhow!("Invalid amount, out of range"))?;
let memo_bytes = env.convert_byte_array(memo).unwrap();
let to = match Address::decode(&network, &to) {
Some(to) => to,
None => {
return Err(anyhow!("Address is for the wrong network"));
}
};
// TODO: consider warning in this case somehow, rather than swallowing this error
let memo = match to {
Address::Sapling(_) | Address::Unified(_) => {
let memo_value =
Memo::from_bytes(&memo_bytes).map_err(|_| anyhow!("Invalid memo"))?;
Some(MemoBytes::from(&memo_value))
}
Address::Transparent(_) => None,
};
let input_selector = zip317_helper(None, use_zip317_fees);
let request = TransactionRequest::new(vec![Payment {
recipient_address: to,
amount: value,
memo,
label: None,
message: None,
other_params: vec![],
}])
.map_err(|e| anyhow!("Error creating transaction request: {:?}", e))?;
let proposal = propose_transfer::<_, _, _, Infallible>(
&mut db_data,
&network,
account,
&input_selector,
request,
ANCHOR_OFFSET,
)
.map_err(|e| anyhow!("Error creating transaction proposal: {}", e))?;
Ok(utils::rust_bytes_to_java(
&env,
Proposal::from_standard_proposal(&network, &proposal)
.encode_to_vec()
.as_ref(),
)?
.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeShielding<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
account: jint,
shielding_threshold: jlong,
memo: JByteArray<'local>,
transparent_receiver: JString<'local>,
network_id: jint,
use_zip317_fees: jboolean,
) -> jbyteArray {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.proposeShielding").entered();
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)?;
let account = account_id_from_jni(&db_data, account)?;
let shielding_threshold = NonNegativeAmount::from_nonnegative_i64(shielding_threshold)
.map_err(|_| anyhow!("Invalid shielding threshold, out of range"))?;
let memo_bytes = env.convert_byte_array(memo).unwrap();
let transparent_receiver =
match utils::java_nullable_string_to_rust(env, &transparent_receiver) {
None => Ok(None),
Some(addr) => match Address::decode(&network, &addr) {
None => Err(anyhow!("Transparent receiver is for the wrong network")),
Some(addr) => match addr {
Address::Sapling(_) | Address::Unified(_) => {
Err(anyhow!("Transparent receiver is not a transparent address"))
}
Address::Transparent(addr) => {
if db_data
.get_transparent_receivers(account)?
.contains_key(&addr)
{
Ok(Some(addr))
} else {
Err(anyhow!("Transparent receiver does not belong to account"))
}
}
},
},
}?;
let min_confirmations = 0;
let min_confirmations_for_heights = NonZeroU32::new(1).unwrap();
let account_receivers = db_data
.get_target_and_anchor_heights(min_confirmations_for_heights)
.map_err(|e| anyhow!("Error while fetching anchor height: {}", e))
.and_then(|opt_anchor| {
opt_anchor
.map(|(target, _)| target) // Include unconfirmed funds.
.ok_or(anyhow!("Anchor height not available; scan required."))
})
.and_then(|anchor| {
db_data
.get_transparent_balances(account, anchor)
.map_err(|e| {
anyhow!(
"Error while fetching transparent balances for {:?}: {}",
account,
e
)
})
})?;
let from_addrs = if let Some((addr, _)) = transparent_receiver.map_or_else(||
if account_receivers.len() > 1 {
Err(anyhow!(
"Account has more than one transparent receiver with funds to shield; this is not yet supported by the SDK. Provide a specific transparent receiver to shield funds from."
))
} else {
Ok(account_receivers.iter().next().map(|(a, v)| (*a, *v)))
},
|addr| Ok(account_receivers.get(&addr).map(|value| (addr, *value)))
)?.filter(|(_, value)| *value >= shielding_threshold.into()) {
[addr]
} else {
// There are no transparent funds to shield; don't create a proposal.
return Ok(ptr::null_mut());
};
let memo = Memo::from_bytes(&memo_bytes).unwrap();
let input_selector = zip317_helper(Some(MemoBytes::from(&memo)), use_zip317_fees);
let proposal = propose_shielding::<_, _, _, Infallible>(
&mut db_data,
&network,
&input_selector,
shielding_threshold,
&from_addrs,
min_confirmations,
)
.map_err(|e| anyhow!("Error while shielding transaction: {}", e))?;
Ok(utils::rust_bytes_to_java(
&env,
Proposal::from_standard_proposal(&network, &proposal)
.encode_to_vec()
.as_ref(),
)?
.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createProposedTransactions<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
proposal: JByteArray<'local>,
usk: JByteArray<'local>,
spend_params: JString<'local>,
output_params: JString<'local>,
network_id: jint,
) -> jobjectArray {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.createProposedTransaction").entered();
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)?;
let usk = decode_usk(&env, usk)?;
let spend_params = utils::java_string_to_rust(env, &spend_params);
let output_params = utils::java_string_to_rust(env, &output_params);
let prover = LocalTxProver::new(Path::new(&spend_params), Path::new(&output_params));
let proposal = Proposal::decode(&env.convert_byte_array(proposal)?[..])
.map_err(|e| anyhow!("Invalid proposal: {}", e))?
.try_into_standard_proposal(&network, &db_data)?;
let txids = create_proposed_transactions::<_, _, Infallible, _, _>(
&mut db_data,
&network,
&prover,
&prover,
&usk,
OvkPolicy::Sender,
&proposal,
)
.map_err(|e| anyhow!("Error while creating transactions: {}", e))?;
Ok(utils::rust_vec_to_java(
env,
txids.into(),
"[B",
|env, txid| utils::rust_bytes_to_java(env, txid.as_ref()),
|env| env.new_byte_array(32),
)?
.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_branchIdForHeight<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
height: jlong,
network_id: jint,
) -> jlong {
let res = panic::catch_unwind(|| {
let _span = tracing::info_span!("RustBackend.branchIdForHeight").entered();
let network = parse_network(network_id as u32)?;
let branch: BranchId = BranchId::for_height(&network, BlockHeight::from(height as u32));
let branch_id: u32 = u32::from(branch);
debug!(
"For height {} found consensus branch {:?} with id {}",
height, branch, branch_id
);
Ok(branch_id.into())
});
unwrap_exc_or(&mut env, res, -1)
}
//
// Utility functions
//
fn parse_network(value: u32) -> anyhow::Result<Network> {
match value {
0 => Ok(TestNetwork),
1 => Ok(MainNetwork),
_ => Err(anyhow!("Invalid network type: {}. Expected either 0 or 1 for Testnet or Mainnet, respectively.", value))
}
}
/// Returns a list of the transparent receivers for the diversified unified addresses that have
/// been allocated for the provided account.
///
/// # Safety
///
/// - `db_data` must be non-null and valid for reads for `db_data_len` bytes, and it must have an
/// alignment of `1`. Its contents must be a string representing a valid system path in the
/// operating system's preferred representation.
/// - The memory referenced by `db_data` must not be mutated for the duration of the function call.
/// - The total size `db_data_len` must be no larger than `isize::MAX`. See the safety
/// documentation of pointer::offset.
/// - Call [`zcashlc_free_keys`] to free the memory associated with the returned pointer
/// when done using it.
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_listTransparentReceivers<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
account_id: jint,
network_id: jint,
) -> jobjectArray {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.listTransparentReceivers").entered();
let network = parse_network(network_id as u32)?;
let zcash_network = network.network_type();
let db_data = wallet_db(env, network, db_data)?;
let account = account_id_from_jni(&db_data, account_id)?;
match db_data.get_transparent_receivers(account) {
Ok(receivers) => {
let trasparent_receivers = receivers
.iter()
.map(|(taddr, _)| {
let taddr = match taddr {
TransparentAddress::PublicKeyHash(data) => {
ZcashAddress::from_transparent_p2pkh(zcash_network, *data)
}
TransparentAddress::ScriptHash(data) => {
ZcashAddress::from_transparent_p2sh(zcash_network, *data)
}
};
taddr.encode()
})
.collect::<Vec<_>>();
Ok(utils::rust_vec_to_java(
env,
trasparent_receivers,
"java/lang/String",
|env, taddr| env.new_string(taddr),
|env| env.new_string(""),
)?
.into_raw())
}
Err(e) => Err(anyhow!("Error while fetching address: {}", e)),
}
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}