From 0331a9928370352e5f684ab6373fcffc6c7becd2 Mon Sep 17 00:00:00 2001 From: Matt Hess Date: Sat, 28 Feb 2026 12:23:11 +0000 Subject: [PATCH] Fix stake double-counting and improve daemon rejection errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stake transactions were adding the stake amount as both a destination and amount_burnt, causing the builder to require 2x the intended balance. Remove the destination — amount_burnt alone handles it. Daemon rejection errors now report boolean flags (double_spend, fee_too_low, invalid_input, etc.) instead of the often-empty reason string. --- crates/salvium-ffi/src/transfer.rs | 60 +++++++++++++++++++----------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/crates/salvium-ffi/src/transfer.rs b/crates/salvium-ffi/src/transfer.rs index cdc9a11..f165fb1 100644 --- a/crates/salvium-ffi/src/transfer.rs +++ b/crates/salvium-ffi/src/transfer.rs @@ -419,25 +419,11 @@ async fn do_stake( return Err("stake amount must be > 0".into()); } - // Stake destination is the wallet's own address. - let keys = wallet.keys(); - let (spend_pub, view_pub) = if is_carrot { - (keys.carrot.account_spend_pubkey, keys.carrot.account_view_pubkey) - } else { - (keys.cn.spend_public_key, keys.cn.view_public_key) - }; - - let destinations = vec![Destination { - spend_pubkey: spend_pub, - view_pubkey: view_pub, - amount, - asset_type: params.asset_type.clone(), - payment_id: [0u8; 8], - is_subaddress: false, - }]; - + // STAKE: the staked amount is handled entirely via amount_burnt. + // No explicit destination — only a change output is created by the builder. + // The protocol returns the staked amount + yield later. let out_type = if is_carrot { output_type::CARROT_V1 } else { output_type::TAGGED_KEY }; - let est_fee = fee::estimate_tx_fee(2, 2, params.ring_size, is_carrot, out_type, priority); + let est_fee = fee::estimate_tx_fee(2, 1, params.ring_size, is_carrot, out_type, priority); let selection = wallet .select_outputs( @@ -451,7 +437,7 @@ async fn do_stake( let built = build_sign_maybe_broadcast( wallet, daemon, - &destinations, + &[], // no destinations — stake amount is in amount_burnt &selection, ¶ms.asset_type, tx_type::STAKE, @@ -776,9 +762,41 @@ async fn build_sign_maybe_broadcast( .map_err(|e| format!("broadcast failed: {e}"))?; if send_result.status != "OK" { + let mut flags = Vec::new(); + if send_result.double_spend { + flags.push("double_spend"); + } + if send_result.fee_too_low { + flags.push("fee_too_low"); + } + if send_result.invalid_input { + flags.push("invalid_input"); + } + if send_result.invalid_output { + flags.push("invalid_output"); + } + if send_result.overspend { + flags.push("overspend"); + } + if send_result.too_big { + flags.push("too_big"); + } + if send_result.sanity_check_failed { + flags.push("sanity_check_failed"); + } + if send_result.tx_extra_too_big { + flags.push("tx_extra_too_big"); + } + let detail = if !flags.is_empty() { + flags.join(", ") + } else if !send_result.reason.is_empty() { + send_result.reason.clone() + } else { + "unknown".to_string() + }; return Err(format!( - "daemon rejected transaction: {} (reason: {})", - send_result.status, send_result.reason + "daemon rejected transaction: {} ({})", + send_result.status, detail )); }