Skip to content
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
7e764d0
feat(ui): unify shield screens into single ShieldScreen with address …
lklimek Mar 26, 2026
7b757ce
Merge branch 'v1.0-dev' into feat/unified-shield-screen
lklimek Mar 26, 2026
40e2614
feat: fetch epoch info on connection sync to populate protocol version
lklimek Mar 26, 2026
ded7612
fix(ui): invalidate cached tx indices when transaction list changes
lklimek Mar 26, 2026
b1aeb41
fix(shielded): add fee headroom to note selection (#795 item 1)
lklimek Mar 26, 2026
c4d3a56
fix(shielded): reduce fee headroom from 5 DASH to 0.1 DASH
lklimek Mar 26, 2026
f4d280e
fix(wallet): bootstrap platform addresses on wallet creation
lklimek Mar 27, 2026
2507e9e
fix(logging): demote cookie auth fallback to trace level
lklimek Mar 27, 2026
cccdb3f
fix(ui): add identities to Send screen destination autocomplete
lklimek Mar 27, 2026
8ac9b7e
docs(ui): document that with_wallets() only extracts Core and Platform
lklimek Mar 27, 2026
a404376
fix(ui): default identity source to highest balance identity
lklimek Mar 27, 2026
e31203e
fix(ui): prevent self-send by filtering source identity from destinat…
lklimek Mar 27, 2026
6213ddb
feat(ui): split Platform→Shielded transactions for privacy
lklimek Mar 27, 2026
89d335e
Revert "feat(ui): split Platform→Shielded transactions for privacy"
lklimek Mar 27, 2026
174d510
fix(ui): shield credits from multiple platform addresses
lklimek Mar 27, 2026
a255582
fix: resolve clippy and fmt CI failures
lklimek Mar 27, 2026
74e2114
refactor: address code review findings from PR #801
lklimek Mar 27, 2026
1ce3fc4
perf(ui): avoid per-frame DB queries and unnecessary clone in Send sc…
lklimek Mar 27, 2026
7b64db6
fix(ui): validate identity self-send at dispatch time
lklimek Mar 27, 2026
5551c0f
refactor(send): replace inverted if-let guard with idiomatic !matches!
lklimek Mar 27, 2026
43485fb
fix(review): address PR #801 review findings
lklimek Mar 27, 2026
893f770
docs(shielded): consolidate duplicate doc comment summary lines
lklimek Mar 27, 2026
bf17628
refactor(shielded): extract restrict_utxos closure into a named helpe…
lklimek Mar 27, 2026
8c8b077
feat(shielded): replace hardcoded fee headroom with dynamic estimation
lklimek Mar 27, 2026
4d76762
refactor(wallet): thread source_address filter through UTXO selection
lklimek Mar 27, 2026
ab7da8a
refactor(shielded): replace fee headroom estimation with iterative no…
lklimek Mar 27, 2026
1e9e4fe
fix(shielded): harden shield screen with graceful locks, theme colors…
lklimek Mar 27, 2026
7d677bf
fix(ui): use theme-aware colors in shield screen for dark mode support
lklimek Mar 27, 2026
9a6f832
refactor(fees): centralize shield-from-core fee estimation in fee_est…
lklimek Mar 27, 2026
b22f370
docs: note fee estimation centralization rule in CLAUDE.md
lklimek Mar 27, 2026
063623e
fix(shielded): address review findings — fee guard, L1 fee, lock safe…
lklimek Mar 27, 2026
d6af76b
fix(shielded): apply fee multiplier, freeze batch inputs, preserve co…
lklimek Mar 27, 2026
46a274a
fix(shielded): skip stale-nonce items in parallel batch instead of ca…
lklimek Mar 27, 2026
67344ca
docs: add typed error matching rule to CLAUDE.md
lklimek Mar 27, 2026
fcc5ffb
fix(ui): restore vibrant progress bar fill colors
lklimek Mar 27, 2026
c03ccf1
fix(shielded): don't cascade-fail on any nonce mismatch in parallel b…
lklimek Mar 27, 2026
66227ac
fix(shielded): handle nonce error via Protocol path, not just Broadca…
lklimek Mar 27, 2026
c5b9a91
fix(ui): use shared fee estimation and theme colors in send screen
lklimek Mar 27, 2026
d7b5dbc
fix(shielded): require block confirmation between parallel batch broa…
lklimek Mar 27, 2026
ef3223e
Merge remote-tracking branch 'origin/feat/unified-shield-screen' into…
lklimek Mar 30, 2026
108aa08
style: fix formatting after merge
lklimek Mar 30, 2026
358ef68
Merge remote-tracking branch 'origin/v1.0-dev' into fix/send-dash-scr…
lklimek Mar 30, 2026
ae7b347
fix(review): address backend review findings from PR #802 triage
lklimek Mar 30, 2026
f5edcd3
fix(ui): address UI review findings from PR #802 triage
lklimek Mar 30, 2026
2c9fe49
Merge branch 'v1.0-dev' into fix/send-dash-screen-improvements
lklimek Mar 30, 2026
c829414
fix(review): address PR #802 review comments
lklimek Mar 30, 2026
827bfa8
Merge branch 'fix/send-dash-screen-improvements' of https://github.co…
lklimek Mar 30, 2026
47b9fc1
fix(shielded): wait for block confirmation after broadcast before ret…
lklimek Mar 30, 2026
ed08be7
fix(review): lock ordering in shield_screen, theme colors in unshield…
lklimek Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/backend_task/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,8 @@ impl AppContext {
let client = match Client::new(&addr, Auth::CookieFile(cookie_path.clone())) {
Ok(client) => client,
Err(_) => {
tracing::debug!(
"Failed to authenticate using .cookie file at {:?}, falling back to user/pass",
tracing::trace!(
"Cookie auth unavailable at {:?}, using user/pass",
cookie_path
);
match Client::new(
Expand Down
159 changes: 63 additions & 96 deletions src/backend_task/shielded/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::context::AppContext;
use crate::context::shielded::get_proving_key;
use crate::model::fee_estimation::{format_credits_as_dash, shielded_fee_for_actions};
use crate::model::wallet::WalletSeedHash;
use crate::model::wallet::shielded::ShieldedWalletState;
use crate::model::wallet::shielded::{ShieldedNote, ShieldedWalletState};
use dash_sdk::dpp::address_funds::{
AddressFundsFeeStrategy, AddressFundsFeeStrategyStep, OrchardAddress, PlatformAddress,
};
Expand All @@ -15,10 +15,12 @@ use dash_sdk::dpp::shielded::builder::{
};
use dash_sdk::dpp::version::PlatformVersion;
use dash_sdk::dpp::withdrawal::Pooling;
use dash_sdk::grovedb_commitment_tree::{Nullifier, PaymentAddress, ProvingKey};
use dash_sdk::grovedb_commitment_tree::{
Anchor, ClientPersistentCommitmentTree, Nullifier, PaymentAddress, ProvingKey,
};
use dash_sdk::platform::transition::broadcast::BroadcastStateTransition;
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use std::sync::{Arc, Mutex, MutexGuard};

/// Wrapper around a cached `ProvingKey` that implements `OrchardProver`.
struct CachedProver {
Expand Down Expand Up @@ -98,7 +100,7 @@ pub fn build_shield_credit(
let recipient_addr = payment_address_to_orchard(recipient_payment_address)?;

let wallet_arc = {
let wallets = app_context.wallets.read().unwrap();
let wallets = app_context.wallets.read()?;
wallets
.get(seed_hash)
.cloned()
Expand All @@ -111,7 +113,8 @@ pub fn build_shield_credit(
let fee_strategy: AddressFundsFeeStrategy =
vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];

let wallet = wallet_arc.read().unwrap();
let wallet = wallet_arc.read()?;
// memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo
build_shield_transition(
&recipient_addr,
amount,
Expand Down Expand Up @@ -148,7 +151,7 @@ pub async fn shield_credits(
let recipient_addr = payment_address_to_orchard(recipient_payment_address)?;

let wallet_arc = {
let wallets = app_context.wallets.read().unwrap();
let wallets = app_context.wallets.read()?;
wallets
.get(seed_hash)
.cloned()
Expand All @@ -158,7 +161,7 @@ pub async fn shield_credits(
let nonce: u32 = if let Some(n) = nonce_override {
n
} else {
let wallet = wallet_arc.read().unwrap();
let wallet = wallet_arc.read()?;
wallet
.platform_address_info
.iter()
Expand Down Expand Up @@ -187,11 +190,12 @@ pub async fn shield_credits(
);

if let Some(s) = &stage {
*s.lock().unwrap() = ShieldStage::BuildingProof { nonce };
*s.lock()? = ShieldStage::BuildingProof { nonce };
}

let state_transition = {
let wallet = wallet_arc.read().unwrap();
let wallet = wallet_arc.read()?;
// memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo
build_shield_transition(
&recipient_addr,
amount,
Expand All @@ -207,7 +211,7 @@ pub async fn shield_credits(
};

if let Some(s) = &stage {
*s.lock().unwrap() = ShieldStage::Broadcasting;
*s.lock()? = ShieldStage::Broadcasting;
}

tracing::trace!("Shield credits: state transition built, broadcasting...");
Expand Down Expand Up @@ -269,35 +273,13 @@ pub async fn shielded_transfer(
let spent_nullifiers: Vec<Nullifier> = spendable_notes.iter().map(|n| n.nullifier).collect();

let (spends, anchor) = {
let tree = shielded_state.commitment_tree.lock().unwrap();
let spends = spendable_notes
.iter()
.map(|note| {
let merkle_path = tree
.witness(note.position, 0)
.map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable {
detail: e.to_string(),
})?
.ok_or(TaskError::ShieldedMerkleWitnessUnavailable {
detail: "No Merkle path available for note".into(),
})?;
Ok(SpendableNote {
note: note.note,
merkle_path,
})
})
.collect::<Result<Vec<_>, TaskError>>()?;

let anchor = tree
.anchor()
.map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable {
detail: e.to_string(),
})?;
(spends, anchor)
let tree = shielded_state.commitment_tree.lock()?;
extract_spends_and_anchor(&tree, &spendable_notes)?
};

let change_addr = payment_address_to_orchard(&shielded_state.keys.default_address)?;

// memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo
let state_transition = build_shielded_transfer_transition(
spends,
&recipient_addr,
Expand Down Expand Up @@ -367,35 +349,13 @@ pub async fn unshield_credits(
let spent_nullifiers: Vec<Nullifier> = spendable_notes.iter().map(|n| n.nullifier).collect();

let (spends, anchor) = {
let tree = shielded_state.commitment_tree.lock().unwrap();
let spends = spendable_notes
.iter()
.map(|note| {
let merkle_path = tree
.witness(note.position, 0)
.map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable {
detail: e.to_string(),
})?
.ok_or(TaskError::ShieldedMerkleWitnessUnavailable {
detail: "No Merkle path available for note".into(),
})?;
Ok(SpendableNote {
note: note.note,
merkle_path,
})
})
.collect::<Result<Vec<_>, TaskError>>()?;

let anchor = tree
.anchor()
.map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable {
detail: e.to_string(),
})?;
(spends, anchor)
let tree = shielded_state.commitment_tree.lock()?;
extract_spends_and_anchor(&tree, &spendable_notes)?
};

let change_addr = payment_address_to_orchard(&shielded_state.keys.default_address)?;

// memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo
let state_transition = build_unshield_transition(
spends,
to_platform_address,
Expand Down Expand Up @@ -457,7 +417,7 @@ pub async fn shield_from_asset_lock(
// Step 1: Create the asset lock transaction
let (asset_lock_transaction, asset_lock_private_key, _asset_lock_address, used_utxos) = {
let wallet_arc = {
let wallets = app_context.wallets.read().unwrap();
let wallets = app_context.wallets.read()?;
wallets
.get(seed_hash)
.cloned()
Expand Down Expand Up @@ -501,10 +461,7 @@ pub async fn shield_from_asset_lock(

// Step 2: Register this transaction as waiting for finality
{
let mut proofs = app_context
.transactions_waiting_for_finality
.lock()
.unwrap();
let mut proofs = app_context.transactions_waiting_for_finality.lock()?;
proofs.insert(tx_id, None);
}

Expand All @@ -520,7 +477,7 @@ pub async fn shield_from_asset_lock(
// Step 4: Remove used UTXOs from wallet
{
let wallet_arc = {
let wallets = app_context.wallets.read().unwrap();
let wallets = app_context.wallets.read()?;
wallets
.get(seed_hash)
.cloned()
Expand Down Expand Up @@ -573,7 +530,7 @@ pub async fn shield_from_asset_lock(
return Err(TaskError::ShieldedAssetLockTimeout);
}
_ = tokio::time::sleep(Duration::from_millis(200)) => {
let proofs = app_context.transactions_waiting_for_finality.lock().unwrap();
let proofs = app_context.transactions_waiting_for_finality.lock()?;
if let Some(Some(proof)) = proofs.get(&tx_id) {
asset_lock_proof = proof.clone();
break;
Expand All @@ -584,10 +541,7 @@ pub async fn shield_from_asset_lock(

// Step 6: Clean up the finality tracking
{
let mut proofs = app_context
.transactions_waiting_for_finality
.lock()
.unwrap();
let mut proofs = app_context.transactions_waiting_for_finality.lock()?;
proofs.remove(&tx_id);
}

Expand All @@ -611,6 +565,7 @@ pub async fn shield_from_asset_lock(
shield_amount_credits,
);

// memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo
let state_transition = build_shield_from_asset_lock_transition(
&recipient,
shield_amount_credits,
Expand Down Expand Up @@ -677,35 +632,13 @@ pub async fn shielded_withdrawal(
let spent_nullifiers: Vec<Nullifier> = spendable_notes.iter().map(|n| n.nullifier).collect();

let (spends, anchor) = {
let tree = shielded_state.commitment_tree.lock().unwrap();
let spends = spendable_notes
.iter()
.map(|note| {
let merkle_path = tree
.witness(note.position, 0)
.map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable {
detail: e.to_string(),
})?
.ok_or(TaskError::ShieldedMerkleWitnessUnavailable {
detail: "No Merkle path available for note".into(),
})?;
Ok(SpendableNote {
note: note.note,
merkle_path,
})
})
.collect::<Result<Vec<_>, TaskError>>()?;

let anchor = tree
.anchor()
.map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable {
detail: e.to_string(),
})?;
(spends, anchor)
let tree = shielded_state.commitment_tree.lock()?;
extract_spends_and_anchor(&tree, &spendable_notes)?
};

let change_addr = payment_address_to_orchard(&shielded_state.keys.default_address)?;

// memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo
let state_transition = build_shielded_withdrawal_transition(
spends,
amount,
Expand Down Expand Up @@ -833,6 +766,40 @@ fn select_notes_for_amount(
Ok((selected, accumulated))
}

/// Extract spendable notes with Merkle witnesses and the tree anchor.
///
/// Locks the commitment tree, computes a Merkle path for each selected note,
/// and returns them alongside the current tree anchor for proof construction.
fn extract_spends_and_anchor(
tree: &MutexGuard<'_, ClientPersistentCommitmentTree>,
notes: &[&ShieldedNote],
) -> Result<(Vec<SpendableNote>, Anchor), TaskError> {
let spends = notes
.iter()
.map(|note| {
let merkle_path = tree
.witness(note.position, 0)
.map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable {
detail: e.to_string(),
})?
.ok_or(TaskError::ShieldedMerkleWitnessUnavailable {
detail: "No Merkle path available for note".into(),
})?;
Ok(SpendableNote {
note: note.note,
merkle_path,
})
})
.collect::<Result<Vec<_>, TaskError>>()?;

let anchor = tree
.anchor()
.map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable {
detail: e.to_string(),
Comment thread
lklimek marked this conversation as resolved.
})?;
Ok((spends, anchor))
}

/// Convert a PaymentAddress to an OrchardAddress for the builder functions.
fn payment_address_to_orchard(addr: &PaymentAddress) -> Result<OrchardAddress, TaskError> {
let raw = addr.to_raw_address_bytes();
Expand Down
4 changes: 2 additions & 2 deletions src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,8 @@ impl AppContext {
if let Ok(client) = Client::new(url, Auth::CookieFile(cookie_path.clone())) {
return Ok(client);
}
tracing::debug!(
"Failed to authenticate using .cookie file at {:?}, falling back to user/pass",
tracing::trace!(
"Cookie auth unavailable at {:?}, using user/pass",
cookie_path,
);
}
Expand Down
Loading
Loading