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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
stay unavailable and the app keeps retrying, instead of assuming the version
the app was built with.

- **Fewer connection failures during unrelated actions**: the app kept asking
the network for epoch details through a request every server currently
refuses. Each attempt consumed part of the app's shared request allowance, so
other actions — adding funds to an identity, for example — could fail with a
connection error. That request is now paused until the upstream fix is
released. The send-fee rate it was meant to refresh is the standard rate every
network charges today, so fees are unchanged, and the Platform Info screen now
says plainly that the rate shown is fixed rather than read from the network.

### Changed

- **Upstream wallet backend updated (`platform-wallet` / `platform-wallet-storage`)**:
Expand Down
172 changes: 124 additions & 48 deletions src/backend_task/platform_info.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::backend_task::BackendTaskSuccessResult;
use crate::backend_task::error::TaskError;
use crate::context::AppContext;
use crate::model::fee_estimation::PlatformFeeEstimator;
use dash_sdk::Error as SdkError;
use dash_sdk::Sdk;
use dash_sdk::dashcore_rpc::RpcApi;
Expand Down Expand Up @@ -187,6 +188,9 @@ pub enum WithdrawalParseError {
}

// Helper functions for formatting platform data
/// Kept for the restore path of the disabled live epoch fetch; see the
/// `TODO(platform#4231)` in the `CurrentEpochInfo` arm.
#[allow(dead_code)]
fn format_extended_epoch_info(
epoch_info: ExtendedEpochInfo,
network: Network,
Expand Down Expand Up @@ -251,21 +255,30 @@ fn format_extended_epoch_info(
}

/// `protocol_version` is `None` while the connected network has not confirmed one.
fn format_unavailable_current_epoch_info(protocol_version: Option<u32>) -> String {
/// The fee multiplier is a fixed value, not a network reading — see the
/// `TODO(platform#4231)` in the `CurrentEpochInfo` arm.
fn format_hardcoded_current_epoch_info(
protocol_version: Option<u32>,
fee_multiplier_permille: u64,
) -> String {
let fee_multiplier = fee_multiplier_permille as f64 / 1000.0;
match protocol_version {
Some(protocol_version) => format!(
"Current Epoch Information:\n\
• Protocol Version: {protocol_version}\n\
• Epoch details and fee multiplier are temporarily unavailable while \
dashpay/platform#4231 is unresolved.\n\n\
(The fee multiplier cache was not updated.)"
• Fee Multiplier: {fee_multiplier}x (a fixed value, not read from the network)\n\n\
Epoch details cannot be read while dashpay/platform#4231 is unresolved. The fee \
multiplier shown is the one every network charges today, and it will be read live \
again once that fix is released."
),
None => "Current Epoch Information:\n\
None => format!(
"Current Epoch Information:\n\
• Protocol Version: the connected network has not confirmed one yet.\n\
• Epoch details and fee multiplier are temporarily unavailable while \
dashpay/platform#4231 is unresolved.\n\n\
(The fee multiplier cache was not updated.)"
.to_string(),
• Fee Multiplier: {fee_multiplier}x (a fixed value, not read from the network)\n\n\
Epoch details cannot be read while dashpay/platform#4231 is unresolved. The fee \
multiplier shown is the one every network charges today, and it will be read live \
again once that fix is released."
),
}
}
Comment on lines +258 to 283

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Raw issue-tracker reference exposed in user-facing text.

Both arms embed dashpay/platform#4231 directly in text returned as PlatformInfoTaskResult::TextResult, which is what users see. A GitHub issue reference is an internal implementation detail; a typical user cannot act on it and it is not calm, plain-language wording. Consider rephrasing to describe the limitation in plain terms (for example, "a known upstream network issue") and keep the tracker reference in logs or developer-facing details only.

Based on path instructions for src/**/*.rs: "User-facing error messages must be calm, brief, jargon-free, actionable, and describe what happened plus what the user can do. Do not expose raw errors, stack traces, SDK details, or error codes; attach technical details with BannerHandle::with_details."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend_task/platform_info.rs` around lines 258 - 283, Update both
user-facing message arms in format_hardcoded_current_epoch_info to remove the
raw dashpay/platform#4231 tracker reference and replace it with calm,
plain-language wording such as a known upstream network issue. Keep the
limitation and expected future live-reading behavior clear, while retaining any
technical reference only in developer-facing logging or details if already
supported.

Source: Path instructions


Expand Down Expand Up @@ -584,39 +597,57 @@ impl AppContext {
),
}

match ExtendedEpochInfo::fetch_current(sdk).await {
Ok(epoch_info) => {
let fee_multiplier = epoch_info.fee_multiplier_permille();
self.set_fee_multiplier_permille(fee_multiplier);
self.set_platform_protocol_version(epoch_info.protocol_version());

let mut formatted =
format_extended_epoch_info(epoch_info, self.network, true);
formatted.push_str(&format!(
"\n\n(Fee multiplier cache updated: {}x)",
fee_multiplier as f64 / 1000.0
));
Ok(BackendTaskSuccessResult::PlatformInfo(
PlatformInfoTaskResult::TextResult(formatted),
))
}
Err(error) => {
tracing::warn!(
%error,
"Current epoch fetch is blocked by dashpay/platform#4231; \
keeping the cached fee multiplier"
);
let confirmed = match self.platform_protocol_version() {
0 => None,
version => Some(version),
};
Ok(BackendTaskSuccessResult::PlatformInfo(
PlatformInfoTaskResult::TextResult(
format_unavailable_current_epoch_info(confirmed),
),
))
}
}
// TODO(platform#4231): restore the commented-out live fetch below and drop the
// hardcoded multiplier once https://github.com/dashpay/platform/pull/4231 merges
// and this repo's platform pin (Cargo.toml/Cargo.lock rev a18bd158…) advances past
// it. The proof verifier in that pin rejects the descending-epoch-without-start
// query shape `fetch_current` sends, so the call fails identically on every DAPI
// node: each attempt cycles the SDK's whole address pool and burns the shared
// per-client request budget, which surfaced as `DapiAllAddressesExhausted` in
// unrelated flows such as identity top-up. This task also runs automatically on
// every SPV Syncing→Synced transition, so the cost is not user-paced.
//
// 1000 permille (1.0x) is what the network actually stores, not a placeholder: an
// epoch's multiplier is written from
// `platform_version.fee_version.uses_version_fee_multiplier_permille`
// (rs-drive-abci/src/execution/platform_events/block_processing_end_events/
// add_process_epoch_change_operations/v0/mod.rs:107) and both fee schedules in the
// pinned crate declare `Some(1000)` — rs-platform-version/src/version/fee/v1.rs:13
// and v2.rs:14, checked 2026-07-31 at rev a18bd158.
//
// match ExtendedEpochInfo::fetch_current(sdk).await {
// Ok(epoch_info) => {
// let fee_multiplier = epoch_info.fee_multiplier_permille();
// self.set_fee_multiplier_permille(fee_multiplier);
// self.set_platform_protocol_version(epoch_info.protocol_version());
//
// let mut formatted =
// format_extended_epoch_info(epoch_info, self.network, true);
// formatted.push_str(&format!(
// "\n\n(Fee multiplier cache updated: {}x)",
// fee_multiplier as f64 / 1000.0
// ));
// Ok(BackendTaskSuccessResult::PlatformInfo(
// PlatformInfoTaskResult::TextResult(formatted),
// ))
// }
// // Restoring keeps a degraded arm here: log, then fall back to
// // `format_hardcoded_current_epoch_info` with the cached multiplier.
// Err(error) => { ... }
// }
let fee_multiplier = PlatformFeeEstimator::DEFAULT_FEE_MULTIPLIER_PERMILLE;
self.set_fee_multiplier_permille(fee_multiplier);

let confirmed = match self.platform_protocol_version() {
0 => None,
version => Some(version),
};
Ok(BackendTaskSuccessResult::PlatformInfo(
PlatformInfoTaskResult::TextResult(format_hardcoded_current_epoch_info(
confirmed,
fee_multiplier,
)),
))
}
PlatformInfoTaskRequestType::TotalCreditsOnPlatform => {
let total_credits = TotalCreditsInPlatform::fetch_current(sdk)
Expand Down Expand Up @@ -939,27 +970,72 @@ mod tests {
use super::*;

#[test]
fn epoch_workaround_reports_protocol_version_and_stale_fee_cache() {
fn epoch_workaround_reports_protocol_version_and_the_hardcoded_fee_multiplier() {
assert_eq!(
format_unavailable_current_epoch_info(Some(12)),
format_hardcoded_current_epoch_info(Some(12), 1000),
"Current Epoch Information:\n\
• Protocol Version: 12\n\
• Epoch details and fee multiplier are temporarily unavailable while \
dashpay/platform#4231 is unresolved.\n\n\
(The fee multiplier cache was not updated.)"
• Fee Multiplier: 1x (a fixed value, not read from the network)\n\n\
Epoch details cannot be read while dashpay/platform#4231 is unresolved. The fee \
multiplier shown is the one every network charges today, and it will be read live \
again once that fix is released."
);
}

/// The multiplier is presented as fixed, never as a live reading: a user
/// comparing it against a network that raised its fees must be able to see
/// which of the two the app is charging by.
#[test]
fn epoch_workaround_never_presents_the_fee_multiplier_as_a_network_reading() {
for protocol_version in [None, Some(12)] {
let formatted = format_hardcoded_current_epoch_info(protocol_version, 1500);
assert!(
formatted
.contains("• Fee Multiplier: 1.5x (a fixed value, not read from the network)"),
"the multiplier must be shown as fixed, got: {formatted}"
);
}
}

#[test]
fn epoch_workaround_never_reports_an_unconfirmed_protocol_version_as_a_number() {
let formatted = format_unavailable_current_epoch_info(None);
let formatted = format_hardcoded_current_epoch_info(None, 1000);
assert!(
formatted
.contains("• Protocol Version: the connected network has not confirmed one yet."),
"an unconfirmed version must be named as such, got: {formatted}"
);
}

/// Fee estimates must not keep running on whatever multiplier a previous
/// refresh happened to leave behind: the task republishes the hardcoded one.
#[tokio::test]
async fn epoch_workaround_republishes_the_hardcoded_fee_multiplier() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let ctx = crate::context::test_support::test_app_context(temp_dir.path());
let sdk = dash_sdk::Sdk::new_mock();
ctx.set_fee_multiplier_permille(7_777);

let result = ctx
.run_platform_info_task(PlatformInfoTaskRequestType::CurrentEpochInfo, &sdk)
.await
.expect("the epoch workaround degrades to a text result");

assert_eq!(
ctx.fee_multiplier_permille(),
PlatformFeeEstimator::DEFAULT_FEE_MULTIPLIER_PERMILLE
);
let BackendTaskSuccessResult::PlatformInfo(PlatformInfoTaskResult::TextResult(text)) =
result
else {
panic!("the epoch workaround returns a text result");
};
assert!(
text.contains("• Fee Multiplier: 1x"),
"the reported multiplier must match the cached one, got: {text}"
);
}

/// A failed ratchet trigger leaves the SDK reporting its local seed, which is
/// not a network observation: caching it would open the shielded capability
/// gate and defeat the `0` retry sentinel `mcp::resolve` polls.
Expand Down
62 changes: 26 additions & 36 deletions src/mcp/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,44 +408,31 @@ pub(crate) fn qualified_identity(
mod tests {
use super::*;
use dash_sdk::Sdk;
use dash_sdk::dpp::block::extended_epoch_info::ExtendedEpochInfo;
use dash_sdk::dpp::block::extended_epoch_info::v0::ExtendedEpochInfoV0;
use dash_sdk::platform::LimitQuery;
use dash_sdk::platform::types::epoch::EpochQuery;

async fn mock_sdk_with_current_epoch(protocol_version: u32) -> Sdk {
let epoch_info = ExtendedEpochInfo::V0(ExtendedEpochInfoV0 {
index: 42,
first_block_time: 1,
first_block_height: 2,
first_core_block_height: 3,
fee_multiplier_permille: 1_000,
protocol_version,
});
let current_epoch_query = LimitQuery {
query: EpochQuery {
start: None,
ascending: false,
},
limit: Some(1),
start_info: None,
};
use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters;
use dash_sdk::dpp::version::LATEST_VERSION;
use dash_sdk::platform::DataContract;

/// Mocks the proved DPNS-contract fetch that drives the protocol-version
/// ratchet — the only network read the epoch task still performs while
/// dashpay/platform#4231 is unresolved. The mock reports `LATEST_VERSION` in
/// the response metadata, so a successful fetch ratchets the SDK exactly as a
/// live network would; a failed one leaves the version unconfirmed.
async fn mock_sdk_with_dpns_contract(ctx: &Arc<AppContext>) -> Sdk {
let contract = DataContract::clone(&ctx.dpns_contract);
let mut sdk = Sdk::new_mock();
sdk.mock()
.expect_fetch(current_epoch_query, Some(epoch_info))
.expect_fetch(contract.id(), Some(contract))
.await
.expect("register current epoch response");
.expect("register DPNS contract response");
sdk
}

#[tokio::test]
async fn headless_sync_populates_protocol_version_after_spv_is_running() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let ctx = crate::mcp::tests::legacy_wallet_context(temp_dir.path());
let protocol_version = 12;
ctx.sdk.store(Arc::new(
mock_sdk_with_current_epoch(protocol_version).await,
));
ctx.sdk
.store(Arc::new(mock_sdk_with_dpns_contract(&ctx).await));
ctx.connection_status().set_spv_status(SpvStatus::Running);

assert_eq!(ctx.platform_protocol_version(), 0, "boot value");
Expand All @@ -454,23 +441,26 @@ mod tests {
.await
.expect("headless sync completion");

assert_eq!(ctx.platform_protocol_version(), protocol_version);
assert_eq!(ctx.fee_multiplier_permille(), 1_000);
assert_eq!(ctx.platform_protocol_version(), LATEST_VERSION);
assert_eq!(
ctx.fee_multiplier_permille(),
crate::model::fee_estimation::PlatformFeeEstimator::DEFAULT_FEE_MULTIPLIER_PERMILLE
);
}

#[tokio::test]
async fn protocol_refresh_observes_activation_after_a_pre_activation_epoch() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let ctx = crate::mcp::tests::legacy_wallet_context(temp_dir.path());
ctx.set_platform_protocol_version(11);
ctx.set_platform_protocol_version(LATEST_VERSION - 1);
ctx.sdk
.store(Arc::new(mock_sdk_with_current_epoch(12).await));
.store(Arc::new(mock_sdk_with_dpns_contract(&ctx).await));

refresh_platform_protocol_version(&ctx)
.await
.expect("refresh current epoch");
.expect("refresh platform metadata");

assert_eq!(ctx.platform_protocol_version(), 12);
assert_eq!(ctx.platform_protocol_version(), LATEST_VERSION);
}

#[tokio::test]
Expand All @@ -486,12 +476,12 @@ mod tests {
assert_eq!(ctx.platform_protocol_version(), 0);

ctx.sdk
.store(Arc::new(mock_sdk_with_current_epoch(12).await));
.store(Arc::new(mock_sdk_with_dpns_contract(&ctx).await));
wait_for_spv_and_refresh_platform_info(&ctx, ProtocolRefresh::BestEffortIfUnpopulated)
.await
.expect("SPV readiness retries Platform metadata");

assert_eq!(ctx.platform_protocol_version(), 12);
assert_eq!(ctx.platform_protocol_version(), LATEST_VERSION);
}

#[tokio::test]
Expand Down
Loading