Skip to content
Closed
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
303 changes: 288 additions & 15 deletions src/ui/wallets/send_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,17 @@
use crate::ui::{MessageType, RootScreenType, ScreenLike};
use dash_sdk::dashcore_rpc::dashcore::Address;
use dash_sdk::dashcore_rpc::dashcore::address::NetworkUnchecked;
use dash_sdk::dpp::address_funds::AddressFundsFeeStrategyStep;
use dash_sdk::dpp::address_funds::PlatformAddress;
use dash_sdk::dpp::balances::credits::Credits;
use dash_sdk::dpp::identity::core_script::CoreScript;
use dash_sdk::dpp::prelude::AddressNonce;
use dash_sdk::dpp::state_transition::StateTransitionEstimatedFeeValidation;
use dash_sdk::dpp::state_transition::address_credit_withdrawal_transition::AddressCreditWithdrawalTransition;
use dash_sdk::dpp::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0;
use dash_sdk::dpp::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition;
use dash_sdk::dpp::state_transition::address_funds_transfer_transition::v0::AddressFundsTransferTransitionV0;
use dash_sdk::dpp::withdrawal::Pooling;
use eframe::egui::{self, Context, RichText, Ui};
use egui::{Color32, Frame, Margin};
use std::collections::BTreeMap;
Expand All @@ -38,6 +46,8 @@
/// Calculate the estimated fee for a platform address funds transfer.
///
/// Uses PlatformFeeEstimator for base costs (input/output fees) plus storage fees.
///
#[deprecated(note = "Use state-transition-based fee estimation instead")]
fn estimate_platform_fee(estimator: &PlatformFeeEstimator, input_count: usize) -> u64 {
let inputs = input_count.max(1);

Expand All @@ -56,6 +66,60 @@
total.saturating_add(total / 5)
}

/// Calculate the estimated fee for a Platform address withdrawal using a constructed state transition.
fn estimate_fee_platform_to_core(
platform_version: &dash_sdk::dpp::version::PlatformVersion,
inputs: &BTreeMap<PlatformAddress, u64>,
output_script: &CoreScript,
) -> Result<u64, String> {
let inputs_with_nonce: BTreeMap<PlatformAddress, (AddressNonce, Credits)> = inputs
.iter()
.map(|(addr, amount)| (*addr, (0, *amount)))
.collect();

let transition = AddressCreditWithdrawalTransition::V0(AddressCreditWithdrawalTransitionV0 {
inputs: inputs_with_nonce,
output: None,
fee_strategy: vec![AddressFundsFeeStrategyStep::DeductFromInput(0)],
core_fee_per_byte: 1,
pooling: Pooling::Never,
output_script: output_script.clone(),
user_fee_increase: 0,
input_witnesses: Vec::new(),
});

transition
.calculate_min_required_fee(platform_version)
.map_err(|e| format!("Platform->Core fee estimation failed: {}", e))
}

/// Calculate the estimated fee for a Platform address transfer using a constructed state transition.
fn estimate_fee_platform_to_platform(
platform_version: &dash_sdk::dpp::version::PlatformVersion,
inputs: &BTreeMap<PlatformAddress, u64>,
destination: &PlatformAddress,
) -> Result<u64, String> {
let inputs_with_nonce: BTreeMap<PlatformAddress, (AddressNonce, Credits)> = inputs
.iter()
.map(|(addr, amount)| (*addr, (0, *amount)))
.collect();

let mut outputs = BTreeMap::new();
outputs.insert(*destination, 1);

let transition = AddressFundsTransferTransition::V0(AddressFundsTransferTransitionV0 {
inputs: inputs_with_nonce,
outputs,
fee_strategy: vec![AddressFundsFeeStrategyStep::DeductFromInput(0)],
user_fee_increase: 0,
input_witnesses: Vec::new(),
});

transition
.calculate_min_required_fee(platform_version)
.map_err(|e| format!("Platform->Platform fee estimation failed: {}", e))
}

/// Result of allocating platform addresses for a transfer.
#[derive(Debug, Clone)]
struct AddressAllocationResult {
Expand All @@ -71,6 +135,109 @@
sorted_addresses: Vec<(PlatformAddress, Address, u64)>,
}

/// Allocates platform addresses for a transfer, using a custom fee calculator.
fn allocate_platform_addresses_with_fee<F>(
addresses: &[(PlatformAddress, Address, u64)],
amount_credits: u64,
destination: Option<&PlatformAddress>,
fee_for_inputs: F,
) -> Result<AddressAllocationResult, String>
where
F: Fn(&BTreeMap<PlatformAddress, u64>) -> Result<u64, String>,
{
// Filter out the destination address if provided (protocol doesn't allow same address as input and output)
let filtered: Vec<_> = addresses
.iter()
.filter(|(platform_addr, _, _)| destination != Some(platform_addr))
.cloned()
.collect();

// Sort addresses by balance descending so the largest balance is used first
let mut sorted_addresses = filtered;
sorted_addresses.sort_by(|a, b| b.2.cmp(&a.2));

// Early return if no addresses available after filtering
if sorted_addresses.is_empty() {
return Ok(AddressAllocationResult {
inputs: BTreeMap::new(),
fee_payer_index: 0,
estimated_fee: fee_for_inputs(&BTreeMap::new())?,
shortfall: amount_credits,
sorted_addresses: vec![],
});
}

// The highest-balance address (first in sorted order) will pay the fee
let fee_payer_addr = sorted_addresses.first().map(|(addr, _, _)| *addr);

let mut estimated_fee = fee_for_inputs(&BTreeMap::new())?;
let mut inputs: BTreeMap<PlatformAddress, u64> = BTreeMap::new();

// Iterate until fee estimate stabilizes (input count affects fee)
for _ in 0..=MAX_PLATFORM_INPUTS {
inputs.clear();
let mut remaining = amount_credits;

for (idx, (platform_addr, _, balance)) in sorted_addresses.iter().enumerate() {
if remaining == 0 || inputs.len() >= MAX_PLATFORM_INPUTS {
break;
}
let is_fee_payer = idx == 0;
let available = if is_fee_payer {
balance.saturating_sub(estimated_fee)
} else {
*balance
};
let use_amount = remaining.min(available);
if use_amount > 0 || is_fee_payer {
inputs.insert(*platform_addr, use_amount);
remaining = remaining.saturating_sub(use_amount);
}
}

let new_fee = fee_for_inputs(&inputs)?;
if new_fee == estimated_fee {
break;
}
estimated_fee = new_fee;
}

// Calculate shortfall (amount we couldn't allocate)
let total_allocated: u64 = inputs.values().sum();
let allocation_shortfall = amount_credits.saturating_sub(total_allocated);

// Check if fee payer can actually afford the fee from their remaining balance.
let fee_deficit = if let Some(fee_payer) = fee_payer_addr {
let fee_payer_balance = sorted_addresses.first().map(|(_, _, b)| *b).unwrap_or(0);
let fee_payer_contribution = inputs.get(&fee_payer).copied().unwrap_or(0);
let fee_payer_remaining = fee_payer_balance.saturating_sub(fee_payer_contribution);
estimated_fee.saturating_sub(fee_payer_remaining)
} else {
estimated_fee
};

let shortfall = allocation_shortfall.saturating_add(fee_deficit);

// Find the index of the fee payer in BTreeMap order (required by backend)
let fee_payer_index = fee_payer_addr
.and_then(|payer| {
inputs
.keys()
.enumerate()
.find(|(_, addr)| **addr == payer)
.map(|(idx, _)| idx as u16)
})
.unwrap_or(0);

Ok(AddressAllocationResult {
inputs,
fee_payer_index,
estimated_fee,
shortfall,
sorted_addresses,
})
}

/// Allocates platform addresses for a transfer, selecting which addresses to use
/// and how much from each.
///
Expand Down Expand Up @@ -104,7 +271,7 @@
return AddressAllocationResult {
inputs: BTreeMap::new(),
fee_payer_index: 0,
estimated_fee: estimate_platform_fee(estimator, 1),

Check failure on line 274 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead

error: use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead --> src/ui/wallets/send_screen.rs:274:28 | 274 | estimated_fee: estimate_platform_fee(estimator, 1), | ^^^^^^^^^^^^^^^^^^^^^ | = note: `-D deprecated` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(deprecated)]`

Check failure on line 274 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead

error: use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead --> src/ui/wallets/send_screen.rs:274:28 | 274 | estimated_fee: estimate_platform_fee(estimator, 1), | ^^^^^^^^^^^^^^^^^^^^^ | = note: `-D deprecated` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(deprecated)]`
shortfall: amount_credits,
sorted_addresses: vec![],
};
Expand All @@ -116,7 +283,7 @@
// Calculate fee based on expected number of inputs (use worst-case for safety)
// This matches what the Max button calculation uses
let max_inputs = sorted_addresses.len().min(MAX_PLATFORM_INPUTS);
let estimated_fee = estimate_platform_fee(estimator, max_inputs.max(1));

Check failure on line 286 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead

error: use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead --> src/ui/wallets/send_screen.rs:286:25 | 286 | let estimated_fee = estimate_platform_fee(estimator, max_inputs.max(1)); | ^^^^^^^^^^^^^^^^^^^^^

Check failure on line 286 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead

error: use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead --> src/ui/wallets/send_screen.rs:286:25 | 286 | let estimated_fee = estimate_platform_fee(estimator, max_inputs.max(1)); | ^^^^^^^^^^^^^^^^^^^^^

// Allocate inputs = outputs (protocol requires equality).
// Fee payer must reserve enough remaining balance to pay the fee separately.
Expand Down Expand Up @@ -340,6 +507,64 @@
}
}

fn estimate_max_fee_for_platform_send(
&self,
fee_estimator: &PlatformFeeEstimator,
addresses: &[(PlatformAddress, Address, u64)],
destination: Option<&PlatformAddress>,
) -> Result<u64, String> {
let mut sorted_addresses: Vec<_> = addresses
.iter()
.filter(|(addr, _, _)| destination != Some(addr))
.cloned()
.collect();
sorted_addresses.sort_by(|a, b| b.2.cmp(&a.2));

let usable_count = sorted_addresses.len().min(MAX_PLATFORM_INPUTS);
if usable_count == 0 {
return Ok(estimate_platform_fee(fee_estimator, 1));

Check failure on line 525 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead

error: use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead --> src/ui/wallets/send_screen.rs:525:23 | 525 | return Ok(estimate_platform_fee(fee_estimator, 1)); | ^^^^^^^^^^^^^^^^^^^^^

Check failure on line 525 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead

error: use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead --> src/ui/wallets/send_screen.rs:525:23 | 525 | return Ok(estimate_platform_fee(fee_estimator, 1)); | ^^^^^^^^^^^^^^^^^^^^^
}

let dest_type = self.detect_address_type(&self.destination_address);
if dest_type == AddressType::Core {
let output_script = self
.destination_address
.trim()
.parse::<Address<NetworkUnchecked>>()
.ok()
.and_then(|addr| addr.require_network(self.app_context.network).ok())
.map(|addr| CoreScript::new(addr.script_pubkey()));
if let Some(output_script) = output_script {
let max_fee_inputs: BTreeMap<PlatformAddress, u64> = sorted_addresses
.iter()
.take(usable_count)
.map(|(addr, _, _)| (*addr, 0))
.collect();
return estimate_fee_platform_to_core(
self.app_context.platform_version(),
&max_fee_inputs,
&output_script,
);
}
} else if dest_type == AddressType::Platform {
if let Some(destination) = destination {
let max_fee_inputs: BTreeMap<PlatformAddress, u64> = sorted_addresses
.iter()
.take(usable_count)
.map(|(addr, _, _)| (*addr, 0))
.collect();
return estimate_fee_platform_to_platform(
self.app_context.platform_version(),
&max_fee_inputs,
destination,
);
}
}

Check failure on line 562 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

this `if` statement can be collapsed

error: this `if` statement can be collapsed --> src/ui/wallets/send_screen.rs:549:16 | 549 | } else if dest_type == AddressType::Platform { | ________________^ 550 | | if let Some(destination) = destination { 551 | | let max_fee_inputs: BTreeMap<PlatformAddress, u64> = sorted_addresses 552 | | .iter() ... | 562 | | } | |_________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.92.0/index.html#collapsible_if = note: `-D clippy::collapsible-if` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::collapsible_if)]` help: collapse nested if block | 549 ~ } else if dest_type == AddressType::Platform 550 ~ && let Some(destination) = destination { 551 | let max_fee_inputs: BTreeMap<PlatformAddress, u64> = sorted_addresses ... 560 | ); 561 ~ } |

Check failure on line 562 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

this `if` statement can be collapsed

error: this `if` statement can be collapsed --> src/ui/wallets/send_screen.rs:549:16 | 549 | } else if dest_type == AddressType::Platform { | ________________^ 550 | | if let Some(destination) = destination { 551 | | let max_fee_inputs: BTreeMap<PlatformAddress, u64> = sorted_addresses 552 | | .iter() ... | 562 | | } | |_________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.92.0/index.html#collapsible_if = note: `-D clippy::collapsible-if` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::collapsible_if)]` help: collapse nested if block | 549 ~ } else if dest_type == AddressType::Platform 550 ~ && let Some(destination) = destination { 551 | let max_fee_inputs: BTreeMap<PlatformAddress, u64> = sorted_addresses ... 560 | ); 561 ~ } |

// Fallback to legacy fee estimation
Ok(estimate_platform_fee(fee_estimator, usable_count))

Check failure on line 565 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead

error: use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead --> src/ui/wallets/send_screen.rs:565:12 | 565 | Ok(estimate_platform_fee(fee_estimator, usable_count)) | ^^^^^^^^^^^^^^^^^^^^^

Check failure on line 565 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead

error: use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead --> src/ui/wallets/send_screen.rs:565:12 | 565 | Ok(estimate_platform_fee(fee_estimator, usable_count)) | ^^^^^^^^^^^^^^^^^^^^^
}

fn reset_form(&mut self) {
self.destination_address.clear();
self.amount = None;
Expand Down Expand Up @@ -684,13 +909,16 @@
.map(|(addr, _)| addr)
.map_err(|e| format!("Invalid platform address: {}", e))?;

// Allocate addresses using the helper function
let allocation = allocate_platform_addresses(
&fee_estimator,
let platform_version = self.app_context.platform_version();

// Allocate addresses using state-transition-based fee estimation
let allocation = allocate_platform_addresses_with_fee(
&addresses,
amount_credits,
Some(&destination),
);
|inputs| estimate_fee_platform_to_platform(platform_version, inputs, &destination),
)
.map_err(|e| format!("Fee estimation failed: {}", e))?;

if allocation.sorted_addresses.is_empty() {
return Err(
Expand Down Expand Up @@ -718,7 +946,13 @@
.take(MAX_PLATFORM_INPUTS)
.map(|(_, _, b)| *b)
.sum();
let max_fee = estimate_platform_fee(&fee_estimator, addresses_available);
let max_fee = self
.estimate_max_fee_for_platform_send(
&fee_estimator,
&allocation.sorted_addresses,
Some(&destination),
)
.map_err(|e| format!("Fee estimation failed: {}", e))?;
let max_sendable = max_balance.saturating_sub(max_fee);

return Err(format!(
Expand Down Expand Up @@ -786,9 +1020,6 @@
return Err("Amount must be greater than 0".to_string());
}

// Get fee estimator with current network multiplier
let fee_estimator = self.app_context.fee_estimator();

// Calculate total balance across all platform addresses
let total_balance: u64 = addresses.iter().map(|(_, _, balance)| *balance).sum();

Expand Down Expand Up @@ -818,9 +1049,14 @@

let output_script = CoreScript::new(dest_address.script_pubkey());

// Allocate addresses using the helper function (no destination filter for withdrawals)
let platform_version = self.app_context.platform_version();

// Allocate addresses using state-transition-based fee estimation (no destination filter)
let allocation =
allocate_platform_addresses(&fee_estimator, &addresses, amount_credits, None);
allocate_platform_addresses_with_fee(&addresses, amount_credits, None, |inputs| {
estimate_fee_platform_to_core(platform_version, inputs, &output_script)
})
.map_err(|e| format!("Fee estimation failed: {}", e))?;

if allocation.shortfall > 0 {
// Calculate the max we can send with MAX_PLATFORM_INPUTS addresses (minus fees)
Expand All @@ -831,7 +1067,15 @@
.take(MAX_PLATFORM_INPUTS)
.map(|(_, _, b)| *b)
.sum();
let max_fee = estimate_platform_fee(&fee_estimator, addresses_available);
let max_fee_inputs: BTreeMap<PlatformAddress, u64> = allocation
.sorted_addresses
.iter()
.take(addresses_available)
.map(|(addr, _, _)| (*addr, 0))
.collect();
let max_fee =
estimate_fee_platform_to_core(platform_version, &max_fee_inputs, &output_script)
.map_err(|e| format!("Fee estimation failed: {}", e))?;
let max_sendable = max_balance.saturating_sub(max_fee);

return Err(format!(
Expand Down Expand Up @@ -1186,18 +1430,47 @@
.take(MAX_PLATFORM_INPUTS)
.map(|(_, _, balance)| *balance)
.sum();
let max_fee = estimate_platform_fee(&fee_estimator, usable_count);
let (max_fee, fee_error) = match self.estimate_max_fee_for_platform_send(
&fee_estimator,
&sorted_addresses,
destination.as_ref(),
) {
Ok(fee) => (fee, None),
Err(e) => {
tracing::warn!("Fee estimation failed for Max calculation: {}", e);
(estimate_platform_fee(&fee_estimator, usable_count), Some(e))

Check failure on line 1441 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead

error: use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead --> src/ui/wallets/send_screen.rs:1441:26 | 1441 | (estimate_platform_fee(&fee_estimator, usable_count), Some(e)) | ^^^^^^^^^^^^^^^^^^^^^

Check failure on line 1441 in src/ui/wallets/send_screen.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead

error: use of deprecated function `ui::wallets::send_screen::estimate_platform_fee`: Use state-transition-based fee estimation instead --> src/ui/wallets/send_screen.rs:1441:26 | 1441 | (estimate_platform_fee(&fee_estimator, usable_count), Some(e)) | ^^^^^^^^^^^^^^^^^^^^^
}
};

// Build hint explaining the limit
let hint = if sorted_addresses.len() > MAX_PLATFORM_INPUTS {
format!(
"Limited to {} input addresses per transaction, ~{} reserved for fees",
"Limited to {} input addresses per transaction, ~{} reserved for fees{}",
MAX_PLATFORM_INPUTS,
Self::format_credits(max_fee)
Self::format_credits(max_fee),
if fee_error.is_some() {
" (fallback estimate)"
} else {
""
}
)
} else {
format!("~{} reserved for fees", Self::format_credits(max_fee))
format!(
"~{} reserved for fees{}",
Self::format_credits(max_fee),
if fee_error.is_some() {
" (fallback estimate)"
} else {
""
}
)
};
tracing::debug!(
"Max amount calculation: total balance {} minus estimated fee {} = max {}",
Self::format_credits(total),
Self::format_credits(max_fee),
Self::format_credits(total.saturating_sub(max_fee))
);
(Some(total.saturating_sub(max_fee)), Some(hint))
}
None => (None, None),
Expand Down
Loading