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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
interactive there, and adding it to the remaining screens, is tracked as a
follow-up.

### Fixed

- **Shielded availability notice**: now distinguishes when the connected network
does not support shielded sending from when the current interface mode does
not unlock it.

### Changed

- **Shielded transactions are available on supported networks**: sending,
receiving, shielding, and unshielding are enabled when the connected network's
protocol version supports them, including mainnet. These operations were
previously gated off everywhere pending upstream activation.

- **The first launch after an upgrade asks for each password-protected wallet's
password**: the app moves your wallets into a new storage format on that first
launch, and it needs each protected wallet's password to finish the move for
Expand Down
8 changes: 5 additions & 3 deletions docs/user-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ As a developer, I want to shield DASH directly from my Core wallet so that I can
- Select Core Wallet source and enter a shielded address as destination.
- System creates an asset lock, waits for proof, and shields the credits.
- Progress banner shows multi-step operation status.
- Developer mode required.
- Available only on Platform protocol v12 or later when Expert view or Developer view is selected.

### SND-008: Top up identity from Send screen [Implemented]
**Persona:** Priya, Jordan
Expand All @@ -391,15 +391,15 @@ As a developer, I want to shield credits from a Platform address into the shield

- Select Platform Addresses as source and enter a shielded address as destination.
- System auto-selects the highest-balance Platform address.
- Developer mode required.
- Available only on Platform protocol v12 or later when Expert view or Developer view is selected.

### SND-010: Withdraw from shielded pool to Core address [Implemented]
**Persona:** Jordan

As a developer, I want to withdraw from the shielded pool directly to a Core address so that I can convert shielded credits back to spendable DASH.

- Select Shielded Pool as source and enter a Core address as destination.
- Developer mode required.
- Available only on Platform protocol v12 or later when Expert view or Developer view is selected.

### SND-011: Transfer identity credits to another identity [Implemented]
**Persona:** Priya, Jordan
Expand Down Expand Up @@ -443,6 +443,7 @@ As a developer, I want to move credits out of the shielded pool to one of my Pla
- Select Shielded Pool as source and enter a Platform address as destination.
- Reachable from the Shielded tab's "Unshield" button, which opens the unified Send screen preset for this flow.
- The shielded balance decreases and the Platform address balance increases after the operation completes.
- Available only on Platform protocol v12 or later when Expert view or Developer view is selected.

### SND-016: Send privately within the shielded pool [Implemented]
**Persona:** Jordan
Expand All @@ -452,6 +453,7 @@ As a developer, I want to transfer credits privately from my shielded pool to an
- Select Shielded Pool as source and enter a shielded address as destination.
- Reachable from the Shielded tab's "Send (Private)" button, which opens the unified Send screen preset for this flow.
- Spending is paused until the shielded balance is verified, and the button is disabled with a clear reason while verification is in progress.
- Available only on Platform protocol v12 or later when Expert view or Developer view is selected.

---

Expand Down
15 changes: 11 additions & 4 deletions src/backend_task/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1901,12 +1901,19 @@ pub enum TaskError {
// ──────────────────────────────────────────────────────────────────────────
// Shielded pool errors
// ──────────────────────────────────────────────────────────────────────────
/// A fund-moving shielded operation was requested while the shielded
/// operations feature gate was closed.
/// A fund-moving shielded operation was requested on a network that does
/// not support shielded state transitions.
#[error(
"Shielding, sending, or withdrawing shielded funds is not available right now. Use a regular payment instead, or try again after a future update."
"Shielded operations are not available on this network yet. Use a regular payment instead, or try again after a future network update."
)]
ShieldedOperationsUnavailable,
ShieldedOperationsNetworkUnavailable,

/// A fund-moving shielded operation was requested from an interface mode
/// that does not unlock experimental features.
#[error(
"Shielded operations need Expert view or higher. Switch your interface mode in Settings to use them."
)]
ShieldedOperationsRoleUnavailable,

/// No unspent shielded notes are available.
#[error("You have no shielded funds available. Please shield some credits first.")]
Expand Down
20 changes: 10 additions & 10 deletions src/backend_task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use crate::backend_task::platform_info::{PlatformInfoTaskRequestType, PlatformIn
use crate::backend_task::system_task::SystemTask;
use crate::backend_task::wallet::WalletTask;
use crate::context::AppContext;
use crate::context::feature_gate::FeatureGate;
use crate::context::identity_load_registry::IdentityLoadToken;
use crate::model::masternode_input::decode_identity_id;
use dash_sdk::dpp::address_funds::PlatformAddress;
Expand Down Expand Up @@ -869,9 +868,9 @@ impl AppContext {
// lock/await/secret), so it is safe to call before backend init. The
// in-handler gate in `run_shielded_task` stays as the authoritative check.
if let BackendTask::ShieldedTask(_) = &task
&& !FeatureGate::ShieldedOperations.is_available(self)
&& let Some(error) = shielded::shielded_operations_unavailable_error(self)
{
return Err(TaskError::ShieldedOperationsUnavailable);
return Err(error);
}

let _contact_request_claim = match dashpay_request_id(&task) {
Expand Down Expand Up @@ -1181,6 +1180,7 @@ impl AppContext {
#[cfg(test)]
mod tests {
use super::*;
use crate::context::feature_gate::FeatureGate;

fn dapi_connection_refused_error() -> TaskError {
use dash_sdk::Error as SdkError;
Expand Down Expand Up @@ -1562,11 +1562,9 @@ mod tests {
}
}

/// The shielded pre-check runs before the migration gate, so an *unavailable*
/// shielded write is refused with `ShieldedOperationsUnavailable` even while a
/// storage update collects wallet passwords — the accurate, actionable message
/// ("shielded is not available") rather than the misleading "wait for the
/// update", since waiting will never make shielded available.
/// The shielded pre-check runs before the migration gate, so a role-gated
/// shielded write reports the interface mode needed to unlock it even while a
/// storage update collects wallet passwords.
///
/// The migration gate for shielded still applies once shielded operations
/// ship (the pre-check passes, then the gate short-circuits); its
Expand All @@ -1579,9 +1577,11 @@ mod tests {
use crate::backend_task::shielded::ShieldedTask;
use crate::context::migration_status::MigrationState;
use crate::context::test_support::test_app_context;
use dash_sdk::dpp::version::feature_initial_protocol_versions::SHIELDED_POOL_INITIAL_PROTOCOL_VERSION;

let tmp = tempfile::tempdir().expect("tempdir");
let ctx = test_app_context(tmp.path());
ctx.set_platform_protocol_version(SHIELDED_POOL_INITIAL_PROTOCOL_VERSION);
assert!(!FeatureGate::ShieldedOperations.is_available(&ctx));
let (tx, _rx) = tokio::sync::mpsc::channel::<TaskResult>(32);
let sender = SenderAsync::new(tx, ctx.egui_ctx().clone());
Expand All @@ -1602,8 +1602,8 @@ mod tests {
)
.await;
assert!(
matches!(result, Err(TaskError::ShieldedOperationsUnavailable)),
"the shielded pre-check must refuse an unavailable write before the migration gate, got {result:?}",
matches!(result, Err(TaskError::ShieldedOperationsRoleUnavailable)),
"the shielded pre-check must report the role gate before the migration gate, got {result:?}",
);

if let Ok(backend) = ctx.wallet_backend() {
Expand Down
54 changes: 49 additions & 5 deletions src/backend_task/shielded/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::backend_task::BackendTaskSuccessResult;
use crate::backend_task::error::TaskError;
use crate::context::AppContext;
use crate::context::feature_gate::FeatureGate;
use crate::context::feature_gate::{Check, FeatureGate};
use crate::model::wallet::WalletSeedHash;
use crate::wallet_backend::PlatformPathIndex;
use dash_sdk::dpp::address_funds::{OrchardAddress, PlatformAddress};
Expand Down Expand Up @@ -55,6 +55,16 @@ pub enum ShieldedTask {
},
}

pub(super) fn shielded_operations_unavailable_error(ctx: &AppContext) -> Option<TaskError> {
match FeatureGate::ShieldedOperations.first_unmet_check(ctx) {
None => None,
Some(Check::Capability(_)) => Some(TaskError::ShieldedOperationsNetworkUnavailable),
Some(Check::MinRole(_) | Check::Experimental(_)) => {
Some(TaskError::ShieldedOperationsRoleUnavailable)
}
}
}

impl AppContext {
/// Run a shielded-pool task by forwarding to the upstream coordinator
/// through the [`WalletBackend`](crate::wallet_backend::WalletBackend)
Expand All @@ -70,11 +80,12 @@ impl AppContext {
self: &Arc<Self>,
task: ShieldedTask,
) -> Result<BackendTaskSuccessResult, TaskError> {
if !FeatureGate::ShieldedOperations.is_available(self) {
if let Some(error) = shielded_operations_unavailable_error(self) {
tracing::warn!(
?error,
"Refused a shielded fund movement because shielded operations are unavailable"
);
return Err(TaskError::ShieldedOperationsUnavailable);
return Err(error);
}

let backend = self.wallet_backend()?;
Expand Down Expand Up @@ -258,11 +269,41 @@ mod tests {
let result = ctx.run_backend_task(task, sender).await;

assert!(
matches!(&result, Err(TaskError::ShieldedOperationsUnavailable)),
matches!(
&result,
Err(TaskError::ShieldedOperationsNetworkUnavailable)
),
"a direct backend dispatch must reject unsupported shielded writes before moving funds: {result:?}"
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shielded_handler_reports_role_gate_before_touching_the_backend() {
use dash_sdk::dpp::version::feature_initial_protocol_versions::SHIELDED_POOL_INITIAL_PROTOCOL_VERSION;

let tmp = tempfile::tempdir().expect("tempdir");
let ctx = test_app_context(tmp.path());
ctx.set_platform_protocol_version(SHIELDED_POOL_INITIAL_PROTOCOL_VERSION);
assert!(!FeatureGate::ShieldedOperations.is_available(&ctx));
assert!(ctx.wallet_backend().is_err(), "precondition");

let result = ctx
.run_shielded_task(ShieldedTask::ShieldFromAssetLock {
seed_hash: WalletSeedHash::default(),
amount_duffs: 1,
})
.await;

assert!(
matches!(&result, Err(TaskError::ShieldedOperationsRoleUnavailable)),
"the handler must explain that the user's role blocks shielded operations: {result:?}"
);
assert!(
ctx.wallet_backend().is_err(),
"the role gate must return before the handler touches the wallet backend"
);
}

/// The shielded pre-check in `run_backend_task` refuses an unavailable
/// shielded write *before* `ensure_wallet_backend` wires the backend — which
/// would otherwise materialize seeds, register upstream, and bind Orchard for
Expand All @@ -289,7 +330,10 @@ mod tests {
let result = ctx.run_backend_task(task, sender).await;

assert!(
matches!(&result, Err(TaskError::ShieldedOperationsUnavailable)),
matches!(
&result,
Err(TaskError::ShieldedOperationsNetworkUnavailable)
),
"the pre-check must reject the shielded write: {result:?}"
);
assert!(
Expand Down
Loading
Loading