diff --git a/CHANGELOG.md b/CHANGELOG.md index acd96ca94..1e90d48a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`)**: diff --git a/src/backend_task/platform_info.rs b/src/backend_task/platform_info.rs index 5e2ae7b96..71fb775a3 100644 --- a/src/backend_task/platform_info.rs +++ b/src/backend_task/platform_info.rs @@ -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; @@ -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, @@ -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) -> 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, + 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." + ), } } @@ -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) @@ -939,20 +970,36 @@ 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."), @@ -960,6 +1007,35 @@ mod tests { ); } + /// 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. diff --git a/src/mcp/resolve.rs b/src/mcp/resolve.rs index b3f8e2668..4c5a99d1e 100644 --- a/src/mcp/resolve.rs +++ b/src/mcp/resolve.rs @@ -408,33 +408,22 @@ 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) -> 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 } @@ -442,10 +431,8 @@ mod tests { 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"); @@ -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] @@ -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]