Add Zcash batch signing for shielded PCZTs
What changed, and why it matters
This commit adds a new Zcash batch-signing feature for shielded transactions and fixes a UI bug where Zcash signing could freeze if the response QR code could not be generated. The changes introduce new Rust code to validate, parse, and sign batches of Zcash PCZT messages, plus new C UI screens to review and approve those batches. It also tightens error handling so that QR/USB generation failures are reported instead of leaving the device stuck. There is no direct evidence in the commit of an exploitable vulnerability, but the size and complexity of the new signing path means it should be reviewed carefully.
Treat this as a feature addition with embedded bug fix rather than a confirmed vulnerability. Review the new batch signing path for memory safety across the Rust/C FFI boundary, ensure the 35-message RAM budget is enforced under adversarial payload sizes, verify that seed zeroization happens on all error paths, and confirm that the `encode_full_response` single-UR path cannot be abused to bypass QR fragmentation limits. Regression-test the QR-generation failure fix and the new batch UI flows before release.
Security signals we found
New batch signing path for Zcash shielded PCZTs with 35-message limit
Atomic batch semantics: any invalid message aborts the whole batch
Preflight check `ensure_pczt_has_signable_shielded_action` prevents silent no-signature results
Postflight check `ensure_signable_shielded_actions_are_signed` verifies signatures were inserted
Batch mode rejects transparent inputs and Sapling spends/outputs
Payload digest verification and duplicate ID/payload detection in batch validation
UR response generation failure now emits an error signal instead of stalling
New unsafe FFI functions exposed for batch check/parse/sign
Dependency switched from crates.io `ur-registry` to a Git fork for new registry types
Evidence from the diff
The commit implements Zcash batch signing for shielded PCZTs under the cypherpunk firmware variant. Key technical changes: (1) new ur-registry fork dependency for zcash-sign-batch/zcash-sign-result UR types; (2) Rust validation in rust/rust_c/src/zcash/mod.rs enforcing version 1, mainnet, atomic=true, 1-35 messages, unique IDs/payloads, and payload digest verification; (3) Rust logic in rust/apps/zcash/src/lib.rs to identify signable Orchard/Ironwood actions, reject Sapling/transparent inputs in batch mode, and verify signatures were actually produced after signing; (4) new C UI views/widgets for reviewing each transaction in the batch and signing with password; (5) encode_full_response helper to emit a single unfragmented UR for USB responses; (6) fix for stalled signing by emitting SIG_BACKGROUND_UR_GENERATE_FAIL when UR generation fails and handling NULL results; (7) simulator QR-scan memory cleanup improvements. The code appears defensive (atomic batches, explicit preflight/postflight checks, duplicate detection, digest verification), but the large diff and new unsafe FFI/C integration warrant further scrutiny.
Changed components
rust/apps/zcash/src/lib.rsrust/rust_c/src/zcash/mod.rsrust/rust_c/src/zcash/structs.rsrust/rust_c/src/common/ur.rsrust/rust_c/src/common/ur_ext.rssrc/ui/gui_chain/multi/gui_zcash.csrc/ui/gui_chain/multi/gui_zcash.hsrc/ui/gui_views/multi/cypherpunk/gui_zcash_batch_view.csrc/ui/gui_widgets/multi/cypherpunk/gui_zcash_batch_widgets.csrc/ui/gui_widgets/multi/cypherpunk/gui_zcash_batch_widgets.hsrc/ui/gui_model/gui_model.csrc/ui/gui_widgets/gui_transaction_signature_widgets.crust/Cargo.tomlrust/Cargo.lockInspect captured patch +2201 / −52
diff --git a/CHANGELOG-ZH.md b/CHANGELOG-ZH.md
index 0a7f0f8..4bd7332 100644
--- a/CHANGELOG-ZH.md
+++ b/CHANGELOG-ZH.md
@@ -1,3 +1,16 @@
+## 未发布
+
+**Cypherpunk:**
+
+### 新增
+
+1. 支持 Zcash 批量 PCZT 签名
+
+### Bug 修复
+
+1. 修复响应二维码生成失败时 Zcash 签名卡住的问题
+
+
## 2.4.6 (2026-6-12)
**Web3:**
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef85bd0..c4e9ecc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+## Unreleased
+
+**Cypherpunk:**
+
+### What's new
+
+1. Added support for Zcash batch PCZT signing
+
+### Bug Fixes
+
+1. Fixed stalled Zcash signing when response QR generation fails
+
+
## 2.4.6 (2026-6-12)
**Web3:**
diff --git a/docs/protocols/ur_registrys/zcash.md b/docs/protocols/ur_registrys/zcash.md
index ce8b83d..3204315 100644
--- a/docs/protocols/ur_registrys/zcash.md
+++ b/docs/protocols/ur_registrys/zcash.md
@@ -23,7 +23,7 @@ UFVK is a standard account expression format in Zcash as per [ZIP-316](https://z
3. Sapling
4. Orchard
-This protocol focuses on the Transparent and Orchard components.
+This protocol focuses on the Transparent, Orchard, and Ironwood components.
#### CDDL for Zcash Accounts
@@ -52,3 +52,74 @@ zcash-pczt {
data: bytes, ; Zcash PCZT, signatures inserted after signing.
}
```
+
+### Zcash Batch Signing
+
+`zcash-sign-batch` wraps multiple signing messages into one Keystone approval.
+Version 1 is supported by cypherpunk firmware and currently supports up to 35
+mainnet PCZT messages. It requires `atomic` to be `true`; if any message is
+invalid or cannot be signed, Keystone returns an error instead of a partial
+result. Batch PCZT entries must be fully Keystone-owned spends from supported
+shielded pools, currently Orchard or Ironwood. Transparent inputs and Sapling
+spends or outputs are rejected.
+
+The 35-message limit is the current batch memory budget for `pczt-v1`. A full
+35-message batch using the supported PCZT message shape was measured at about
+35% RAM on target hardware, so this version does not define separate byte caps
+for request ids, message ids, or payloads. Revisit the limit if new message
+kinds or substantially larger payload encodings are added.
+
+Message kinds:
+
+```cddl
+pczt-v1 = 1
+```
+
+Networks:
+
+```cddl
+zcash-mainnet = 1
+```
+
+Result statuses:
+
+```cddl
+signed = 0
+```
+
+#### CDDL for Zcash Sign Batch
+
+```cddl
+zcash-sign-batch = {
+ 1: uint, ; version. Must be 1.
+ 2: bytes, ; request id. Echoed by zcash-sign-result.
+ 3: uint, ; network. Must be zcash-mainnet.
+ 4: [1*35 zcash-sign-message],
+ ?11: bool, ; atomic. Defaults to true. Must be true.
+}
+
+zcash-sign-message = {
+ 1: bytes, ; caller-defined message id. Must be unique.
+ 2: uint, ; message kind. Must be pczt-v1.
+ 3: bytes, ; message payload. For pczt-v1 this is raw PCZT bytes. Must be unique in the batch.
+ ?6: bytes.32, ; SHA-256 of payload.
+}
+```
+
+#### CDDL for Zcash Sign Result
+
+```cddl
+zcash-sign-result = {
+ 1: uint, ; version. Matches request version.
+ 2: bytes, ; request id from zcash-sign-batch.
+ 3: [1*35 zcash-sign-message-result],
+}
+
+zcash-sign-message-result = {
+ 1: bytes, ; message id from zcash-sign-message.
+ 2: uint, ; status. signed = 0.
+ 3: uint, ; message kind from zcash-sign-message.
+ 4: bytes, ; signed payload. For pczt-v1 this is signed PCZT bytes.
+ 6: bytes.32, ; SHA-256 of signed payload.
+}
+```
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 0a50f41..83a2bd1 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -4811,8 +4811,7 @@ dependencies = [
[[package]]
name = "ur-registry"
version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c09637b609337d361420befd9a218a0ebf6923c10a56b8d3c895419973cd71c"
+source = "git+https://github.com/valargroup/keystone-sdk-rust.git?rev=630b7cd4c92a3473b472ecbe8a7030fed16ce2c4#630b7cd4c92a3473b472ecbe8a7030fed16ce2c4"
dependencies = [
"bs58",
"hex",
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index f03bd8a..71059b6 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -130,3 +130,5 @@ zcash_keys = { git = "https://github.com/valargroup/librustzcash", rev = "dc95dc
zcash_primitives = { git = "https://github.com/valargroup/librustzcash", rev = "dc95dcef33a081b925db551eac8bf6533fff22ed" }
zcash_protocol = { git = "https://github.com/valargroup/librustzcash", rev = "dc95dcef33a081b925db551eac8bf6533fff22ed" }
zcash_transparent = { git = "https://github.com/valargroup/librustzcash", rev = "dc95dcef33a081b925db551eac8bf6533fff22ed" }
+# Use the SDK fork for Zcash batch registry types until they are available from the published crate.
+ur-registry = { git = "https://github.com/valargroup/keystone-sdk-rust.git", rev = "630b7cd4c92a3473b472ecbe8a7030fed16ce2c4" }
diff --git a/rust/apps/zcash/src/lib.rs b/rust/apps/zcash/src/lib.rs
index e6469b4..aacc11b 100644
--- a/rust/apps/zcash/src/lib.rs
+++ b/rust/apps/zcash/src/lib.rs
@@ -18,9 +18,12 @@ use zcash_vendor::{
zip32,
};
-#[cfg(any(test, feature = "multi_coins"))]
+#[cfg(any(test, feature = "multi_coins", feature = "cypherpunk"))]
use zcash_vendor::pczt::Pczt;
+#[cfg(feature = "cypherpunk")]
+use zcash_vendor::zcash_protocol::consensus::NetworkConstants;
+
/// Generates a Zcash address from a Unified Full Viewing Key (UFVK).
///
/// # Parameters
@@ -324,6 +327,327 @@ mod legacy_tests {
}
}
+#[cfg(feature = "cypherpunk")]
+fn map_shielded_verifier_error(
+ e: zcash_vendor::pczt::roles::verifier::OrchardError<ZcashError>,
+) -> ZcashError {
+ use zcash_vendor::pczt::roles::verifier::OrchardError;
+
+ match e {
+ OrchardError::Custom(e) => e,
+ _ => ZcashError::InvalidPczt(alloc::format!("{e:?}")),
+ }
+}
+
+#[cfg(feature = "cypherpunk")]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum SignableShieldedPool {
+ Orchard,
+ #[cfg(zcash_unstable = "nu6.3")]
+ Ironwood,
+}
+
+#[cfg(feature = "cypherpunk")]
+impl SignableShieldedPool {
+ fn label(self) -> &'static str {
+ match self {
+ SignableShieldedPool::Orchard => "Orchard",
+ #[cfg(zcash_unstable = "nu6.3")]
+ SignableShieldedPool::Ironwood => "Ironwood",
+ }
+ }
+
+ fn shielded_pool(self) -> pczt::ShieldedPool {
+ match self {
+ SignableShieldedPool::Orchard => pczt::ShieldedPool::Orchard,
+ #[cfg(zcash_unstable = "nu6.3")]
+ SignableShieldedPool::Ironwood => pczt::ShieldedPool::Ironwood,
+ }
+ }
+}
+
+#[cfg(feature = "cypherpunk")]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+struct SignableShieldedAction {
+ pool: SignableShieldedPool,
+ index: usize,
+}
+
+#[cfg(feature = "cypherpunk")]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum ShieldedActionPolicy {
+ Batch,
+ Single,
+}
+
+#[cfg(feature = "cypherpunk")]
+fn reject_unsupported_batch_pczt(pczt: &Pczt) -> Result<()> {
+ if !pczt.sapling().spends().is_empty() || !pczt.sapling().outputs().is_empty() {
+ return Err(ZcashError::InvalidPczt(
+ "Zcash batch PCZT must not contain Sapling spends or outputs".to_string(),
+ ));
+ }
+
+ if !pczt.transparent().inputs().is_empty() {
+ return Err(ZcashError::InvalidPczt(
+ "Zcash batch PCZT must not contain transparent inputs".to_string(),
+ ));
+ }
+
+ Ok(())
+}
+
+#[cfg(feature = "cypherpunk")]
+fn collect_signable_shielded_actions<P: consensus::Parameters>(
+ params: &P,
+ bundle: &zcash_vendor::orchard::pczt::Bundle,
+ pool: SignableShieldedPool,
+ seed_fingerprint: &[u8; 32],
+ account_index: zip32::AccountId,
+ policy: ShieldedActionPolicy,
+ actions: &mut Vec<SignableShieldedAction>,
+) -> core::result::Result<(), zcash_vendor::pczt::roles::verifier::OrchardError<ZcashError>> {
+ use zcash_vendor::pczt::roles::verifier::OrchardError;
+
+ for (index, action) in bundle.actions().iter().enumerate() {
+ if action.spend().dummy_sk().is_some() {
+ continue;
+ }
+
+ let value = action.spend().value().ok_or_else(|| {
+ OrchardError::Custom(ZcashError::InvalidPczt(alloc::format!(
+ "missing {} spend value",
+ pool.label(),
+ )))
+ })?;
+ if value.inner() == 0 {
+ continue;
+ }
+
+ let matches_account = pczt::matching_seed_supported_orchard_account(
+ seed_fingerprint,
+ action.spend().zip32_derivation().as_ref(),
+ params.network_type().coin_type(),
+ pool.shielded_pool(),
+ )
+ .map_err(OrchardError::Custom)?
+ == Some(account_index);
+ if !matches_account {
+ if policy == ShieldedActionPolicy::Batch {
+ return Err(OrchardError::Custom(ZcashError::PcztNoMyInputs));
+ }
+ continue;
+ }
+
+ actions.push(SignableShieldedAction { pool, index });
+ }
+
+ Ok(())
+}
+
+#[cfg(feature = "cypherpunk")]
+fn ensure_actions_are_signed(
+ bundle: &zcash_vendor::orchard::pczt::Bundle,
+ pool: SignableShieldedPool,
+ signable_actions: &[SignableShieldedAction],
+) -> core::result::Result<(), zcash_vendor::pczt::roles::verifier::OrchardError<ZcashError>> {
+ use zcash_vendor::pczt::roles::verifier::OrchardError;
+
+ for action_ref in signable_actions.iter().filter(|action| action.pool == pool) {
+ let action = bundle.actions().get(action_ref.index).ok_or_else(|| {
+ OrchardError::Custom(ZcashError::SigningError(alloc::format!(
+ "signed PCZT is missing an {} action",
+ pool.label(),
+ )))
+ })?;
+ if action.spend().spend_auth_sig().is_none() {
+ return Err(OrchardError::Custom(ZcashError::SigningError(
+ alloc::format!(
+ "signed PCZT is missing an {} spend authorization signature",
+ pool.label(),
+ ),
+ )));
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(feature = "cypherpunk")]
+fn signable_shielded_actions<P: consensus::Parameters>(
+ params: &P,
+ pczt: Pczt,
+ seed_fingerprint: &[u8; 32],
+ account_index: zip32::AccountId,
+ policy: ShieldedActionPolicy,
+) -> Result<Vec<SignableShieldedAction>> {
+ use zcash_vendor::pczt::roles::verifier::Verifier;
+
+ if policy == ShieldedActionPolicy::Batch {
+ reject_unsupported_batch_pczt(&pczt)?;
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ let should_process_ironwood = pczt::pczt_should_process_ironwood(&pczt);
+ let mut actions = Vec::new();
+ let verifier = Verifier::new(pczt)
+ .with_orchard::<ZcashError, _>(|bundle| {
+ collect_signable_shielded_actions(
+ params,
+ bundle,
+ SignableShieldedPool::Orchard,
+ seed_fingerprint,
+ account_index,
+ policy,
+ &mut actions,
+ )
+ })
+ .map_err(map_shielded_verifier_error)?;
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ let verifier = if should_process_ironwood {
+ verifier
+ .with_ironwood::<ZcashError, _>(|bundle| {
+ collect_signable_shielded_actions(
+ params,
+ bundle,
+ SignableShieldedPool::Ironwood,
+ seed_fingerprint,
+ account_index,
+ policy,
+ &mut actions,
+ )
+ })
+ .map_err(map_shielded_verifier_error)?
+ } else {
+ verifier
+ };
+ drop(verifier);
+
+ Ok(actions)
+}
+
+#[cfg(feature = "cypherpunk")]
+fn ensure_shielded_actions_are_signed(
+ signed_pczt: Pczt,
+ signable_actions: &[SignableShieldedAction],
+) -> Result<()> {
+ use zcash_vendor::pczt::roles::verifier::Verifier;
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ let should_process_ironwood = pczt::pczt_should_process_ironwood(&signed_pczt);
+ let verifier = Verifier::new(signed_pczt)
+ .with_orchard::<ZcashError, _>(|bundle| {
+ ensure_actions_are_signed(bundle, SignableShieldedPool::Orchard, signable_actions)
+ })
+ .map_err(map_shielded_verifier_error)?;
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ let verifier = if should_process_ironwood {
+ verifier
+ .with_ironwood::<ZcashError, _>(|bundle| {
+ ensure_actions_are_signed(bundle, SignableShieldedPool::Ironwood, signable_actions)
+ })
+ .map_err(map_shielded_verifier_error)?
+ } else {
+ verifier
+ };
+ drop(verifier);
+
+ Ok(())
+}
+
+/// Checks whether the PCZT contains at least one non-dummy supported shielded
+/// action that can be signed by the account identified by `seed_fingerprint` and
+/// `account_index`.
+///
+/// `sign_pczt` intentionally returns a redacted PCZT even when no key matched.
+/// Batch signing needs this explicit preflight so one approval cannot silently
+/// produce a result with zero shielded signatures for an entry.
+#[cfg(feature = "cypherpunk")]
+pub fn ensure_pczt_has_signable_shielded_action<P: consensus::Parameters>(
+ params: &P,
+ pczt: &[u8],
+ seed_fingerprint: &[u8; 32],
+ account_index: u32,
+) -> Result<()> {
+ let pczt = pczt::parse_pczt(pczt)?;
+ let account_index = zip32::AccountId::try_from(account_index)
+ .map_err(|_e| ZcashError::InvalidDataError("invalid account index".to_string()))?;
+
+ if signable_shielded_actions(
+ params,
+ pczt,
+ seed_fingerprint,
+ account_index,
+ ShieldedActionPolicy::Batch,
+ )?
+ .is_empty()
+ {
+ Err(ZcashError::PcztNoMyInputs)
+ } else {
+ Ok(())
+ }
+}
+
+/// Confirms that every signable supported shielded action in `unsigned_pczt`
+/// has a spend authorization signature in the same position in `signed_pczt`.
+#[cfg(feature = "cypherpunk")]
+pub fn ensure_signable_shielded_actions_are_signed<P: consensus::Parameters>(
+ params: &P,
+ unsigned_pczt: &[u8],
+ signed_pczt: &[u8],
+ seed_fingerprint: &[u8; 32],
+ account_index: u32,
+) -> Result<()> {
+ let unsigned_pczt = pczt::parse_pczt(unsigned_pczt)?;
+ let account_index = zip32::AccountId::try_from(account_index)
+ .map_err(|_e| ZcashError::InvalidDataError("invalid account index".to_string()))?;
+ let signable_actions = signable_shielded_actions(
+ params,
+ unsigned_pczt,
+ seed_fingerprint,
+ account_index,
+ ShieldedActionPolicy::Batch,
+ )?;
+ if signable_actions.is_empty() {
+ Err(ZcashError::PcztNoMyInputs)
+ } else {
+ let signed_pczt = pczt::parse_pczt(signed_pczt)
+ .map_err(|_| ZcashError::InvalidPczt("invalid signed pczt data".to_string()))?;
+ ensure_shielded_actions_are_signed(signed_pczt, &signable_actions)
+ }
+}
+
+/// Confirms that supported shielded actions owned by this account were signed
+/// without applying the batch-only shielded input policy to ordinary PCZTs.
+#[cfg(feature = "cypherpunk")]
+pub fn ensure_owned_supported_shielded_actions_are_signed<P: consensus::Parameters>(
+ params: &P,
+ unsigned_pczt: &[u8],
+ signed_pczt: &[u8],
+ seed_fingerprint: &[u8; 32],
+ account_index: u32,
+) -> Result<()> {
+ let unsigned_pczt = pczt::parse_pczt(unsigned_pczt)?;
+ let account_index = zip32::AccountId::try_from(account_index)
+ .map_err(|_e| ZcashError::InvalidDataError("invalid account index".to_string()))?;
+ let signable_actions = signable_shielded_actions(
+ params,
+ unsigned_pczt,
+ seed_fingerprint,
+ account_index,
+ ShieldedActionPolicy::Single,
+ )?;
+ if signable_actions.is_empty() {
+ Ok(())
+ } else {
+ let signed_pczt = pczt::parse_pczt(signed_pczt)
+ .map_err(|_| ZcashError::InvalidPczt("invalid signed pczt data".to_string()))?;
+ ensure_shielded_actions_are_signed(signed_pczt, &signable_actions)
+ }
+}
+
#[cfg(feature = "cypherpunk")]
#[cfg(test)]
mod tests {
@@ -825,4 +1149,176 @@ mod tests {
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ZcashError::InvalidPczt(_)));
}
+
+ const BATCH_UNSUPPORTED_SAPLING_ERROR: &str =
+ "Zcash batch PCZT must not contain Sapling spends or outputs";
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ fn pczt_with_sapling_output() -> pczt::test_support::SamplePczt {
+ let mut sample = pczt::test_support::sample_orchard_change_pczt();
+ let mut pczt: PcztMirror = postcard::from_bytes(&sample.bytes[8..]).unwrap();
+ pczt.sapling.outputs.push(SaplingOutputMirror {
+ cv: [0; 32],
+ cmu: [0; 32],
+ ephemeral_key: [0; 32],
+ enc_ciphertext: Vec::new(),
+ out_ciphertext: Vec::new(),
+ zkproof: None,
+ recipient: None,
+ value: Some(1),
+ rseed: None,
+ rcv: None,
+ ock: None,
+ zip32_derivation: None,
+ user_address: None,
+ proprietary: BTreeMap::new(),
+ });
+ pczt.sapling.value_sum = -1;
+
+ sample.bytes.truncate(8);
+ sample.bytes = postcard::to_extend(&pczt, sample.bytes).unwrap();
+ sample
+ }
+
+ fn assert_batch_unsupported_sapling_error<T: core::fmt::Debug>(result: Result<T>) {
+ assert_eq!(
+ result.unwrap_err(),
+ ZcashError::InvalidPczt(BATCH_UNSUPPORTED_SAPLING_ERROR.to_string())
+ );
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_batch_preflight_accepts_orchard_spend() {
+ let sample = pczt::test_support::sample_orchard_change_pczt();
+
+ ensure_pczt_has_signable_shielded_action(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .unwrap();
+ assert_eq!(
+ ensure_pczt_has_signable_shielded_action(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.seed_fingerprint,
+ 1,
+ )
+ .unwrap_err(),
+ ZcashError::PcztNoMyInputs
+ );
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_batch_postflight_confirms_orchard_signature() {
+ let sample = pczt::test_support::sample_orchard_change_pczt();
+ let signed = sign_pczt(&sample.bytes, &sample.seed).expect("Orchard PCZT should sign");
+
+ ensure_signable_shielded_actions_are_signed(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &signed,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .unwrap();
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_single_postflight_confirms_orchard_signature_when_present() {
+ let sample = pczt::test_support::sample_orchard_change_pczt();
+
+ assert!(matches!(
+ ensure_owned_supported_shielded_actions_are_signed(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.bytes,
+ &sample.seed_fingerprint,
+ 0,
+ ),
+ Err(ZcashError::SigningError(message))
+ if message == "signed PCZT is missing an Orchard spend authorization signature"
+ ));
+
+ let signed = sign_pczt(&sample.bytes, &sample.seed).expect("Orchard PCZT should sign");
+ ensure_owned_supported_shielded_actions_are_signed(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &signed,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .unwrap();
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_batch_preflight_rejects_sapling_outputs() {
+ let sample = pczt_with_sapling_output();
+
+ assert_batch_unsupported_sapling_error(ensure_pczt_has_signable_shielded_action(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.seed_fingerprint,
+ 0,
+ ));
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_batch_postflight_rejects_sapling_outputs() {
+ let sample = pczt_with_sapling_output();
+
+ assert_batch_unsupported_sapling_error(ensure_signable_shielded_actions_are_signed(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.bytes,
+ &sample.seed_fingerprint,
+ 0,
+ ));
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_batch_preflight_accepts_ironwood_spend() {
+ let sample = pczt::test_support::sample_ironwood_pczt();
+
+ ensure_pczt_has_signable_shielded_action(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .unwrap();
+ assert_eq!(
+ ensure_pczt_has_signable_shielded_action(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.seed_fingerprint,
+ 1,
+ )
+ .unwrap_err(),
+ ZcashError::PcztNoMyInputs
+ );
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_batch_postflight_confirms_ironwood_signature() {
+ let sample = pczt::test_support::sample_ironwood_pczt();
+ let signed = sign_pczt(&sample.bytes, &sample.seed).expect("Ironwood PCZT should sign");
+
+ ensure_signable_shielded_actions_are_signed(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &signed,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .unwrap();
+ }
}
diff --git a/rust/rust_c/src/common/ur.rs b/rust/rust_c/src/common/ur.rs
index 4ad15d6..3d8a421 100644
--- a/rust/rust_c/src/common/ur.rs
+++ b/rust/rust_c/src/common/ur.rs
@@ -73,6 +73,8 @@ use ur_registry::ton::ton_sign_request::TonSignRequest;
use ur_registry::tron::tron_sign_request::TronSignRequest;
#[cfg(feature = "zcash")]
use ur_registry::zcash::zcash_pczt::ZcashPczt;
+#[cfg(feature = "zcash_cypherpunk")]
+use ur_registry::zcash::zcash_sign_batch::ZcashSignBatch;
use super::errors::{ErrorCodes, RustCError};
use super::free::Free;
@@ -132,12 +134,22 @@ impl UREncodeResult {
}
}
- pub fn encode(data: Vec<u8>, tag: String, max_fragment_length: usize) -> Self {
+ fn encode_with_multipart_policy(
+ data: Vec<u8>,
+ tag: String,
+ max_fragment_length: usize,
+ allow_multipart: bool,
+ ) -> Self {
let result =
ur_parse_lib::keystone_ur_encoder::probe_encode(&data, max_fragment_length, tag);
match result {
Ok(result) => {
if result.is_multi_part {
+ if !allow_multipart {
+ return Self::from(RustCError::UnsupportedTransaction(
+ "encoded UR is too large for a single USB response".to_string(),
+ ));
+ }
match result.encoder {
Some(v) => Self::multi(result.data.to_uppercase(), v),
None => Self::from(RustCError::UnexpectedError(
@@ -151,6 +163,15 @@ impl UREncodeResult {
Err(e) => Self::from(e),
}
}
+
+ pub fn encode(data: Vec<u8>, tag: String, max_fragment_length: usize) -> Self {
+ Self::encode_with_multipart_policy(data, tag, max_fragment_length, true)
+ }
+
+ pub fn encode_full_response(data: Vec<u8>, tag: String) -> Self {
+ let max_fragment_length = data.len().max(1);
+ Self::encode_with_multipart_policy(data, tag, max_fragment_length, false)
+ }
}
impl Free for UREncodeResult {
@@ -277,6 +298,8 @@ pub enum ViewType {
TonSignProof,
#[cfg(feature = "zcash")]
ZcashTx,
+ #[cfg(feature = "zcash_cypherpunk")]
+ ZcashBatchTx,
#[cfg(feature = "aptos")]
AptosTx,
#[cfg(feature = "monero")]
@@ -365,6 +388,8 @@ pub enum QRCodeType {
AvaxSignRequest,
#[cfg(feature = "zcash")]
ZcashPczt,
+ #[cfg(feature = "zcash_cypherpunk")]
+ ZcashSignBatch,
#[cfg(feature = "monero")]
XmrOutputSignRequest,
#[cfg(feature = "monero")]
@@ -433,6 +458,8 @@ impl QRCodeType {
InnerURType::TonSignRequest(_) => Ok(QRCodeType::TonSignRequest),
#[cfg(feature = "zcash")]
InnerURType::ZcashPczt(_) => Ok(QRCodeType::ZcashPczt),
+ #[cfg(feature = "zcash_cypherpunk")]
+ InnerURType::ZcashSignBatch(_) => Ok(QRCodeType::ZcashSignBatch),
#[cfg(feature = "monero")]
InnerURType::XmrTxUnsigned(_) => Ok(QRCodeType::XmrTxUnsignedRequest),
#[cfg(feature = "monero")]
@@ -484,12 +511,7 @@ impl URParseResult {
}
}
- pub fn multi(
- progress: u32,
- t: ViewType,
- ur_type: QRCodeType,
- decoder: KeystoneURDecoder,
- ) -> Self {
+ fn multi(progress: u32, t: ViewType, ur_type: QRCodeType, decoder: KeystoneURDecoder) -> Self {
let _self = Self::new();
let decoder = Box::into_raw(Box::new(decoder)) as PtrUR;
Self {
@@ -606,6 +628,10 @@ unsafe fn free_ur(ur_type: &QRCodeType, data: PtrUR) {
QRCodeType::CardanoCatalystVotingRegistrationRequest => {
free_ptr_with_type!(data, CardanoCatalystVotingRegistrationRequest);
}
+ #[cfg(feature = "zcash_cypherpunk")]
+ QRCodeType::ZcashSignBatch => {
+ free_ptr_with_type!(data, ZcashSignBatch);
+ }
#[cfg(feature = "monero")]
QRCodeType::XmrOutputSignRequest => {
free_ptr_with_type!(data, XmrOutput);
@@ -687,6 +713,55 @@ impl Free for URParseMultiResult {
impl_response!(URParseMultiResult);
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_encode_full_response_returns_complete_ur_when_capped_encoder_would_fragment() {
+ let capped_result = UREncodeResult::encode(
+ vec![0; FRAGMENT_UNLIMITED_LENGTH + 1],
+ "bytes".to_string(),
+ FRAGMENT_UNLIMITED_LENGTH,
+ );
+ assert_eq!(capped_result.error_code, ErrorCodes::Success as u32);
+ assert!(capped_result.is_multi_part);
+ let capped_data = unsafe { recover_c_char(capped_result.data) };
+ assert!(capped_data.contains("/1-"));
+ unsafe {
+ capped_result.free();
+ }
+
+ let full_result = UREncodeResult::encode_full_response(
+ vec![0; FRAGMENT_UNLIMITED_LENGTH + 1],
+ "bytes".to_string(),
+ );
+ assert_eq!(full_result.error_code, ErrorCodes::Success as u32);
+ assert!(!full_result.is_multi_part);
+ assert!(full_result.encoder.is_null());
+ let full_data = unsafe { recover_c_char(full_result.data) };
+ assert!(full_data.starts_with("UR:BYTES/"));
+ assert!(!full_data.contains("/1-"));
+ unsafe {
+ full_result.free();
+ }
+
+ let zcash_result = UREncodeResult::encode_full_response(
+ vec![0; FRAGMENT_UNLIMITED_LENGTH + 1],
+ "zcash-sign-result".to_string(),
+ );
+ assert_eq!(zcash_result.error_code, ErrorCodes::Success as u32);
+ assert!(!zcash_result.is_multi_part);
+ assert!(zcash_result.encoder.is_null());
+ let zcash_data = unsafe { recover_c_char(zcash_result.data) };
+ assert!(zcash_data.starts_with("UR:ZCASH-SIGN-RESULT/"));
+ assert!(!zcash_data.contains("/1-"));
+ unsafe {
+ zcash_result.free();
+ }
+ }
+}
+
fn get_ur_type(ur: &String) -> Result<QRCodeType, URError> {
let t = ur_parse_lib::keystone_ur_decoder::get_type(ur)?;
QRCodeType::from(&t)
@@ -794,6 +869,8 @@ pub fn decode_ur(ur: String) -> URParseResult {
QRCodeType::TonSignRequest => _decode_ur::<TonSignRequest>(ur, ur_type),
#[cfg(feature = "zcash")]
QRCodeType::ZcashPczt => _decode_ur::<ZcashPczt>(ur, ur_type),
+ #[cfg(feature = "zcash_cypherpunk")]
+ QRCodeType::ZcashSignBatch => _decode_ur::<ZcashSignBatch>(ur, ur_type),
#[cfg(feature = "monero")]
QRCodeType::XmrOutputSignRequest => _decode_ur::<XmrOutput>(ur, ur_type),
#[cfg(feature = "monero")]
@@ -906,6 +983,8 @@ fn receive_ur(ur: String, decoder: &mut KeystoneURDecoder) -> URParseMultiResult
QRCodeType::TonSignRequest => _receive_ur::<TonSignRequest>(ur, ur_type, decoder),
#[cfg(feature = "zcash")]
QRCodeType::ZcashPczt => _receive_ur::<ZcashPczt>(ur, ur_type, decoder),
+ #[cfg(feature = "zcash_cypherpunk")]
+ QRCodeType::ZcashSignBatch => _receive_ur::<ZcashSignBatch>(ur, ur_type, decoder),
#[cfg(feature = "monero")]
QRCodeType::XmrOutputSignRequest => _receive_ur::<XmrOutput>(ur, ur_type, decoder),
#[cfg(feature = "monero")]
diff --git a/rust/rust_c/src/common/ur_ext.rs b/rust/rust_c/src/common/ur_ext.rs
index 695c60a..38c1215 100644
--- a/rust/rust_c/src/common/ur_ext.rs
+++ b/rust/rust_c/src/common/ur_ext.rs
@@ -67,6 +67,8 @@ use ur_registry::ton::ton_sign_request::{DataType, TonSignRequest};
use ur_registry::tron::tron_sign_request::TronSignRequest;
#[cfg(feature = "zcash")]
use ur_registry::zcash::zcash_pczt::ZcashPczt;
+#[cfg(feature = "zcash_cypherpunk")]
+use ur_registry::zcash::zcash_sign_batch::ZcashSignBatch;
use super::ur::ViewType;
@@ -223,6 +225,13 @@ impl InferViewType for ZcashPczt {
}
}
+#[cfg(feature = "zcash_cypherpunk")]
+impl InferViewType for ZcashSignBatch {
+ fn infer(&self) -> Result<ViewType, URError> {
+ Ok(ViewType::ZcashBatchTx)
+ }
+}
+
#[cfg(feature = "avalanche")]
impl InferViewType for AvaxSignRequest {
fn infer(&self) -> Result<ViewType, URError> {
diff --git a/rust/rust_c/src/zcash/mod.rs b/rust/rust_c/src/zcash/mod.rs
index aeff106..c79ff52 100644
--- a/rust/rust_c/src/zcash/mod.rs
+++ b/rust/rust_c/src/zcash/mod.rs
@@ -5,7 +5,7 @@ use crate::common::{
free::Free,
structs::{SimpleResponse, TransactionCheckResult, TransactionParseResult},
types::{Ptr, PtrBytes, PtrString, PtrT, PtrUR},
- ur::{UREncodeResult, FRAGMENT_MAX_LENGTH_DEFAULT},
+ ur::{UREncodeResult, FRAGMENT_MAX_LENGTH_DEFAULT, FRAGMENT_UNLIMITED_LENGTH},
utils::{convert_c_char, recover_c_char},
};
use crate::{extract_array, extract_array_mut};
@@ -20,10 +20,23 @@ use keystore::algorithms::{
zcash::{calculate_seed_fingerprint, derive_ufvk},
};
use structs::DisplayPczt;
-use ur_registry::{traits::RegistryItem, zcash::zcash_pczt::ZcashPczt};
+use structs::DisplayZcashBatch;
+use ur_registry::traits::RegistryItem;
+use ur_registry::zcash::zcash_pczt::ZcashPczt;
+use ur_registry::zcash::zcash_sign_batch::{
+ ZcashSignBatch, ZcashSignMessage, ZCASH_SIGN_BATCH_NETWORK_MAINNET, ZCASH_SIGN_BATCH_VERSION,
+ ZCASH_SIGN_MESSAGE_KIND_PCZT_V1,
+};
+use ur_registry::zcash::zcash_sign_result::{ZcashSignMessageResult, ZcashSignResult};
use zcash_vendor::zcash_protocol::consensus::MainNetwork;
use zeroize::Zeroize;
+// Batch memory is intentionally bounded by message count rather than separate
+// byte caps. With the supported pczt-v1 messages, a full 35-message batch used
+// about 35% of RAM on target hardware. Revisit this if new message kinds or
+// substantially larger payload encodings are added.
+const ZCASH_BATCH_MAX_MESSAGES: usize = 35;
+
#[no_mangle]
pub unsafe extern "C" fn derive_zcash_ufvk(
seed: PtrBytes,
@@ -168,11 +181,382 @@ pub unsafe extern "C" fn parse_zcash_tx_multi_coins(
}
}
+fn validate_zcash_batch(batch: &ZcashSignBatch) -> Result<(), RustCError> {
+ let messages = batch.get_messages();
+ if batch.get_version() != ZCASH_SIGN_BATCH_VERSION {
+ return Err(RustCError::UnsupportedTransaction(format!(
+ "unsupported Zcash batch version {}",
+ batch.get_version()
+ )));
+ }
+ if batch.get_network() != ZCASH_SIGN_BATCH_NETWORK_MAINNET {
+ return Err(RustCError::UnsupportedTransaction(
+ "only Zcash mainnet batch signing is supported".to_string(),
+ ));
+ }
+ if batch.get_request_id().is_empty() {
+ return Err(RustCError::InvalidData(
+ "Zcash batch has no request id".to_string(),
+ ));
+ }
+ if !batch.get_atomic() {
+ return Err(RustCError::UnsupportedTransaction(
+ "Zcash batch signing requires atomic=true".to_string(),
+ ));
+ }
+ if messages.is_empty() {
+ return Err(RustCError::InvalidData(
+ "Zcash batch has no messages".to_string(),
+ ));
+ }
+ if messages.len() > ZCASH_BATCH_MAX_MESSAGES {
+ return Err(RustCError::UnsupportedTransaction(format!(
+ "Zcash batch supports at most {ZCASH_BATCH_MAX_MESSAGES} messages"
+ )));
+ }
+
+ for (index, message) in messages.iter().enumerate() {
+ if message.get_kind() != ZCASH_SIGN_MESSAGE_KIND_PCZT_V1 {
+ return Err(RustCError::UnsupportedTransaction(format!(
+ "unsupported Zcash batch message kind {}",
+ message.get_kind()
+ )));
+ }
+ if message.get_id().is_empty() {
+ return Err(RustCError::InvalidData(format!(
+ "Zcash batch message {index} has no id"
+ )));
+ }
+ if message.get_payload().is_empty() {
+ return Err(RustCError::InvalidData(format!(
+ "Zcash batch message {index} has no payload"
+ )));
+ }
+
+ let digest = sha256(message.get_payload());
+ if let Some(expected_digest) = message.get_payload_digest() {
+ if expected_digest.as_slice() != digest.as_slice() {
+ return Err(RustCError::InvalidData(format!(
+ "Zcash batch message {index} payload digest mismatch"
+ )));
+ }
+ }
+
+ for previous in &messages[..index] {
+ if sha256(previous.get_payload()) == digest {
+ return Err(RustCError::InvalidData(
+ "Zcash batch contains duplicate payloads".to_string(),
+ ));
+ }
+ if previous.get_id() == message.get_id() {
+ return Err(RustCError::InvalidData(
+ "Zcash batch contains duplicate message ids".to_string(),
+ ));
+ }
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(feature = "cypherpunk")]
+fn check_zcash_batch_message_cypherpunk(
+ message: &ZcashSignMessage,
+ ufvk_text: &str,
+ seed_fingerprint: &[u8; 32],
+ account_index: u32,
+) -> app_zcash::errors::Result<()> {
+ app_zcash::check_pczt_cypherpunk(
+ &MainNetwork,
+ message.get_payload(),
+ ufvk_text,
+ seed_fingerprint,
+ account_index,
+ )?;
+ app_zcash::ensure_pczt_has_signable_shielded_action(
+ &MainNetwork,
+ message.get_payload(),
+ seed_fingerprint,
+ account_index,
+ )
+}
+
+#[cfg(feature = "cypherpunk")]
+fn check_zcash_pczt_message_cypherpunk(
+ payload: &[u8],
+ ufvk_text: &str,
+ seed_fingerprint: &[u8; 32],
+ account_index: u32,
+) -> app_zcash::errors::Result<()> {
+ app_zcash::check_pczt_cypherpunk(
+ &MainNetwork,
+ payload,
+ ufvk_text,
+ seed_fingerprint,
+ account_index,
+ )
+}
+
+#[cfg(feature = "cypherpunk")]
+#[no_mangle]
+pub unsafe extern "C" fn check_zcash_batch_tx_cypherpunk(
+ tx: PtrUR,
+ ufvk: PtrString,
+ seed_fingerprint: PtrBytes,
+ account_index: u32,
+ disabled: bool,
+) -> *mut TransactionCheckResult {
+ if disabled {
+ return TransactionCheckResult::from(RustCError::UnsupportedTransaction(
+ "Zcash requires at least 256-bit entropy (use 33-word Shamir shares)".to_string(),
+ ))
+ .c_ptr();
+ }
+ let batch = extract_ptr_with_type!(tx, ZcashSignBatch);
+ let ufvk_text = unsafe { recover_c_char(ufvk) };
+ let seed_fingerprint = extract_array!(seed_fingerprint, u8, 32);
+ let seed_fingerprint = seed_fingerprint.try_into().unwrap();
+
+ if let Err(e) = validate_zcash_batch(batch) {
+ return TransactionCheckResult::from(e).c_ptr();
+ }
+
+ for message in batch.get_messages() {
+ if let Err(e) = check_zcash_batch_message_cypherpunk(
+ message,
+ &ufvk_text,
+ seed_fingerprint,
+ account_index,
+ ) {
+ return TransactionCheckResult::from(e).c_ptr();
+ }
+ }
+
+ TransactionCheckResult::new().c_ptr()
+}
+
+#[cfg(feature = "cypherpunk")]
+#[no_mangle]
+pub unsafe extern "C" fn parse_zcash_batch_tx_cypherpunk(
+ tx: PtrUR,
+ ufvk: PtrString,
+ seed_fingerprint: PtrBytes,
+ account_index: u32,
+ disabled: bool,
+) -> Ptr<TransactionParseResult<DisplayZcashBatch>> {
+ if disabled {
+ return TransactionParseResult::from(RustCError::UnsupportedTransaction(
+ "Zcash requires at least 256-bit entropy (use 33-word Shamir shares)".to_string(),
+ ))
+ .c_ptr();
+ }
+ let batch = extract_ptr_with_type!(tx, ZcashSignBatch);
+ let ufvk_text = unsafe { recover_c_char(ufvk) };
+ let seed_fingerprint = extract_array!(seed_fingerprint, u8, 32);
+ let seed_fingerprint = seed_fingerprint.try_into().unwrap();
+
+ if let Err(e) = validate_zcash_batch(batch) {
+ return TransactionParseResult::from(e).c_ptr();
+ }
+
+ let mut display_items = Vec::new();
+ for message in batch.get_messages() {
+ if let Err(e) = check_zcash_batch_message_cypherpunk(
+ message,
+ &ufvk_text,
+ seed_fingerprint,
+ account_index,
+ ) {
+ return TransactionParseResult::from(e).c_ptr();
+ }
+ match app_zcash::parse_pczt_cypherpunk(
+ &MainNetwork,
+ message.get_payload(),
+ &ufvk_text,
+ seed_fingerprint,
+ ) {
+ Ok(pczt) => display_items.push(DisplayPczt::from(&pczt)),
+ Err(e) => return TransactionParseResult::from(e).c_ptr(),
+ }
+ }
+
+ TransactionParseResult::success(DisplayZcashBatch::from(display_items).c_ptr()).c_ptr()
+}
+
+#[cfg(feature = "cypherpunk")]
+unsafe fn sign_zcash_batch_tx_cypherpunk_dynamic(
+ tx: PtrUR,
+ ufvk: PtrString,
+ seed_fingerprint: PtrBytes,
+ account_index: u32,
+ disabled: bool,
+ seed: PtrBytes,
+ seed_len: u32,
+ max_fragment_length: usize,
+ allow_multipart: bool,
+) -> *mut UREncodeResult {
+ if disabled {
+ return UREncodeResult::from(RustCError::UnsupportedTransaction(
+ "Zcash requires at least 256-bit entropy (use 33-word Shamir shares)".to_string(),
+ ))
+ .c_ptr();
+ }
+ let batch = extract_ptr_with_type!(tx, ZcashSignBatch);
+ let ufvk_text = unsafe { recover_c_char(ufvk) };
+ let expected_seed_fingerprint = extract_array!(seed_fingerprint, u8, 32);
+ let expected_seed_fingerprint: &[u8; 32] = expected_seed_fingerprint.try_into().unwrap();
+ let mut seed = extract_array_mut!(seed, u8, seed_len as usize);
+
+ let result = match validate_zcash_batch(batch) {
+ Ok(()) => {
+ let seed_fingerprint = calculate_seed_fingerprint(seed);
+ match seed_fingerprint {
+ Ok(seed_fingerprint) => {
+ if &seed_fingerprint != expected_seed_fingerprint {
+ seed.zeroize();
+ return UREncodeResult::from(RustCError::MasterFingerprintMismatch).c_ptr();
+ }
+
+ let mut results = Vec::new();
+ for message in batch.get_messages() {
+ if let Err(e) = check_zcash_batch_message_cypherpunk(
+ message,
+ &ufvk_text,
+ &seed_fingerprint,
+ account_index,
+ ) {
+ seed.zeroize();
+ return UREncodeResult::from(e).c_ptr();
+ }
+
+ match app_zcash::sign_pczt(message.get_payload(), seed) {
+ Ok(payload) => {
+ if let Err(e) =
+ app_zcash::ensure_signable_shielded_actions_are_signed(
+ &MainNetwork,
+ message.get_payload(),
+ &payload,
+ &seed_fingerprint,
+ account_index,
+ )
+ {
+ seed.zeroize();
+ return UREncodeResult::from(e).c_ptr();
+ }
+
+ let payload_digest = sha256(&payload).to_vec();
+ results.push(ZcashSignMessageResult::signed(
+ message.get_id().clone(),
+ message.get_kind(),
+ payload,
+ payload_digest,
+ ));
+ }
+ Err(e) => {
+ seed.zeroize();
+ return UREncodeResult::from(e).c_ptr();
+ }
+ }
+ }
+
+ let result = ZcashSignResult::new(
+ ZCASH_SIGN_BATCH_VERSION,
+ batch.get_request_id().clone(),
+ results,
+ );
+ match TryInto::<Vec<u8>>::try_into(result) {
+ Ok(bytes) => {
+ let registry_type = ZcashSignResult::get_registry_type().get_type();
+ let encode_result = if allow_multipart {
+ UREncodeResult::encode(bytes, registry_type, max_fragment_length)
+ } else {
+ UREncodeResult::encode_full_response(bytes, registry_type)
+ };
+ encode_result.c_ptr()
+ }
+ Err(e) => UREncodeResult::from(e).c_ptr(),
+ }
+ }
+ Err(e) => UREncodeResult::from(e).c_ptr(),
+ }
+ }
+ Err(e) => UREncodeResult::from(e).c_ptr(),
+ };
+ seed.zeroize();
+ result
+}
+
+#[cfg(feature = "cypherpunk")]
+#[no_mangle]
+pub unsafe extern "C" fn sign_zcash_batch_tx_cypherpunk(
+ tx: PtrUR,
+ ufvk: PtrString,
+ seed_fingerprint: PtrBytes,
+ account_index: u32,
+ disabled: bool,
+ seed: PtrBytes,
+ seed_len: u32,
+) -> *mut UREncodeResult {
+ sign_zcash_batch_tx_cypherpunk_dynamic(
+ tx,
+ ufvk,
+ seed_fingerprint,
+ account_index,
+ disabled,
+ seed,
+ seed_len,
+ FRAGMENT_MAX_LENGTH_DEFAULT,
+ true,
+ )
+}
+
+#[cfg(feature = "cypherpunk")]
+#[no_mangle]
+pub unsafe extern "C" fn sign_zcash_batch_tx_cypherpunk_unlimited(
+ tx: PtrUR,
+ ufvk: PtrString,
+ seed_fingerprint: PtrBytes,
+ account_index: u32,
+ disabled: bool,
+ seed: PtrBytes,
+ seed_len: u32,
+) -> *mut UREncodeResult {
+ sign_zcash_batch_tx_cypherpunk_dynamic(
+ tx,
+ ufvk,
+ seed_fingerprint,
+ account_index,
+ disabled,
+ seed,
+ seed_len,
+ FRAGMENT_UNLIMITED_LENGTH,
+ false,
+ )
+}
+
#[no_mangle]
pub unsafe extern "C" fn sign_zcash_tx(
tx: PtrUR,
seed: PtrBytes,
seed_len: u32,
+) -> *mut UREncodeResult {
+ sign_zcash_tx_dynamic(tx, seed, seed_len, FRAGMENT_MAX_LENGTH_DEFAULT)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sign_zcash_tx_unlimited(
+ tx: PtrUR,
+ seed: PtrBytes,
+ seed_len: u32,
+) -> *mut UREncodeResult {
+ sign_zcash_tx_dynamic(tx, seed, seed_len, FRAGMENT_UNLIMITED_LENGTH)
+}
+
+unsafe fn sign_zcash_tx_dynamic(
+ tx: PtrUR,
+ seed: PtrBytes,
+ seed_len: u32,
+ max_fragment_length: usize,
) -> *mut UREncodeResult {
let pczt = extract_ptr_with_type!(tx, ZcashPczt);
let mut seed = extract_array_mut!(seed, u8, seed_len as usize);
@@ -182,7 +566,7 @@ pub unsafe extern "C" fn sign_zcash_tx(
Ok(v) => UREncodeResult::encode(
v,
ZcashPczt::get_registry_type().get_type(),
- FRAGMENT_MAX_LENGTH_DEFAULT,
+ max_fragment_length,
)
.c_ptr(),
},
@@ -192,7 +576,131 @@ pub unsafe extern "C" fn sign_zcash_tx(
result
}
+#[cfg(feature = "cypherpunk")]
+unsafe fn sign_zcash_tx_cypherpunk_dynamic(
+ tx: PtrUR,
+ ufvk: PtrString,
+ seed_fingerprint: PtrBytes,
+ account_index: u32,
+ disabled: bool,
+ seed: PtrBytes,
+ seed_len: u32,
+ max_fragment_length: usize,
+) -> *mut UREncodeResult {
+ if disabled {
+ return UREncodeResult::from(RustCError::UnsupportedTransaction(
+ "Zcash requires at least 256-bit entropy (use 33-word Shamir shares)".to_string(),
+ ))
+ .c_ptr();
+ }
+
+ let pczt = extract_ptr_with_type!(tx, ZcashPczt);
+ let ufvk_text = unsafe { recover_c_char(ufvk) };
+ let expected_seed_fingerprint = extract_array!(seed_fingerprint, u8, 32);
+ let expected_seed_fingerprint: &[u8; 32] = expected_seed_fingerprint.try_into().unwrap();
+ let mut seed = extract_array_mut!(seed, u8, seed_len as usize);
+ let pczt_data = pczt.get_data();
+
+ let result = match check_zcash_pczt_message_cypherpunk(
+ &pczt_data,
+ &ufvk_text,
+ expected_seed_fingerprint,
+ account_index,
+ ) {
+ Ok(()) => {
+ let seed_fingerprint = calculate_seed_fingerprint(seed);
+ match seed_fingerprint {
+ Ok(seed_fingerprint) => {
+ if &seed_fingerprint != expected_seed_fingerprint {
+ seed.zeroize();
+ return UREncodeResult::from(RustCError::MasterFingerprintMismatch).c_ptr();
+ }
+
+ match app_zcash::sign_pczt(&pczt_data, seed) {
+ Ok(signed_pczt) => {
+ if let Err(e) =
+ app_zcash::ensure_owned_supported_shielded_actions_are_signed(
+ &MainNetwork,
+ &pczt_data,
+ &signed_pczt,
+ &seed_fingerprint,
+ account_index,
+ )
+ {
+ seed.zeroize();
+ return UREncodeResult::from(e).c_ptr();
+ }
+
+ match ZcashPczt::new(signed_pczt).try_into() {
+ Err(e) => UREncodeResult::from(e).c_ptr(),
+ Ok(v) => UREncodeResult::encode(
+ v,
+ ZcashPczt::get_registry_type().get_type(),
+ max_fragment_length,
+ )
+ .c_ptr(),
+ }
+ }
+ Err(e) => UREncodeResult::from(e).c_ptr(),
+ }
+ }
+ Err(e) => UREncodeResult::from(e).c_ptr(),
+ }
+ }
+ Err(e) => UREncodeResult::from(e).c_ptr(),
+ };
+ seed.zeroize();
+ result
+}
+
+#[cfg(feature = "cypherpunk")]
+#[no_mangle]
+pub unsafe extern "C" fn sign_zcash_tx_cypherpunk(
+ tx: PtrUR,
+ ufvk: PtrString,
+ seed_fingerprint: PtrBytes,
+ account_index: u32,
+ disabled: bool,
+ seed: PtrBytes,
+ seed_len: u32,
+) -> *mut UREncodeResult {
+ sign_zcash_tx_cypherpunk_dynamic(
+ tx,
+ ufvk,
+ seed_fingerprint,
+ account_index,
+ disabled,
+ seed,
+ seed_len,
+ FRAGMENT_MAX_LENGTH_DEFAULT,
+ )
+}
+
+#[cfg(feature = "cypherpunk")]
+#[no_mangle]
+pub unsafe extern "C" fn sign_zcash_tx_cypherpunk_unlimited(
+ tx: PtrUR,
+ ufvk: PtrString,
+ seed_fingerprint: PtrBytes,
+ account_index: u32,
+ disabled: bool,
+ seed: PtrBytes,
+ seed_len: u32,
+) -> *mut UREncodeResult {
+ sign_zcash_tx_cypherpunk_dynamic(
+ tx,
+ ufvk,
+ seed_fingerprint,
+ account_index,
+ disabled,
+ seed,
+ seed_len,
+ FRAGMENT_UNLIMITED_LENGTH,
+ )
+}
+
make_free_method!(TransactionParseResult<DisplayPczt>);
+make_free_method!(TransactionParseResult<DisplayZcashBatch>);
use aes::cipher::block_padding::Pkcs7;
use aes::cipher::generic_array::GenericArray;
@@ -258,10 +766,217 @@ pub unsafe extern "C" fn rust_derive_iv_from_seed(
#[cfg(test)]
mod tests {
- use alloc::{string::String, vec::Vec};
+ use alloc::{format, string::String, vec, vec::Vec};
use super::*;
+ fn test_zcash_batch(messages: Vec<ZcashSignMessage>) -> ZcashSignBatch {
+ ZcashSignBatch::new(
+ ZCASH_SIGN_BATCH_VERSION,
+ b"test-request".to_vec(),
+ ZCASH_SIGN_BATCH_NETWORK_MAINNET,
+ messages,
+ Some(true),
+ )
+ }
+
+ fn test_zcash_message(id: &[u8], payload: &[u8]) -> ZcashSignMessage {
+ ZcashSignMessage::new(
+ id.to_vec(),
+ ZCASH_SIGN_MESSAGE_KIND_PCZT_V1,
+ payload.to_vec(),
+ Some(sha256(payload).to_vec()),
+ )
+ }
+
+ #[test]
+ fn test_validate_zcash_batch_accepts_valid_envelope() {
+ let batch = test_zcash_batch(vec![
+ test_zcash_message(b"one", b"pczt-one"),
+ test_zcash_message(b"two", b"pczt-two"),
+ ]);
+
+ validate_zcash_batch(&batch).unwrap();
+ }
+
+ #[test]
+ fn test_validate_zcash_batch_accepts_missing_atomic_as_default() {
+ let batch = ZcashSignBatch::new(
+ ZCASH_SIGN_BATCH_VERSION,
+ b"test-request".to_vec(),
+ ZCASH_SIGN_BATCH_NETWORK_MAINNET,
+ vec![test_zcash_message(b"one", b"pczt-one")],
+ None,
+ );
+
+ validate_zcash_batch(&batch).unwrap();
+ }
+
+ #[test]
+ fn test_validate_zcash_batch_accepts_max_messages() {
+ let batch = test_zcash_batch(
+ (0..ZCASH_BATCH_MAX_MESSAGES)
+ .map(|index| {
+ test_zcash_message(
+ format!("id-{index}").as_bytes(),
+ format!("pczt-{index}").as_bytes(),
+ )
+ })
+ .collect(),
+ );
+
+ validate_zcash_batch(&batch).unwrap();
+ }
+
+ #[test]
+ fn test_validate_zcash_batch_rejects_version_network_and_atomic_policy() {
+ let message = test_zcash_message(b"one", b"pczt-one");
+
+ let wrong_version = ZcashSignBatch::new(
+ ZCASH_SIGN_BATCH_VERSION + 1,
+ b"test-request".to_vec(),
+ ZCASH_SIGN_BATCH_NETWORK_MAINNET,
+ vec![message.clone()],
+ Some(true),
+ );
+ assert!(matches!(
+ validate_zcash_batch(&wrong_version),
+ Err(RustCError::UnsupportedTransaction(message))
+ if message.contains("unsupported Zcash batch version")
+ ));
+
+ let wrong_network = ZcashSignBatch::new(
+ ZCASH_SIGN_BATCH_VERSION,
+ b"test-request".to_vec(),
+ ZCASH_SIGN_BATCH_NETWORK_MAINNET + 1,
+ vec![message.clone()],
+ Some(true),
+ );
+ assert!(matches!(
+ validate_zcash_batch(&wrong_network),
+ Err(RustCError::UnsupportedTransaction(message))
+ if message.contains("only Zcash mainnet")
+ ));
+
+ let non_atomic = ZcashSignBatch::new(
+ ZCASH_SIGN_BATCH_VERSION,
+ b"test-request".to_vec(),
+ ZCASH_SIGN_BATCH_NETWORK_MAINNET,
+ vec![message],
+ Some(false),
+ );
+ assert!(matches!(
+ validate_zcash_batch(&non_atomic),
+ Err(RustCError::UnsupportedTransaction(message))
+ if message.contains("atomic=true")
+ ));
+ }
+
+ #[test]
+ fn test_validate_zcash_batch_rejects_empty_request_id_and_messages() {
+ let empty_request_id = ZcashSignBatch::new(
+ ZCASH_SIGN_BATCH_VERSION,
+ vec![],
+ ZCASH_SIGN_BATCH_NETWORK_MAINNET,
+ vec![test_zcash_message(b"one", b"pczt-one")],
+ Some(true),
+ );
+ assert_eq!(
+ validate_zcash_batch(&empty_request_id).unwrap_err(),
+ RustCError::InvalidData("Zcash batch has no request id".to_string())
+ );
+
+ let empty_messages = test_zcash_batch(vec![]);
+ assert_eq!(
+ validate_zcash_batch(&empty_messages).unwrap_err(),
+ RustCError::InvalidData("Zcash batch has no messages".to_string())
+ );
+ }
+
+ #[test]
+ fn test_validate_zcash_batch_rejects_invalid_message_fields() {
+ let unsupported_kind = ZcashSignMessage::new(
+ b"one".to_vec(),
+ ZCASH_SIGN_MESSAGE_KIND_PCZT_V1 + 1,
+ b"pczt-one".to_vec(),
+ Some(sha256(b"pczt-one").to_vec()),
+ );
+ assert!(matches!(
+ validate_zcash_batch(&test_zcash_batch(vec![unsupported_kind])),
+ Err(RustCError::UnsupportedTransaction(message))
+ if message.contains("unsupported Zcash batch message kind")
+ ));
+
+ let empty_message_id = test_zcash_message(b"", b"pczt-one");
+ assert_eq!(
+ validate_zcash_batch(&test_zcash_batch(vec![empty_message_id])).unwrap_err(),
+ RustCError::InvalidData("Zcash batch message 0 has no id".to_string())
+ );
+
+ let empty_payload = test_zcash_message(b"one", b"");
+ assert_eq!(
+ validate_zcash_batch(&test_zcash_batch(vec![empty_payload])).unwrap_err(),
+ RustCError::InvalidData("Zcash batch message 0 has no payload".to_string())
+ );
+ }
+
+ #[test]
+ fn test_validate_zcash_batch_rejects_too_many_messages() {
+ let batch = test_zcash_batch(
+ (0..=ZCASH_BATCH_MAX_MESSAGES)
+ .map(|index| {
+ test_zcash_message(
+ format!("id-{index}").as_bytes(),
+ format!("pczt-{index}").as_bytes(),
+ )
+ })
+ .collect(),
+ );
+
+ assert!(matches!(
+ validate_zcash_batch(&batch),
+ Err(RustCError::UnsupportedTransaction(message))
+ if message.contains("supports at most")
+ ));
+ }
+
+ #[test]
+ fn test_validate_zcash_batch_rejects_duplicate_ids_and_payloads() {
+ let duplicate_ids = test_zcash_batch(vec![
+ test_zcash_message(b"same", b"pczt-one"),
+ test_zcash_message(b"same", b"pczt-two"),
+ ]);
+ assert_eq!(
+ validate_zcash_batch(&duplicate_ids).unwrap_err(),
+ RustCError::InvalidData("Zcash batch contains duplicate message ids".to_string())
+ );
+
+ let duplicate_payloads = test_zcash_batch(vec![
+ test_zcash_message(b"one", b"pczt"),
+ test_zcash_message(b"two", b"pczt"),
+ ]);
+ assert_eq!(
+ validate_zcash_batch(&duplicate_payloads).unwrap_err(),
+ RustCError::InvalidData("Zcash batch contains duplicate payloads".to_string())
+ );
+ }
+
+ #[test]
+ fn test_validate_zcash_batch_rejects_payload_digest_mismatch() {
+ let message = ZcashSignMessage::new(
+ b"one".to_vec(),
+ ZCASH_SIGN_MESSAGE_KIND_PCZT_V1,
+ b"pczt-one".to_vec(),
+ Some(sha256(b"different-payload").to_vec()),
+ );
+ let batch = test_zcash_batch(vec![message]);
+
+ assert_eq!(
+ validate_zcash_batch(&batch).unwrap_err(),
+ RustCError::InvalidData("Zcash batch message 0 payload digest mismatch".to_string())
+ );
+ }
+
#[test]
fn test_aes256_cbc_encrypt() {
let mut data = convert_c_char("hello world".to_string());
diff --git a/rust/rust_c/src/zcash/structs.rs b/rust/rust_c/src/zcash/structs.rs
index 0a0cf6f..4598de1 100644
--- a/rust/rust_c/src/zcash/structs.rs
+++ b/rust/rust_c/src/zcash/structs.rs
@@ -55,6 +55,25 @@ impl Free for DisplayPczt {
}
}
+#[repr(C)]
+pub struct DisplayZcashBatch {
+ pub txs: Ptr<VecFFI<DisplayPczt>>,
+}
+
+impl From<Vec<DisplayPczt>> for DisplayZcashBatch {
+ fn from(txs: Vec<DisplayPczt>) -> Self {
+ Self {
+ txs: VecFFI::from(txs).c_ptr(),
+ }
+ }
+}
+
+impl Free for DisplayZcashBatch {
+ unsafe fn free(&self) {
+ free_vec!(self.txs);
+ }
+}
+
unsafe fn free_display_ptr<T: Free>(ptr: Ptr<T>) {
if ptr.is_null() {
return;
@@ -189,6 +208,7 @@ impl Free for DisplayOrchard {
impl_c_ptrs!(
DisplayPczt,
+ DisplayZcashBatch,
DisplayTransparent,
DisplayFrom,
DisplayTo,
diff --git a/src/tasks/ui_display_task.c b/src/tasks/ui_display_task.c
index 9ee3a01..225d89d 100644
--- a/src/tasks/ui_display_task.c
+++ b/src/tasks/ui_display_task.c
@@ -167,6 +167,12 @@ static void UiDisplayTask(void *argument)
break;
#endif
case UI_MSG_PREPARE_RECEIVE_UR_USB: {
+#ifdef CYPHERPUNK_VERSION
+ if (rcvMsg.value == ZcashBatchTx) {
+ GuiFrameOpenView(&g_zcashBatchView);
+ break;
+ }
+#endif
GuiFrameOpenViewWithParam(&g_transactionDetailView, &rcvMsg.value, sizeof(rcvMsg.value));
}
break;
diff --git a/src/ui/gui_analyze/gui_resolve_ur.c b/src/ui/gui_analyze/gui_resolve_ur.c
index c8aa5e3..78d32ac 100644
--- a/src/ui/gui_analyze/gui_resolve_ur.c
+++ b/src/ui/gui_analyze/gui_resolve_ur.c
@@ -20,6 +20,9 @@
#include "gui_derive_context_hash_request_widgets.h"
#include "gui_eth_batch_tx_widgets.h"
#endif
+#ifdef CYPHERPUNK_VERSION
+#include "gui_zcash_batch_widgets.h"
+#endif
// The order of the enumeration must be guaranteed
static SetChainData_t g_chainViewArray[] = {
@@ -27,6 +30,7 @@ static SetChainData_t g_chainViewArray[] = {
{REMAPVIEW_BTC_MESSAGE, (SetChainDataFunc)GuiSetPsbtUrData},
#ifdef CYPHERPUNK_VERSION
{REMAPVIEW_ZCASH, (SetChainDataFunc)GuiSetZcashUrData},
+ {REMAPVIEW_ZCASH_BATCH_TX, (SetChainDataFunc)GuiSetZcashBatchUrData},
{REMAPVIEW_XMR_OUTPUT, (SetChainDataFunc)GuiSetMoneroUrData},
{REMAPVIEW_XMR_UNSIGNED, (SetChainDataFunc)GuiSetMoneroUrData},
#endif
@@ -92,6 +96,11 @@ void handleURResult(URParseResult *urResult, URParseMultiResult *urMultiResult,
GuiSetEthBatchTxData(urResult, urMultiResult, is_multi);
break;
#endif
+#ifdef CYPHERPUNK_VERSION
+ case ZcashBatchTx:
+ GuiSetZcashBatchUrData(urResult, urMultiResult, is_multi);
+ break;
+#endif
#ifdef BTC_ONLY
case MultisigWalletImport:
GuiSetMultisigImportWalletDataByQRCode(urResult, urMultiResult, is_multi);
@@ -112,6 +121,9 @@ void handleURResult(URParseResult *urResult, URParseMultiResult *urMultiResult,
|| urViewType.viewType == DeriveContextHashRequest
|| urViewType.viewType == EthBatchTx
#endif
+#ifdef CYPHERPUNK_VERSION
+ || urViewType.viewType == ZcashBatchTx
+#endif
#ifdef BTC_ONLY
|| urViewType.viewType == MultisigWalletImport
|| urViewType.viewType == MultisigBytesImportXpub
@@ -131,4 +143,4 @@ void handleURResult(URParseResult *urResult, URParseMultiResult *urMultiResult,
}
#endif
}
-}
\ No newline at end of file
+}
diff --git a/src/ui/gui_chain/gui_chain.c b/src/ui/gui_chain/gui_chain.c
index 7c1a681..93ca8ee 100644
--- a/src/ui/gui_chain/gui_chain.c
+++ b/src/ui/gui_chain/gui_chain.c
@@ -44,7 +44,11 @@ bool CheckViewTypeIsAllow(uint8_t viewType)
}
#endif
#ifdef CYPHERPUNK_VERSION
- return ViewTypeReMap(viewType) == REMAPVIEW_BTC || ViewTypeReMap(viewType) == REMAPVIEW_BTC_MESSAGE;
+ GuiRemapViewType remapType = ViewTypeReMap(viewType);
+ return remapType == REMAPVIEW_BTC ||
+ remapType == REMAPVIEW_BTC_MESSAGE ||
+ remapType == REMAPVIEW_ZCASH ||
+ remapType == REMAPVIEW_ZCASH_BATCH_TX;
#endif
return false;
}
@@ -108,7 +112,8 @@ static const ViewHandlerEntry g_viewHandlerMap[] = {
#endif
#ifdef CYPHERPUNK_VERSION
- {ZcashTx, GuiGetZcashSignQrCodeData, NULL, GuiGetZcashCheckResult, CHAIN_ZCASH, REMAPVIEW_ZCASH},
+ {ZcashTx, GuiGetZcashSignQrCodeData, GuiGetZcashSignUrDataUnlimited, GuiGetZcashCheckResult, CHAIN_ZCASH, REMAPVIEW_ZCASH},
+ {ZcashBatchTx, GuiGetZcashBatchSignQrCodeData, GuiGetZcashBatchSignUrDataUnlimited, GuiGetZcashBatchCheckResult, CHAIN_ZCASH, REMAPVIEW_ZCASH_BATCH_TX},
{XmrOutput, GuiGetMoneroKeyimagesQrCodeData, NULL, GuiGetMoneroOutputCheckResult, CHAIN_XMR, REMAPVIEW_XMR_OUTPUT},
{XmrTxUnsigned, GuiGetMoneroSignedTransactionQrCodeData, NULL, GuiGetMoneroUnsignedTxCheckResult, CHAIN_XMR, REMAPVIEW_XMR_UNSIGNED},
#endif
diff --git a/src/ui/gui_chain/gui_chain.h b/src/ui/gui_chain/gui_chain.h
index 2382891..e66861d 100644
--- a/src/ui/gui_chain/gui_chain.h
+++ b/src/ui/gui_chain/gui_chain.h
@@ -5,6 +5,9 @@
#include "gui_btc.h"
#ifndef BTC_ONLY
#include "gui_zcash.h"
+#ifdef CYPHERPUNK_VERSION
+#include "gui_zcash_batch_widgets.h"
+#endif
#ifdef WEB3_VERSION
#include "gui_eth.h"
#include "gui_eth_batch_tx_widgets.h"
@@ -140,6 +143,7 @@ typedef enum {
#ifdef CYPHERPUNK_VERSION
REMAPVIEW_ZCASH,
+ REMAPVIEW_ZCASH_BATCH_TX,
REMAPVIEW_XMR_OUTPUT,
REMAPVIEW_XMR_UNSIGNED,
#endif
diff --git a/src/ui/gui_chain/multi/gui_zcash.c b/src/ui/gui_chain/multi/gui_zcash.c
index 1a165ba..4a362d0 100644
--- a/src/ui/gui_chain/multi/gui_zcash.c
+++ b/src/ui/gui_chain/multi/gui_zcash.c
@@ -8,6 +8,7 @@
#include "gui_chain.h"
#define MAX_MEMO_LENGTH 1024
+#define ZCASH_OVERVIEW_DEFAULT_HEIGHT 480
static bool g_isMulti = false;
static URParseResult *g_urResult = NULL;
@@ -54,44 +55,52 @@ void *GuiGetZcashGUIData(void)
return g_parseResult;
}
-static lv_obj_t* GuiZcashOverviewTransparent(lv_obj_t *parent, lv_obj_t *last_view);
+static lv_obj_t* GuiZcashOverviewTransparent(lv_obj_t *parent, lv_obj_t *last_view, DisplayTransparent *transparent);
static lv_obj_t* GuiZcashOverviewShielded(lv_obj_t *parent, lv_obj_t *last_view, DisplayOrchard *pool, const char *labelText);
static lv_obj_t* GuiZcashOverviewFrom(lv_obj_t *parent, VecFFI_DisplayFrom *from, lv_obj_t *last_view);
static lv_obj_t* GuiZcashOverviewTo(lv_obj_t *parent, VecFFI_DisplayTo *to, lv_obj_t *last_view);
void GuiZcashOverview(lv_obj_t *parent, void *totalData)
{
- lv_obj_set_size(parent, 408, 480);
+ GuiZcashOverviewWithDataAndHeight(parent, g_zcashData, ZCASH_OVERVIEW_DEFAULT_HEIGHT);
+}
+
+void GuiZcashOverviewWithDataAndHeight(lv_obj_t *parent, DisplayPczt *data, lv_coord_t height)
+{
+ if (height <= 0) {
+ height = ZCASH_OVERVIEW_DEFAULT_HEIGHT;
+ }
+ lv_obj_set_size(parent, 408, height);
lv_obj_add_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(parent, LV_OBJ_FLAG_CLICKABLE);
- lv_obj_t* container = GuiCreateContainerWithParent(parent, 408, 480);
+ lv_obj_t* container = GuiCreateContainerWithParent(parent, 408, height);
lv_obj_add_flag(container, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(container, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t* last_view = NULL;
- if (g_zcashData->has_sapling) {
+ if (data->has_sapling) {
last_view = CreateTransactionItemView(container, _("Warning"), _("This transaction contains Sapling spends or outputs. Keystone does not support Sapling spend signing and output checking. Please take care of the potential risks."), last_view);
}
- last_view = CreateTransactionItemView(container, _("Amount"), g_zcashData->total_transfer_value, last_view);
- last_view = CreateTransactionItemView(container, _("Fee"), g_zcashData->fee_value, last_view);
+ last_view = CreateTransactionItemView(container, _("Amount"), data->total_transfer_value, last_view);
+ last_view = CreateTransactionItemView(container, _("Fee"), data->fee_value, last_view);
- if (g_zcashData->transparent != NULL) {
- last_view = GuiZcashOverviewTransparent(container, last_view);
+ if (data->transparent != NULL) {
+ last_view = GuiZcashOverviewTransparent(container, last_view, data->transparent);
}
- if (g_zcashData->orchard != NULL) {
- last_view = GuiZcashOverviewShielded(container, last_view, g_zcashData->orchard, _("Orchard"));
+ if (data->orchard != NULL) {
+ last_view = GuiZcashOverviewShielded(container, last_view, data->orchard, _("Orchard"));
}
- if (g_zcashData->ironwood != NULL) {
- last_view = GuiZcashOverviewShielded(container, last_view, g_zcashData->ironwood, _("Ironwood"));
+ if (data->ironwood != NULL) {
+ last_view = GuiZcashOverviewShielded(container, last_view, data->ironwood, _("Ironwood"));
}
}
-static lv_obj_t* GuiZcashOverviewTransparent(lv_obj_t *parent, lv_obj_t *last_view)
+static lv_obj_t* GuiZcashOverviewTransparent(lv_obj_t *parent, lv_obj_t *last_view, DisplayTransparent *transparent)
{
lv_obj_t* inner_last_view;
lv_obj_t* label = GuiCreateIllustrateLabel(parent, _("Transparent"));
@@ -99,13 +108,13 @@ static lv_obj_t* GuiZcashOverviewTransparent(lv_obj_t *parent, lv_obj_t *last_vi
inner_last_view = label;
- if (g_zcashData->transparent->from->size > 0) {
- lv_obj_t* from_view = GuiZcashOverviewFrom(parent, g_zcashData->transparent->from, inner_last_view);
+ if (transparent->from->size > 0) {
+ lv_obj_t* from_view = GuiZcashOverviewFrom(parent, transparent->from, inner_last_view);
inner_last_view = from_view;
}
- if (g_zcashData->transparent->to->size > 0) {
- lv_obj_t* to_view = GuiZcashOverviewTo(parent, g_zcashData->transparent->to, inner_last_view);
+ if (transparent->to->size > 0) {
+ lv_obj_t* to_view = GuiZcashOverviewTo(parent, transparent->to, inner_last_view);
inner_last_view = to_view;
}
@@ -167,6 +176,7 @@ static lv_obj_t* GuiZcashOverviewFrom(lv_obj_t *parent, VecFFI_DisplayFrom *from
char *order = (char *)SRAM_MALLOC(5);
snprintf_s(order, 5, "#%d", i + 1);
indexLabel = GuiCreateIllustrateLabel(innerContainer, order);
+ SRAM_FREE(order);
lv_obj_align(indexLabel, LV_ALIGN_TOP_LEFT, 0, innerHeight);
valueLabel = GuiCreateIllustrateLabel(innerContainer, from->data[i].value);
@@ -249,6 +259,7 @@ static lv_obj_t* GuiZcashOverviewTo(lv_obj_t *parent, VecFFI_DisplayTo *to, lv_o
char *order = (char *)SRAM_MALLOC(5);
snprintf_s(order, 5, "#%d", i + 1);
indexLabel = GuiCreateIllustrateLabel(innerContainer, order);
+ SRAM_FREE(order);
lv_obj_align(indexLabel, LV_ALIGN_TOP_LEFT, 0, innerHeight);
valueLabel = GuiCreateIllustrateLabel(innerContainer, to->data[i].value);
@@ -283,6 +294,7 @@ static lv_obj_t* GuiZcashOverviewTo(lv_obj_t *parent, VecFFI_DisplayTo *to, lv_o
char *memo = (char *)SRAM_MALLOC(MAX_MEMO_LENGTH);
snprintf_s(memo, MAX_MEMO_LENGTH, "Memo: %s", to->data[i].memo);
lv_obj_t *memoLabel = GuiCreateIllustrateLabel(innerContainer, memo);
+ SRAM_FREE(memo);
lv_obj_align(memoLabel, LV_ALIGN_TOP_LEFT, 0, innerHeight);
lv_obj_set_style_text_color(memoLabel, WHITE_COLOR, LV_PART_MAIN);
lv_obj_set_style_text_opa(memoLabel, LV_OPA_56, LV_PART_MAIN);
@@ -326,10 +338,82 @@ PtrT_TransactionCheckResult GuiGetZcashCheckResult(void)
#endif
}
+#ifdef CYPHERPUNK_VERSION
+UREncodeResult *GuiSignZcashCypherpunkWithSeed(void *data,
+ bool unlimited,
+ ZcashCypherpunkSignFunc signFunc,
+ ZcashCypherpunkSignFunc unlimitedSignFunc)
+{
+ bool enable = IsPreviousLockScreenEnable();
+ SetLockScreen(false);
+ UREncodeResult *encodeResult = NULL;
+ uint8_t seed[SEED_LEN] = {0};
+ uint8_t sfp[32] = {0};
+ uint32_t zcashAccountIndex = 0;
+ char ufvk[ZCASH_UFVK_MAX_LEN + 1] = {0};
+ bool disabled = !IsZcashSupportedForCurrentMnemonic();
+ int ret = 0;
+
+ do {
+ ZcashCypherpunkSignFunc selectedSignFunc = unlimited ? unlimitedSignFunc : signFunc;
+ if (disabled) {
+ encodeResult = selectedSignFunc(data, ufvk, sfp, zcashAccountIndex, true, seed, 0);
+ CHECK_CHAIN_BREAK(encodeResult);
+ break;
+ }
+
+ ret = GetAccountSeed(GetCurrentAccountIndex(), seed, SecretCacheGetPassword());
+ if (ret != 0) {
+ break;
+ }
+ ret = GetZcashSFP(GetCurrentAccountIndex(), sfp);
+ if (ret != 0) {
+ break;
+ }
+ ret = GetZcashUFVK(GetCurrentAccountIndex(), ufvk);
+ if (ret != 0) {
+ break;
+ }
+
+ int len = GetMnemonicType() == MNEMONIC_TYPE_BIP39 ? sizeof(seed) : GetCurrentAccountEntropyLen();
+ encodeResult = selectedSignFunc(data, ufvk, sfp, zcashAccountIndex, false, seed, len);
+ CHECK_CHAIN_BREAK(encodeResult);
+ } while (0);
+
+ memset_s(seed, sizeof(seed), 0, sizeof(seed));
+ ClearSecretCache();
+ SetLockScreen(enable);
+ return encodeResult;
+}
+
+static UREncodeResult *SignZcashCypherpunkInternal(void *data, bool unlimited)
+{
+ return GuiSignZcashCypherpunkWithSeed(
+ data,
+ unlimited,
+ sign_zcash_tx_cypherpunk,
+ sign_zcash_tx_cypherpunk_unlimited);
+}
+#endif
+
UREncodeResult *GuiGetZcashSignQrCodeData(void)
{
void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+#ifdef CYPHERPUNK_VERSION
+ return SignZcashCypherpunkInternal(data, false);
+#else
return SignInternal(sign_zcash_tx, data);
+#endif
+}
+
+UREncodeResult *GuiGetZcashSignUrDataUnlimited(void)
+{
+ void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+#ifdef CYPHERPUNK_VERSION
+ return SignZcashCypherpunkInternal(data, true);
+#else
+ return SignInternal(sign_zcash_tx_unlimited, data);
+#endif
}
void FreeZcashMemory(void)
diff --git a/src/ui/gui_chain/multi/gui_zcash.h b/src/ui/gui_chain/multi/gui_zcash.h
index ed3f807..7fcd8cf 100644
--- a/src/ui/gui_chain/multi/gui_zcash.h
+++ b/src/ui/gui_chain/multi/gui_zcash.h
@@ -5,10 +5,25 @@
void GuiSetZcashUrData(URParseResult *urResult, URParseMultiResult *urMultiResult, bool multi);
void *GuiGetZcashGUIData(void);
+void GuiZcashOverviewWithDataAndHeight(lv_obj_t *parent, DisplayPczt *data, lv_coord_t height);
+void GuiZcashOverview(lv_obj_t *parent, void *totalData);
PtrT_TransactionCheckResult GuiGetZcashCheckResult(void);
+#ifdef CYPHERPUNK_VERSION
+typedef UREncodeResult *(*ZcashCypherpunkSignFunc)(void *data,
+ PtrString ufvk,
+ PtrBytes seedFingerprint,
+ uint32_t accountIndex,
+ bool disabled,
+ PtrBytes seed,
+ uint32_t seedLen);
+UREncodeResult *GuiSignZcashCypherpunkWithSeed(void *data,
+ bool unlimited,
+ ZcashCypherpunkSignFunc signFunc,
+ ZcashCypherpunkSignFunc unlimitedSignFunc);
+#endif
UREncodeResult *GuiGetZcashSignQrCodeData(void);
+UREncodeResult *GuiGetZcashSignUrDataUnlimited(void);
void FreeZcashMemory(void);
-void GuiZcashOverview(lv_obj_t *parent, void *totalData);
-#endif
\ No newline at end of file
+#endif
diff --git a/src/ui/gui_frame/gui_framework.c b/src/ui/gui_frame/gui_framework.c
index 0ccf483..6f6a20b 100644
--- a/src/ui/gui_frame/gui_framework.c
+++ b/src/ui/gui_frame/gui_framework.c
@@ -230,7 +230,8 @@ static const char *GuiFrameIdToName(SCREEN_ID_ENUM ID)
"SCREEN_TUTORIAL\0" "SCREEN_CONNECTION\0" "SCREEN_MULTI_ACCOUNTS_RECEIVE\0" "SCREEN_KEY_DERIVATION_REQUEST\0"
"SCREEN_SCAN\0" "SCREEN_TRANSACTION_DETAIL\0" "SCREEN_TRANSACTION_SIGNATURE\0" "SCREEN_USB_TRANSPORT\0"
"SCREEN_DEVICE_PUB_KEY\0" "SCREEN_DEVICE_UPDATE_SUCCESS\0" "SCREEN_BTC_WALLET_PROFILE\0" "SCREEN_MULTI_SIG_IMPORT_WALLET_INFO\0"
- "SCREEN_MULTISIG_WALLET_EXPORT\0" "SCREEN_CREATE_MULTI\0" "SCREEN_MANAGE_MULTI_SIG\0" "SCREEN_ETH_BATCH_TX\0";
+ "SCREEN_MULTISIG_WALLET_EXPORT\0" "SCREEN_CREATE_MULTI\0" "SCREEN_MANAGE_MULTI_SIG\0" "SCREEN_ETH_BATCH_TX\0"
+ "SCREEN_ZCASH_BATCH_TX\0";
SCREEN_ID_ENUM i;
for (i = SCREEN_INIT; i != ID && *str; i++) {
@@ -239,4 +240,4 @@ static const char *GuiFrameIdToName(SCREEN_ID_ENUM ID)
printf("id = %d name = %s\n", ID, str);
const char *name = str;
return name;
-}
\ No newline at end of file
+}
diff --git a/src/ui/gui_frame/gui_obj.h b/src/ui/gui_frame/gui_obj.h
index 6feb9c0..9072af5 100644
--- a/src/ui/gui_frame/gui_obj.h
+++ b/src/ui/gui_frame/gui_obj.h
@@ -65,6 +65,7 @@ typedef int32_t(*GuiEventProcessFunc)(void *self, uint16_t usEvent, void *param,
add(SCREEN_CONNECT_USB) \
add(SCREEN_CHECK_DELETE_WALLET) \
add(SCREEN_ETH_BATCH_TX) \
+ add(SCREEN_ZCASH_BATCH_TX) \
add(SCREEN_BOOT_UPDATE) \
typedef enum {
diff --git a/src/ui/gui_model/gui_model.c b/src/ui/gui_model/gui_model.c
index 1b2dc70..fa55e56 100644
--- a/src/ui/gui_model/gui_model.c
+++ b/src/ui/gui_model/gui_model.c
@@ -603,13 +603,18 @@ static int32_t ModelURGenerateQRCode(const void *indata, uint32_t inDataLen, Bac
{
GenerateUR func = (GenerateUR)getUR;
g_urResult = func();
+ if (g_urResult == NULL) {
+ printf("error message: failed to generate UR\r\n");
+ GuiApiEmitSignal(SIG_BACKGROUND_UR_GENERATE_FAIL, NULL, 0);
+ return SUCCESS_CODE;
+ }
if (g_urResult->error_code == 0) {
// printf("%s\r\n", g_urResult->data);
GuiApiEmitSignal(SIG_BACKGROUND_UR_GENERATE_SUCCESS, g_urResult->data, strnlen_s(g_urResult->data, SIMPLERESPONSE_C_CHAR_MAX_LEN) + 1);
} else {
char *message = g_urResult->error_message != NULL ? g_urResult->error_message : "";
printf("error message: %s\r\n", message);
- GuiApiEmitSignal(SIG_BACKGROUND_UR_GENERATE_FAIL, message, strnlen_s(message, SIMPLERESPONSE_C_CHAR_MAX_LEN) + 1);
+ GuiApiEmitSignal(SIG_BACKGROUND_UR_GENERATE_FAIL, &g_urResult, sizeof(g_urResult));
}
return SUCCESS_CODE;
}
diff --git a/src/ui/gui_views/gui_transaction_signature_view.c b/src/ui/gui_views/gui_transaction_signature_view.c
index d23f0e2..d84cd13 100644
--- a/src/ui/gui_views/gui_transaction_signature_view.c
+++ b/src/ui/gui_views/gui_transaction_signature_view.c
@@ -39,6 +39,9 @@ int32_t GuiTransactionSignatureViewEventProcess(void *self, uint16_t usEvent, vo
case SIG_BACKGROUND_UR_UPDATE:
GuiTransactionSignatureHandleURUpdate((char*)param, usLen);
break;
+ case SIG_BACKGROUND_UR_GENERATE_FAIL:
+ GuiTransactionSignatureHandleURGenerateFail(param);
+ break;
default:
return ERR_GUI_UNHANDLED;
}
@@ -51,4 +54,4 @@ GUI_VIEW g_transactionSignatureView = {
.isActive = false,
.optimization = false,
.pEvtHandler = GuiTransactionSignatureViewEventProcess,
-};
\ No newline at end of file
+};
diff --git a/src/ui/gui_views/gui_views.h b/src/ui/gui_views/gui_views.h
index 39fa976..a86d7bd 100644
--- a/src/ui/gui_views/gui_views.h
+++ b/src/ui/gui_views/gui_views.h
@@ -143,6 +143,7 @@ typedef enum {
SIG_BACKGROUND_UR_GENERATE_SUCCESS,
SIG_BACKGROUND_UR_GENERATE_FAIL,
SIG_BACKGROUND_UR_UPDATE,
+ SIG_BACKGROUND_UR_GENERATE_FAIL,
SIG_BACKGROUND_UR_BUTT,
SIG_TRANSACTION_CHECK_PASS = SIG_BACKGROUND_UR_BUTT + 50,
@@ -223,6 +224,9 @@ extern GUI_VIEW g_multiAccountsReceiveView;
extern GUI_VIEW g_keyDerivationRequestView;
extern GUI_VIEW g_deriveContextHashRequestView;
extern GUI_VIEW g_ethBatchTxView;
+#ifdef CYPHERPUNK_VERSION
+extern GUI_VIEW g_zcashBatchView;
+#endif
#endif
extern GUI_VIEW g_checkDeleteWalletView;
extern GUI_VIEW g_scanView;
diff --git a/src/ui/gui_views/multi/cypherpunk/gui_zcash_batch_view.c b/src/ui/gui_views/multi/cypherpunk/gui_zcash_batch_view.c
new file mode 100644
index 0000000..da21aff
--- /dev/null
+++ b/src/ui/gui_views/multi/cypherpunk/gui_zcash_batch_view.c
@@ -0,0 +1,81 @@
+#include "gui.h"
+#include "gui_obj.h"
+#include "gui_model.h"
+#include "gui_views.h"
+#include "gui_zcash_batch_widgets.h"
+#include "gui_lock_widgets.h"
+#include "gui_pending_hintbox.h"
+
+static int32_t GuiZcashBatchViewInit(void)
+{
+ GuiZcashBatchWidgetsInit();
+ return SUCCESS_CODE;
+}
+
+static int32_t GuiZcashBatchViewDeInit(void)
+{
+ GuiZcashBatchWidgetsDeInit();
+ return SUCCESS_CODE;
+}
+
+int32_t GuiZcashBatchViewEventProcess(void *self, uint16_t usEvent, void *param, uint16_t usLen)
+{
+ switch (usEvent) {
+ case GUI_EVENT_OBJ_INIT:
+ return GuiZcashBatchViewInit();
+ case GUI_EVENT_OBJ_DEINIT:
+ return GuiZcashBatchViewDeInit();
+ case GUI_EVENT_REFRESH:
+ GuiZcashBatchWidgetsRefresh();
+ break;
+ case SIG_VERIFY_PASSWORD_PASS:
+ if (param != NULL) {
+ uint16_t sig = *(uint16_t *)param;
+ if (sig == SIG_LOCK_VIEW_SCREEN_GO_HOME_PASS) {
+ GuiLockScreenToHome();
+ return SUCCESS_CODE;
+ }
+ }
+ GuiZcashBatchWidgetsVerifyPasswordSuccess();
+ break;
+ case SIG_VERIFY_PASSWORD_FAIL:
+ if (param != NULL) {
+ PasswordVerifyResult_t *passwordVerifyResult = (PasswordVerifyResult_t *)param;
+ uint16_t sig = *(uint16_t *)passwordVerifyResult->signal;
+ if (sig == SIG_LOCK_VIEW_SCREEN_GO_HOME_PASS) {
+ GuiLockScreenPassCode(false);
+ GuiLockScreenErrorCount(param);
+ return SUCCESS_CODE;
+ }
+ }
+ GuiLockScreenPassCode(false);
+ GuiZcashBatchWidgetsSignVerifyPasswordErrorCount(param);
+ break;
+ case SIG_INIT_PULLOUT_USB:
+ GuiZcashBatchWidgetsUsbPullout();
+ break;
+ case SIG_TRANSACTION_PARSE_SUCCESS:
+ GuiZcashBatchWidgetsTransactionParseSuccess();
+ break;
+ case SIG_TRANSACTION_PARSE_FAIL:
+ GuiZcashBatchWidgetsTransactionParseFail();
+ break;
+ case SIG_HIDE_TRANSACTION_LOADING:
+ GuiPendingHintBoxRemove();
+ break;
+ case SIG_SHOW_TRANSACTION_LOADING:
+ GuiNoPendingHintBoxOpen(_("Loading"));
+ break;
+ default:
+ return ERR_GUI_UNHANDLED;
+ }
+ return SUCCESS_CODE;
+}
+
+GUI_VIEW g_zcashBatchView = {
+ .id = SCREEN_ZCASH_BATCH_TX,
+ .previous = NULL,
+ .isActive = false,
+ .optimization = false,
+ .pEvtHandler = GuiZcashBatchViewEventProcess,
+};
diff --git a/src/ui/gui_views/multi/web3/gui_derive_context_hash_request_view.c b/src/ui/gui_views/multi/web3/gui_derive_context_hash_request_view.c
index ceed939..2402d5d 100644
--- a/src/ui/gui_views/multi/web3/gui_derive_context_hash_request_view.c
+++ b/src/ui/gui_views/multi/web3/gui_derive_context_hash_request_view.c
@@ -28,7 +28,7 @@ int32_t GuiDeriveContextHashRequestViewEventProcess(void *self, uint16_t usEvent
GuiDeriveContextHashWidgetHandleURUpdate((char *)param, usLen);
break;
case SIG_BACKGROUND_UR_GENERATE_FAIL:
- GuiDeriveContextHashWidgetHandleURGenerateFail((char *)param);
+ GuiDeriveContextHashWidgetHandleURGenerateFail(param);
break;
case SIG_VERIFY_PASSWORD_PASS:
if (param != NULL) {
diff --git a/src/ui/gui_widgets/gui_scan_widgets.c b/src/ui/gui_widgets/gui_scan_widgets.c
index 642df9b..6fcd731 100644
--- a/src/ui/gui_widgets/gui_scan_widgets.c
+++ b/src/ui/gui_widgets/gui_scan_widgets.c
@@ -171,6 +171,15 @@ void GuiScanResult(bool result, void *param)
ThrowError(ERR_INVALID_QRCODE);
return;
}
+#ifdef CYPHERPUNK_VERSION
+ if (g_qrcodeViewType == ZcashBatchTx) {
+ if (!GuiCheckIfTopView(&g_homeView)) {
+ GuiCloseCurrentWorkingView();
+ }
+ GuiFrameOpenView(&g_zcashBatchView);
+ return;
+ }
+#endif
GuiModelCheckTransaction(g_qrcodeViewType);
} else {
UrViewType_t *urViewType = (UrViewType_t *)param;
@@ -287,4 +296,4 @@ static void GuiScanStart()
{
GuiSetScanCorner();
GuiModeControlQrDecode(true);
-}
\ No newline at end of file
+}
diff --git a/src/ui/gui_widgets/gui_transaction_detail_widgets.c b/src/ui/gui_widgets/gui_transaction_detail_widgets.c
index 581ab43..2392222 100644
--- a/src/ui/gui_widgets/gui_transaction_detail_widgets.c
+++ b/src/ui/gui_widgets/gui_transaction_detail_widgets.c
@@ -190,6 +190,13 @@ void GuiTransactionDetailDeInit()
//should get error cod here
void GuiTransactionParseFailed()
{
+#ifndef BTC_ONLY
+ if (GetCurrentTransactionMode() == TRANSACTION_MODE_USB) {
+ const char *data = "UR parsing failed";
+ HandleURResultViaUSBFunc(data, strlen(data), GetCurrentUSParsingRequestID(), PRS_PARSING_ERROR);
+ return;
+ }
+#endif
ThrowError(ERR_INVALID_QRCODE);
}
@@ -234,11 +241,17 @@ void GuiTransactionDetailVerifyPasswordSuccess(void)
return;
}
UREncodeResult *urResult = func();
+ if (urResult == NULL) {
+ const char *data = "Signing failed";
+ HandleURResultViaUSBFunc(data, strlen(data), GetCurrentUSParsingRequestID(), PRS_PARSING_ERROR);
+ return;
+ }
if (urResult->error_code == 0) {
HandleURResultViaUSBFunc(urResult->data, strlen(urResult->data), GetCurrentUSParsingRequestID(), RSP_SUCCESS_CODE);
} else {
HandleURResultViaUSBFunc(urResult->error_message, strlen(urResult->error_message), GetCurrentUSParsingRequestID(), PRS_PARSING_ERROR);
}
+ free_ur_encode_result(urResult);
return;
}
#endif
@@ -423,4 +436,4 @@ bool supportBlindSigning(uint8_t viewType)
#else
return false;
#endif
-}
\ No newline at end of file
+}
diff --git a/src/ui/gui_widgets/gui_transaction_signature_widgets.c b/src/ui/gui_widgets/gui_transaction_signature_widgets.c
index bd0f03d..9d53fc3 100644
--- a/src/ui/gui_widgets/gui_transaction_signature_widgets.c
+++ b/src/ui/gui_widgets/gui_transaction_signature_widgets.c
@@ -22,6 +22,7 @@
#include "fingerprint_process.h"
#include "gui_fullscreen_mode.h"
#include "gui_keyboard_hintbox.h"
+#include "gui_pending_hintbox.h"
#include "gui_page.h"
#include "account_manager.h"
#include "gui_animating_qrcode.h"
@@ -66,6 +67,20 @@ void GuiTransactionSignatureHandleURUpdate(char *data, uint16_t len)
GuiAnimatingQRCodeUpdate(data, len);
}
+void GuiTransactionSignatureHandleURGenerateFail(void *param)
+{
+ GuiPendingHintBoxRemove();
+ UREncodeResult *result = NULL;
+ if (param != NULL) {
+ result = *(UREncodeResult **)param;
+ }
+ if (result != NULL) {
+ GuiCreateRustErrorWindow(result->error_code, result->error_message, NULL, (ErrorWindowCallback)GuiCloseCurrentWorkingView);
+ } else {
+ GuiCreateErrorCodeWindow(ERR_INVALID_QRCODE, NULL, (ErrorWindowCallback)GuiCloseCurrentWorkingView);
+ }
+}
+
static void GuiTransactionSignatureNVSBarInit()
{
SetNavBarLeftBtn(g_pageWidget->navBarWidget, NVS_BAR_RETURN, GoToHomeViewHandler, NULL);
@@ -111,4 +126,4 @@ static void GuiCreateSignatureQRCode(lv_obj_t *parent)
#endif
GuiAnimatingQRCodeInitWithCustomSize(qrCont, func, showPending, 336, 336, (char *)_("sign_transaction"));
}
-}
\ No newline at end of file
+}
diff --git a/src/ui/gui_widgets/gui_transaction_signature_widgets.h b/src/ui/gui_widgets/gui_transaction_signature_widgets.h
index f8528c9..0f9fe3a 100644
--- a/src/ui/gui_widgets/gui_transaction_signature_widgets.h
+++ b/src/ui/gui_widgets/gui_transaction_signature_widgets.h
@@ -6,4 +6,5 @@ void GuiTransactionSignatureDeInit(void);
void GuiTransactionSignatureRefresh(void);
void GuiTransactionSignatureHandleURGenerate(char *data, uint16_t len);
void GuiTransactionSignatureHandleURUpdate(char *data, uint16_t len);
-#endif /* _GUI_TRANSACTION_WIDGETS_H */
\ No newline at end of file
+void GuiTransactionSignatureHandleURGenerateFail(void *param);
+#endif /* _GUI_TRANSACTION_WIDGETS_H */
diff --git a/src/ui/gui_widgets/multi/cypherpunk/gui_zcash_batch_widgets.c b/src/ui/gui_widgets/multi/cypherpunk/gui_zcash_batch_widgets.c
new file mode 100644
index 0000000..c5a5ffd
--- /dev/null
+++ b/src/ui/gui_widgets/multi/cypherpunk/gui_zcash_batch_widgets.c
@@ -0,0 +1,433 @@
+#include "gui_zcash_batch_widgets.h"
+#include "account_manager.h"
+#include "gui_button.h"
+#include "gui_chain.h"
+#include "gui_chain_components.h"
+#include "gui_hintbox.h"
+#include "gui_keyboard_hintbox.h"
+#include "gui_lock_widgets.h"
+#include "gui_model.h"
+#include "gui_page.h"
+#include "gui_views.h"
+#include "gui_zcash.h"
+#include "keystore.h"
+#include "screen_manager.h"
+#include "general/eapdu_services/service_resolve_ur.h"
+#include "user_memory.h"
+
+#define QRCODE_CONFIRM_SIGN_PROCESS 66
+#define USB_REQUEST_IDLE 0xFFFF
+#define ZCASH_BATCH_CONTENT_HEIGHT 656
+#define ZCASH_BATCH_SIGN_SLIDER_HEIGHT 114
+#define ZCASH_BATCH_BOTTOM_BTN_HEIGHT 90
+#define ZCASH_BATCH_BOTTOM_BTN_MARGIN 12
+
+static URParseResult *g_urResult = NULL;
+static URParseMultiResult *g_urMultiResult = NULL;
+static bool g_isMulti = false;
+
+static uint32_t g_currentTxIndex = 0;
+static uint32_t g_txCount = 0;
+
+static TransactionParseResult_DisplayZcashBatch *g_parseResult = NULL;
+static DisplayZcashBatch *g_displayZcashBatch = NULL;
+static DisplayPczt *g_currentTransaction = NULL;
+
+static PageWidget_t *g_pageWidget = NULL;
+static lv_obj_t *g_cont = NULL;
+static lv_obj_t *g_txContainer = NULL;
+static lv_obj_t *g_bottomBtnContainer = NULL;
+static lv_obj_t *g_signSlider = NULL;
+static lv_obj_t *g_parseErrorHintBox = NULL;
+static KeyboardWidget_t *g_keyboardWidget = NULL;
+
+static void *GuiParseZcashBatchData(void);
+static void CheckSliderProcessHandler(lv_event_t *e);
+static void GuiRenderCurrentTransaction(bool showSignSlider);
+static void GuiRenderBottomBtn(lv_obj_t *parent, bool showSignSlider);
+static void HandleClickPreviousBtn(lv_event_t *e);
+static void HandleClickNextBtn(lv_event_t *e);
+static void GuiReturnHome(void);
+static void CloseParseErrorHandler(lv_event_t *e);
+static UREncodeResult *SignZcashBatchInternal(void *data, bool unlimited);
+static bool IsZcashBatchUsbMode(void);
+static void RejectZcashBatchUsbRequest(void);
+static void RespondZcashBatchUsbParseError(const char *errorMessage);
+
+static void ClearPageData(void)
+{
+ g_currentTxIndex = 0;
+ g_txCount = 0;
+ g_currentTransaction = NULL;
+ g_displayZcashBatch = NULL;
+
+ if (g_parseResult != NULL) {
+ free_TransactionParseResult_DisplayZcashBatch(g_parseResult);
+ g_parseResult = NULL;
+ }
+
+ if (g_isMulti) {
+ CHECK_FREE_UR_RESULT(g_urMultiResult, true);
+ } else {
+ CHECK_FREE_UR_RESULT(g_urResult, false);
+ }
+}
+
+void GuiSetZcashBatchUrData(URParseResult *urResult, URParseMultiResult *urMultiResult, bool multi)
+{
+ g_urResult = urResult;
+ g_urMultiResult = urMultiResult;
+ g_isMulti = multi;
+}
+
+UREncodeResult *GuiGetZcashBatchSignQrCodeData(void)
+{
+ void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+ return SignZcashBatchInternal(data, false);
+}
+
+UREncodeResult *GuiGetZcashBatchSignUrDataUnlimited(void)
+{
+ void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+ return SignZcashBatchInternal(data, true);
+}
+
+static bool IsZcashBatchUsbMode(void)
+{
+ return GetCurrentUSParsingRequestID() != USB_REQUEST_IDLE;
+}
+
+static void RejectZcashBatchUsbRequest(void)
+{
+ if (!IsZcashBatchUsbMode()) {
+ return;
+ }
+
+ const char *data = "UR parsing rejected";
+ HandleURResultViaUSBFunc(data, strlen(data), GetCurrentUSParsingRequestID(), PRS_PARSING_REJECTED);
+}
+
+static void RespondZcashBatchUsbParseError(const char *errorMessage)
+{
+ if (!IsZcashBatchUsbMode()) {
+ return;
+ }
+
+ const char *data = (errorMessage != NULL && strlen(errorMessage) > 0)
+ ? errorMessage
+ : "UR parsing failed";
+ HandleURResultViaUSBFunc(data, strlen(data), GetCurrentUSParsingRequestID(), PRS_PARSING_ERROR);
+}
+
+static UREncodeResult *SignZcashBatchInternal(void *data, bool unlimited)
+{
+ return GuiSignZcashCypherpunkWithSeed(
+ data,
+ unlimited,
+ sign_zcash_batch_tx_cypherpunk,
+ sign_zcash_batch_tx_cypherpunk_unlimited);
+}
+
+#ifdef CYPHERPUNK_VERSION
+PtrT_TransactionCheckResult GuiGetZcashBatchCheckResult(void)
+{
+ void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+ uint8_t sfp[32] = {0};
+ uint32_t zcashAccountIndex = 0;
+ uint8_t accountNum = 0;
+ char ufvk[ZCASH_UFVK_MAX_LEN + 1] = {0};
+
+ GetExistAccountNum(&accountNum);
+ if (accountNum <= 0) {
+ return check_zcash_batch_tx_cypherpunk(data, ufvk, sfp, zcashAccountIndex, true);
+ }
+
+ GetZcashSFP(GetCurrentAccountIndex(), sfp);
+ GetZcashUFVK(GetCurrentAccountIndex(), ufvk);
+ return check_zcash_batch_tx_cypherpunk(
+ data,
+ ufvk,
+ sfp,
+ zcashAccountIndex,
+ !IsZcashSupportedForCurrentMnemonic());
+}
+#endif
+
+void GuiZcashBatchWidgetsVerifyPasswordSuccess(void)
+{
+ GuiDeleteKeyboardWidget(g_keyboardWidget);
+ if (IsZcashBatchUsbMode()) {
+ UREncodeResult *urResult = GuiGetZcashBatchSignUrDataUnlimited();
+ if (urResult != NULL && urResult->error_code == 0) {
+ if (urResult->is_multi_part) {
+ const char *errorMessage = "Signing result is too large for USB transport";
+ HandleURResultViaUSBFunc(errorMessage, strlen(errorMessage), GetCurrentUSParsingRequestID(), PRS_PARSING_ERROR);
+ } else {
+ HandleURResultViaUSBFunc(urResult->data, strlen(urResult->data), GetCurrentUSParsingRequestID(), RSP_SUCCESS_CODE);
+ }
+ } else {
+ const char *errorMessage = (urResult != NULL && urResult->error_message != NULL) ? urResult->error_message : "Signing failed";
+ HandleURResultViaUSBFunc(errorMessage, strlen(errorMessage), GetCurrentUSParsingRequestID(), PRS_PARSING_ERROR);
+ }
+ if (urResult != NULL) {
+ free_ur_encode_result(urResult);
+ }
+ return;
+ }
+ uint8_t viewType = ZcashBatchTx;
+ GuiFrameOpenViewWithParam(&g_transactionSignatureView, &viewType, sizeof(viewType));
+}
+
+void GuiZcashBatchWidgetsSignVerifyPasswordErrorCount(void *param)
+{
+ PasswordVerifyResult_t *passwordVerifyResult = (PasswordVerifyResult_t *)param;
+ if (passwordVerifyResult->errorCount == MAX_CURRENT_PASSWORD_ERROR_COUNT_SHOW_HINTBOX &&
+ IsZcashBatchUsbMode()) {
+ const char *data = "Please try again after unlocking";
+ HandleURResultViaUSBFunc(data, strlen(data), GetCurrentUSParsingRequestID(), PRS_PARSING_VERIFY_PASSWORD_ERROR);
+ }
+ GuiShowErrorNumber(g_keyboardWidget, passwordVerifyResult);
+}
+
+void GuiZcashBatchWidgetsUsbPullout(void)
+{
+ if (!IsZcashBatchUsbMode()) {
+ return;
+ }
+
+ GuiDeleteKeyboardWidget(g_keyboardWidget);
+ ClearUSBRequestId();
+ GuiReturnHome();
+}
+
+static void SignByPasswordCb(bool cancel)
+{
+ (void)cancel;
+ g_keyboardWidget = GuiCreateKeyboardWidget(g_pageWidget->contentZone);
+ SetKeyboardWidgetSelf(g_keyboardWidget, &g_keyboardWidget);
+ static uint16_t sig = SIG_SIGN_TRANSACTION_WITH_PASSWORD;
+ SetKeyboardWidgetSig(g_keyboardWidget, &sig);
+}
+
+static void CheckSliderProcessHandler(lv_event_t *e)
+{
+ if (lv_event_get_code(e) != LV_EVENT_RELEASED) {
+ return;
+ }
+
+ int32_t value = lv_slider_get_value(lv_event_get_target(e));
+ if (value >= QRCODE_CONFIRM_SIGN_PROCESS) {
+ SignByPasswordCb(false);
+ lv_slider_set_value(lv_event_get_target(e), 0, LV_ANIM_OFF);
+ } else {
+ lv_slider_set_value(lv_event_get_target(e), 0, LV_ANIM_ON);
+ }
+}
+
+static void HandleClickPreviousBtn(lv_event_t *e)
+{
+ if (lv_event_get_code(e) == LV_EVENT_CLICKED && g_currentTxIndex > 0) {
+ g_currentTxIndex--;
+ GuiZcashBatchWidgetsRefresh();
+ }
+}
+
+static void HandleClickNextBtn(lv_event_t *e)
+{
+ if (lv_event_get_code(e) == LV_EVENT_CLICKED && g_currentTxIndex < g_txCount - 1) {
+ g_currentTxIndex++;
+ GuiZcashBatchWidgetsRefresh();
+ }
+}
+
+static void GuiReturnHome(void)
+{
+ GuiCloseToTargetView(&g_homeView);
+}
+
+static void CloseParseErrorHandler(lv_event_t *e)
+{
+ lv_obj_del(lv_event_get_user_data(e));
+ g_parseErrorHintBox = NULL;
+ GuiReturnHome();
+}
+
+static lv_obj_t *GuiCreateZcashBatchParseErrorWindow(const char *errorMessage)
+{
+ const char *descText = (errorMessage != NULL && strlen(errorMessage) > 0)
+ ? errorMessage
+ : _("scan_qr_code_error_invalid_qrcode_desc");
+ lv_obj_t *cont = GuiCreateConfirmHintBox(
+ &imgFailed,
+ _("scan_qr_code_error_invalid_qrcode"),
+ descText,
+ NULL,
+ _("OK"),
+ WHITE_COLOR_OPA20);
+ lv_obj_add_event_cb(GuiGetHintBoxRightBtn(cont), CloseParseErrorHandler, LV_EVENT_CLICKED, cont);
+ return cont;
+}
+
+static void OnReturnHandler(lv_event_t *e)
+{
+ RejectZcashBatchUsbRequest();
+ GuiReturnHome();
+}
+
+static void ZcashBatchNavBarInit(void)
+{
+ SetNavBarLeftBtn(g_pageWidget->navBarWidget, NVS_BAR_RETURN, OnReturnHandler, NULL);
+}
+
+static void ZcashBatchNavBarRefresh(void)
+{
+ char text[BUFFER_SIZE_128] = {0};
+ if (g_txCount > 1) {
+ snprintf_s(
+ text,
+ sizeof(text),
+ "%s (%d/%d)",
+ _("confirm_transaction"),
+ (int)(g_currentTxIndex + 1),
+ (int)g_txCount);
+ } else {
+ snprintf_s(text, sizeof(text), "%s", _("confirm_transaction"));
+ }
+ SetCoinWallet(g_pageWidget->navBarWidget, CHAIN_ZCASH, text);
+
+ if (g_currentTxIndex == 0) {
+ SetNavBarLeftBtn(g_pageWidget->navBarWidget, NVS_BAR_RETURN, OnReturnHandler, NULL);
+ } else {
+ SetNavBarLeftBtn(g_pageWidget->navBarWidget, NVS_BAR_RETURN, HandleClickPreviousBtn, NULL);
+ }
+}
+
+static void GuiRenderBottomBtn(lv_obj_t *parent, bool showSignSlider)
+{
+ if (showSignSlider) {
+ GUI_DEL_OBJ(g_bottomBtnContainer)
+ if (g_signSlider == NULL) {
+ g_signSlider = GuiCreateConfirmSlider(parent, CheckSliderProcessHandler);
+ }
+ return;
+ }
+
+ GUI_DEL_OBJ(g_signSlider)
+ if (g_bottomBtnContainer != NULL) {
+ return;
+ }
+
+ g_bottomBtnContainer = GuiCreateContainerWithParent(parent, 480, ZCASH_BATCH_BOTTOM_BTN_HEIGHT);
+ lv_obj_align(g_bottomBtnContainer, LV_ALIGN_BOTTOM_MID, 0, 0);
+
+ lv_obj_t *leftBtn = GuiCreateTextBtn(g_bottomBtnContainer, _("Previous"));
+ lv_obj_align(leftBtn, LV_ALIGN_BOTTOM_LEFT, 36, -ZCASH_BATCH_BOTTOM_BTN_MARGIN);
+ lv_obj_set_size(leftBtn, 192, 66);
+ lv_obj_set_style_bg_color(leftBtn, DARK_GRAY_COLOR, LV_PART_MAIN);
+ lv_obj_add_event_cb(leftBtn, HandleClickPreviousBtn, LV_EVENT_CLICKED, NULL);
+
+ lv_obj_t *rightBtn = GuiCreateTextBtn(g_bottomBtnContainer, _("Next"));
+ lv_obj_align(rightBtn, LV_ALIGN_BOTTOM_RIGHT, -36, -ZCASH_BATCH_BOTTOM_BTN_MARGIN);
+ lv_obj_set_size(rightBtn, 192, 66);
+ lv_obj_set_style_bg_color(rightBtn, ORANGE_COLOR, LV_PART_MAIN);
+ lv_obj_add_event_cb(rightBtn, HandleClickNextBtn, LV_EVENT_CLICKED, NULL);
+}
+
+static void GuiRenderCurrentTransaction(bool showSignSlider)
+{
+ GUI_DEL_OBJ(g_txContainer)
+
+ uint16_t txHeight = showSignSlider
+ ? ZCASH_BATCH_CONTENT_HEIGHT - ZCASH_BATCH_SIGN_SLIDER_HEIGHT
+ : ZCASH_BATCH_CONTENT_HEIGHT - ZCASH_BATCH_BOTTOM_BTN_HEIGHT;
+
+ g_txContainer = GuiCreateContainerWithParent(g_cont, 408, txHeight);
+ lv_obj_align(g_txContainer, LV_ALIGN_TOP_MID, 0, 0);
+ lv_obj_add_flag(g_txContainer, LV_OBJ_FLAG_SCROLLABLE);
+ lv_obj_remove_style(g_txContainer, NULL, LV_PART_SCROLLBAR);
+
+ GuiZcashOverviewWithDataAndHeight(g_txContainer, g_currentTransaction, txHeight);
+ GuiRenderBottomBtn(g_cont, showSignSlider);
+}
+
+void GuiZcashBatchWidgetsRefresh(void)
+{
+ if (g_parseResult == NULL || g_parseResult->error_code != 0) {
+ return;
+ }
+
+ ZcashBatchNavBarRefresh();
+ g_currentTransaction = &g_displayZcashBatch->txs->data[g_currentTxIndex];
+ GuiRenderCurrentTransaction(g_currentTxIndex == g_txCount - 1);
+}
+
+static void *GuiParseZcashBatchData(void)
+{
+ void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+ uint8_t sfp[32];
+ uint32_t zcashAccountIndex = 0;
+ GetZcashSFP(GetCurrentAccountIndex(), sfp);
+
+ char ufvk[ZCASH_UFVK_MAX_LEN + 1] = {0};
+ GetZcashUFVK(GetCurrentAccountIndex(), ufvk);
+ g_parseResult = parse_zcash_batch_tx_cypherpunk(
+ data,
+ ufvk,
+ sfp,
+ zcashAccountIndex,
+ !IsZcashSupportedForCurrentMnemonic());
+
+ return g_parseResult;
+}
+
+void GuiZcashBatchWidgetsTransactionParseSuccess(void)
+{
+ g_displayZcashBatch = g_parseResult->data;
+ g_txCount = g_displayZcashBatch->txs->size;
+ g_currentTxIndex = 0;
+ GuiZcashBatchWidgetsRefresh();
+}
+
+void GuiZcashBatchWidgetsTransactionParseFail(void)
+{
+ printf("GuiZcashBatchWidgetsTransactionParseFail\n");
+ if (g_parseResult != NULL) {
+ printf("error: %s\n", g_parseResult->error_message);
+ if (IsZcashBatchUsbMode()) {
+ RespondZcashBatchUsbParseError(g_parseResult->error_message);
+ return;
+ }
+ g_parseErrorHintBox = GuiCreateZcashBatchParseErrorWindow(g_parseResult->error_message);
+ return;
+ }
+ if (IsZcashBatchUsbMode()) {
+ RespondZcashBatchUsbParseError(NULL);
+ return;
+ }
+ g_parseErrorHintBox = GuiCreateErrorCodeWindow(ERR_INVALID_QRCODE, &g_parseErrorHintBox, GuiReturnHome);
+}
+
+void GuiZcashBatchWidgetsInit(void)
+{
+ g_pageWidget = CreatePageWidget();
+ g_cont = g_pageWidget->contentZone;
+ g_txContainer = NULL;
+ g_bottomBtnContainer = NULL;
+ g_signSlider = NULL;
+ g_parseErrorHintBox = NULL;
+ g_keyboardWidget = NULL;
+
+ lv_obj_add_flag(g_cont, LV_OBJ_FLAG_SCROLLABLE);
+ lv_obj_remove_style(g_cont, NULL, LV_PART_SCROLLBAR);
+
+ ZcashBatchNavBarInit();
+ GuiEmitSignal(SIG_SHOW_TRANSACTION_LOADING, NULL, 0);
+ GuiModelParseTransaction(GuiParseZcashBatchData);
+}
+
+void GuiZcashBatchWidgetsDeInit(void)
+{
+ GuiDeleteKeyboardWidget(g_keyboardWidget);
+ GUI_PAGE_DEL(g_pageWidget);
+ ClearPageData();
+}
diff --git a/src/ui/gui_widgets/multi/cypherpunk/gui_zcash_batch_widgets.h b/src/ui/gui_widgets/multi/cypherpunk/gui_zcash_batch_widgets.h
new file mode 100644
index 0000000..66cb97e
--- /dev/null
+++ b/src/ui/gui_widgets/multi/cypherpunk/gui_zcash_batch_widgets.h
@@ -0,0 +1,22 @@
+#ifndef _GUI_ZCASH_BATCH_WIDGETS_H
+#define _GUI_ZCASH_BATCH_WIDGETS_H
+
+#include "gui.h"
+#include "rust.h"
+
+void GuiSetZcashBatchUrData(URParseResult *urResult, URParseMultiResult *urMultiResult, bool multi);
+void GuiZcashBatchWidgetsInit(void);
+void GuiZcashBatchWidgetsDeInit(void);
+void GuiZcashBatchWidgetsRefresh(void);
+void GuiZcashBatchWidgetsVerifyPasswordSuccess(void);
+void GuiZcashBatchWidgetsSignVerifyPasswordErrorCount(void *param);
+void GuiZcashBatchWidgetsUsbPullout(void);
+void GuiZcashBatchWidgetsTransactionParseSuccess(void);
+void GuiZcashBatchWidgetsTransactionParseFail(void);
+UREncodeResult *GuiGetZcashBatchSignQrCodeData(void);
+UREncodeResult *GuiGetZcashBatchSignUrDataUnlimited(void);
+#ifdef CYPHERPUNK_VERSION
+PtrT_TransactionCheckResult GuiGetZcashBatchCheckResult(void);
+#endif
+
+#endif
diff --git a/src/ui/gui_widgets/multi/web3/gui_derive_context_hash_request_widgets.c b/src/ui/gui_widgets/multi/web3/gui_derive_context_hash_request_widgets.c
index 88d8d63..7c22c9b 100644
--- a/src/ui/gui_widgets/multi/web3/gui_derive_context_hash_request_widgets.c
+++ b/src/ui/gui_widgets/multi/web3/gui_derive_context_hash_request_widgets.c
@@ -352,14 +352,20 @@ void GuiDeriveContextHashWidgetHandleURGenerate(char *data, uint16_t len)
GuiAnimantingQRCodeFirstUpdate(data, len);
}
-void GuiDeriveContextHashWidgetHandleURGenerateFail(char *message)
+void GuiDeriveContextHashWidgetHandleURGenerateFail(void *param)
{
GuiPendingHintBoxRemove();
GuiAnimatingQRCodeDestroyTimer();
+ UREncodeResult *result = NULL;
+ if (param != NULL) {
+ result = *(UREncodeResult **)param;
+ }
+ char *message = result != NULL && result->error_message != NULL
+ ? result->error_message
+ : (char *)_("Invalid derive-context-hash request");
GuiCreateHardwareCallInvaildParamHintboxWithHandler(
(char *)_("Invalid Request"),
- message != NULL ? message : (char *)_("Invalid derive-context-hash request"),
- CloseInvalidRequestHandler);
+ message, CloseInvalidRequestHandler);
}
void GuiDeriveContextHashWidgetHandleURUpdate(char *data, uint16_t len)
diff --git a/src/ui/gui_widgets/multi/web3/gui_derive_context_hash_request_widgets.h b/src/ui/gui_widgets/multi/web3/gui_derive_context_hash_request_widgets.h
index 57504a1..25b4818 100644
--- a/src/ui/gui_widgets/multi/web3/gui_derive_context_hash_request_widgets.h
+++ b/src/ui/gui_widgets/multi/web3/gui_derive_context_hash_request_widgets.h
@@ -9,7 +9,7 @@ void GuiDeriveContextHashRequestInit(bool isUsb);
void GuiDeriveContextHashRequestDeInit(void);
void GuiDeriveContextHashRequestRefresh(void);
void GuiDeriveContextHashWidgetHandleURGenerate(char *data, uint16_t len);
-void GuiDeriveContextHashWidgetHandleURGenerateFail(char *message);
+void GuiDeriveContextHashWidgetHandleURGenerateFail(void *param);
void GuiDeriveContextHashWidgetHandleURUpdate(char *data, uint16_t len);
void GuiSetDeriveContextHashRequestData(void *urResult, void *multiResult, bool is_multi);
void GuiDeriveContextHashPasswordErrorCount(void *param);
diff --git a/ui_simulator/simulator_model.c b/ui_simulator/simulator_model.c
index 553f5ee..3b3ecd2 100644
--- a/ui_simulator/simulator_model.c
+++ b/ui_simulator/simulator_model.c
@@ -552,6 +552,7 @@ int32_t prepare_qrcode()
}
#ifdef GET_QR_DATA_FROM_SCREEN
+#define SCREEN_QR_MAX_LOOP_COUNT 512
static struct URParseResult *urResult;
static UrViewType_t viewType;
static bool firstQrFlag = true;
@@ -559,6 +560,10 @@ static PtrDecoder decoder = NULL;
static void reset_qr_state()
{
+ if (urResult != NULL) {
+ free_ur_parse_result(urResult);
+ urResult = NULL;
+ }
firstQrFlag = true;
decoder = NULL;
}
@@ -583,6 +588,7 @@ static bool on_qr_detected(const char *qrString)
viewType.viewType = urResult->t;
viewType.urType = urResult->ur_type;
handleURResult(urResult, NULL, viewType, false);
+ urResult = NULL;
return true;
} else {
// first qr code
@@ -598,12 +604,16 @@ static bool on_qr_detected(const char *qrString)
viewType.viewType = MultiurResult->t;
viewType.urType = MultiurResult->ur_type;
printf("MultiurResult->t: %u\n", MultiurResult->t);
+ free_ur_parse_result(urResult);
+ urResult = NULL;
+ decoder = NULL;
handleURResult(NULL, MultiurResult, viewType, true);
return true;
}
} else {
printf("error code: %d\r\n", MultiurResult->error_code);
printf("error message: %s\r\n", MultiurResult->error_message);
+ free_ur_parse_multi_result(MultiurResult);
return true;
}
if (!(MultiurResult->is_complete)) {
@@ -616,7 +626,9 @@ static bool on_qr_detected(const char *qrString)
int32_t read_qrcode()
{
- read_qr_code_from_screen(on_qr_detected, 64);
+ reset_qr_state();
+ read_qr_code_from_screen(on_qr_detected, SCREEN_QR_MAX_LOOP_COUNT);
+ reset_qr_state();
return 0;
}
#else
@@ -713,4 +725,4 @@ int32_t read_qrcode()
bool GetEnsName(const char *addr, char *name)
{
return false;
-}
\ No newline at end of file
+}
Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.