diff --git a/crates/salvium-cli/src/commands/multisig.rs b/crates/salvium-cli/src/commands/multisig.rs index 75f6e4e..47bf524 100644 --- a/crates/salvium-cli/src/commands/multisig.rs +++ b/crates/salvium-cli/src/commands/multisig.rs @@ -264,9 +264,10 @@ pub async fn transfer_multisig(ctx: &AppContext, address: &str, amount_str: &str println!(" Amount: {} SAL", format_sal_u64(amount)); println!(); - let fee_priority = - tx_common::adjust_priority(salvium_tx::fee::FeePriority::Default, &ctx.pool).await; - let est_fee = salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_priority); + let fee_ctx = + tx_common::resolve_fee_context(&ctx.pool, salvium_tx::fee::FeePriority::Default).await; + let est_fee = + salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_ctx.fee_per_byte, fee_ctx.priority); println!(" Estimated fee: {} SAL", format_sal_u64(est_fee)); let balance = wallet.get_balance("SAL", 0)?; @@ -286,7 +287,7 @@ pub async fn transfer_multisig(ctx: &AppContext, address: &str, amount_str: &str } // 1. Select UTXOs and derive per-output secret keys. - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (input_data, actual_fee) = pipeline.select_and_prepare_inputs( amount, est_fee, diff --git a/crates/salvium-cli/src/commands/transfers.rs b/crates/salvium-cli/src/commands/transfers.rs index 4b4b38f..3d8efda 100644 --- a/crates/salvium-cli/src/commands/transfers.rs +++ b/crates/salvium-cli/src/commands/transfers.rs @@ -16,7 +16,7 @@ pub async fn transfer(ctx: &AppContext, address: &str, amount_str: &str, priorit let parsed_addr = salvium_types::address::parse_address(address) .map_err(|e| format!("invalid destination address: {}", e))?; let fee_priority = tx_common::parse_fee_priority(priority); - let fee_priority = tx_common::adjust_priority(fee_priority, &ctx.pool).await; + let fee_ctx = tx_common::resolve_fee_context(&ctx.pool, fee_priority).await; println!("Transfer:"); println!(" To: {}", address); @@ -24,7 +24,8 @@ pub async fn transfer(ctx: &AppContext, address: &str, amount_str: &str, priorit println!(" Format: {:?}", parsed_addr.format); println!(); - let est_fee = salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_priority); + let est_fee = + salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_ctx.fee_per_byte, fee_ctx.priority); println!(" Estimated fee: {} SAL", format_sal_u64(est_fee)); println!(); @@ -44,7 +45,7 @@ pub async fn transfer(ctx: &AppContext, address: &str, amount_str: &str, priorit return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( amount, est_fee, @@ -92,9 +93,10 @@ pub async fn stake(ctx: &AppContext, amount_str: &str) -> Result { println!(" Amount: {} SAL", format_sal_u64(amount)); println!(); - let fee_priority = - tx_common::adjust_priority(salvium_tx::fee::FeePriority::Default, &ctx.pool).await; - let est_fee = salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_priority); + let fee_ctx = + tx_common::resolve_fee_context(&ctx.pool, salvium_tx::fee::FeePriority::Default).await; + let est_fee = + salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_ctx.fee_per_byte, fee_ctx.priority); println!(" Estimated fee: {} SAL", format_sal_u64(est_fee)); println!(); @@ -114,7 +116,7 @@ pub async fn stake(ctx: &AppContext, amount_str: &str) -> Result { return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( amount, est_fee, @@ -157,13 +159,14 @@ pub async fn burn(ctx: &AppContext, amount_str: &str, priority: &str) -> Result let amount = parse_sal_amount(amount_str)?; let fee_priority = tx_common::parse_fee_priority(priority); - let fee_priority = tx_common::adjust_priority(fee_priority, &ctx.pool).await; + let fee_ctx = tx_common::resolve_fee_context(&ctx.pool, fee_priority).await; println!("Burn:"); println!(" Amount: {} SAL", format_sal_u64(amount)); println!(); - let est_fee = salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_priority); + let est_fee = + salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_ctx.fee_per_byte, fee_ctx.priority); println!(" Estimated fee: {} SAL", format_sal_u64(est_fee)); let balance = wallet.get_balance("SAL", 0)?; @@ -182,7 +185,7 @@ pub async fn burn(ctx: &AppContext, amount_str: &str, priority: &str) -> Result return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( amount, est_fee, @@ -224,7 +227,7 @@ pub async fn convert( let amount = parse_sal_amount(amount_str)?; let fee_priority = tx_common::parse_fee_priority(priority); - let fee_priority = tx_common::adjust_priority(fee_priority, &ctx.pool).await; + let fee_ctx = tx_common::resolve_fee_context(&ctx.pool, fee_priority).await; println!("Convert:"); println!(" Amount: {} {}", format_sal_u64(amount), source_asset); @@ -232,7 +235,8 @@ pub async fn convert( println!(" To: {}", dest_asset); println!(); - let est_fee = salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_priority); + let est_fee = + salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_ctx.fee_per_byte, fee_ctx.priority); println!(" Estimated fee: {} {}", format_sal_u64(est_fee), source_asset); let balance = wallet.get_balance(source_asset, 0)?; @@ -252,7 +256,7 @@ pub async fn convert( return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( amount, est_fee, @@ -294,12 +298,13 @@ pub async fn audit(ctx: &AppContext, priority: &str) -> Result { } let fee_priority = tx_common::parse_fee_priority(priority); - let fee_priority = tx_common::adjust_priority(fee_priority, &ctx.pool).await; + let fee_ctx = tx_common::resolve_fee_context(&ctx.pool, fee_priority).await; // Audit sweeps all funds back to self as a verifiable on-chain proof. let balance = wallet.get_balance("SAL", 0)?; let unlocked: u64 = balance.unlocked_balance.parse().unwrap_or(0); - let est_fee = salvium_tx::estimate_tx_fee(4, 2, 16, true, 0x04, fee_priority); + let est_fee = + salvium_tx::estimate_tx_fee(4, 2, 16, true, 0x04, fee_ctx.fee_per_byte, fee_ctx.priority); if unlocked <= est_fee { return Err("insufficient balance for audit transaction".into()); @@ -316,7 +321,7 @@ pub async fn audit(ctx: &AppContext, priority: &str) -> Result { return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( amount, est_fee, @@ -365,7 +370,7 @@ pub async fn locked_transfer( let parsed_addr = salvium_types::address::parse_address(address) .map_err(|e| format!("invalid destination address: {}", e))?; let fee_priority = tx_common::parse_fee_priority(priority); - let fee_priority = tx_common::adjust_priority(fee_priority, &ctx.pool).await; + let fee_ctx = tx_common::resolve_fee_context(&ctx.pool, fee_priority).await; println!("Locked Transfer:"); println!(" To: {}", address); @@ -373,7 +378,8 @@ pub async fn locked_transfer( println!(" Unlock time: {}", unlock_time); println!(); - let est_fee = salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_priority); + let est_fee = + salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_ctx.fee_per_byte, fee_ctx.priority); println!(" Estimated fee: {} SAL", format_sal_u64(est_fee)); if !tx_common::confirm("Confirm locked transfer? [y/N] ")? { @@ -381,7 +387,7 @@ pub async fn locked_transfer( return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( amount, est_fee, @@ -427,11 +433,12 @@ pub async fn sweep_all(ctx: &AppContext, address: &str, priority: &str) -> Resul let parsed_addr = salvium_types::address::parse_address(address) .map_err(|e| format!("invalid destination address: {}", e))?; let fee_priority = tx_common::parse_fee_priority(priority); - let fee_priority = tx_common::adjust_priority(fee_priority, &ctx.pool).await; + let fee_ctx = tx_common::resolve_fee_context(&ctx.pool, fee_priority).await; let balance = wallet.get_balance("SAL", 0)?; let unlocked: u64 = balance.unlocked_balance.parse().unwrap_or(0); - let est_fee = salvium_tx::estimate_tx_fee(4, 1, 16, true, 0x04, fee_priority); + let est_fee = + salvium_tx::estimate_tx_fee(4, 1, 16, true, 0x04, fee_ctx.fee_per_byte, fee_ctx.priority); if unlocked <= est_fee { return Err("insufficient balance for sweep".into()); @@ -448,7 +455,7 @@ pub async fn sweep_all(ctx: &AppContext, address: &str, priority: &str) -> Resul return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( amount, est_fee, @@ -499,7 +506,7 @@ pub async fn sweep_below( let parsed_addr = salvium_types::address::parse_address(address) .map_err(|e| format!("invalid destination address: {}", e))?; let fee_priority = tx_common::parse_fee_priority(priority); - let fee_priority = tx_common::adjust_priority(fee_priority, &ctx.pool).await; + let fee_ctx = tx_common::resolve_fee_context(&ctx.pool, fee_priority).await; // Get outputs below threshold. let query = salvium_crypto::storage::OutputQuery { @@ -522,7 +529,15 @@ pub async fn sweep_below( } let total: u64 = below.iter().map(|o| o.amount.parse::().unwrap_or(0)).sum(); - let est_fee = salvium_tx::estimate_tx_fee(below.len(), 1, 16, true, 0x04, fee_priority); + let est_fee = salvium_tx::estimate_tx_fee( + below.len(), + 1, + 16, + true, + 0x04, + fee_ctx.fee_per_byte, + fee_ctx.priority, + ); if total <= est_fee { return Err("total of outputs below threshold doesn't cover the fee".into()); @@ -540,7 +555,7 @@ pub async fn sweep_below( return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( total - est_fee, est_fee, @@ -588,14 +603,15 @@ pub async fn sweep_single( let parsed_addr = salvium_types::address::parse_address(address) .map_err(|e| format!("invalid destination address: {}", e))?; let fee_priority = tx_common::parse_fee_priority(priority); - let fee_priority = tx_common::adjust_priority(fee_priority, &ctx.pool).await; + let fee_ctx = tx_common::resolve_fee_context(&ctx.pool, fee_priority).await; let output = wallet .get_output(key_image)? .ok_or_else(|| format!("output not found for key image: {}", key_image))?; let amount: u64 = output.amount.parse().unwrap_or(0); - let est_fee = salvium_tx::estimate_tx_fee(1, 1, 16, true, 0x04, fee_priority); + let est_fee = + salvium_tx::estimate_tx_fee(1, 1, 16, true, 0x04, fee_ctx.fee_per_byte, fee_ctx.priority); if amount <= est_fee { return Err("output amount doesn't cover the fee".into()); @@ -611,7 +627,7 @@ pub async fn sweep_single( return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( amount - est_fee, est_fee, @@ -678,10 +694,18 @@ pub async fn sweep_unmixable(ctx: &AppContext) -> Result { return Ok(()); } - let fee_priority = - tx_common::adjust_priority(salvium_tx::fee::FeePriority::Default, &ctx.pool).await; - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); - let est_fee = salvium_tx::estimate_tx_fee(unmixable.len(), 1, 16, true, 0x04, fee_priority); + let fee_ctx = + tx_common::resolve_fee_context(&ctx.pool, salvium_tx::fee::FeePriority::Default).await; + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); + let est_fee = salvium_tx::estimate_tx_fee( + unmixable.len(), + 1, + 16, + true, + 0x04, + fee_ctx.fee_per_byte, + fee_ctx.priority, + ); if total <= est_fee { return Err("total of unmixable outputs doesn't cover the fee".into()); @@ -734,11 +758,12 @@ pub async fn locked_sweep_all( let parsed_addr = salvium_types::address::parse_address(address) .map_err(|e| format!("invalid destination address: {}", e))?; let fee_priority = tx_common::parse_fee_priority(priority); - let fee_priority = tx_common::adjust_priority(fee_priority, &ctx.pool).await; + let fee_ctx = tx_common::resolve_fee_context(&ctx.pool, fee_priority).await; let balance = wallet.get_balance("SAL", 0)?; let unlocked: u64 = balance.unlocked_balance.parse().unwrap_or(0); - let est_fee = salvium_tx::estimate_tx_fee(4, 1, 16, true, 0x04, fee_priority); + let est_fee = + salvium_tx::estimate_tx_fee(4, 1, 16, true, 0x04, fee_ctx.fee_per_byte, fee_ctx.priority); if unlocked <= est_fee { return Err("insufficient balance".into()); @@ -755,7 +780,7 @@ pub async fn locked_sweep_all( return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( amount, est_fee, @@ -795,7 +820,7 @@ pub async fn return_payment(ctx: &AppContext, tx_hash: &str, priority: &str) -> } let fee_priority = tx_common::parse_fee_priority(priority); - let fee_priority = tx_common::adjust_priority(fee_priority, &ctx.pool).await; + let fee_ctx = tx_common::resolve_fee_context(&ctx.pool, fee_priority).await; // Look up the incoming TX to find the sender's info. let query = salvium_crypto::storage::TxQuery { @@ -832,7 +857,8 @@ pub async fn return_payment(ctx: &AppContext, tx_hash: &str, priority: &str) -> let parsed_addr = salvium_types::address::parse_address(&return_addr) .map_err(|e| format!("invalid return address: {}", e))?; - let est_fee = salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_priority); + let est_fee = + salvium_tx::estimate_tx_fee(2, 2, 16, true, 0x04, fee_ctx.fee_per_byte, fee_ctx.priority); println!("Return payment:"); println!(" Original TX: {}", tx_hash); @@ -844,7 +870,7 @@ pub async fn return_payment(ctx: &AppContext, tx_hash: &str, priority: &str) -> return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( amount, est_fee, @@ -900,7 +926,7 @@ pub async fn sweep_account( let parsed_addr = salvium_types::address::parse_address(address) .map_err(|e| format!("invalid destination address: {}", e))?; let fee_priority = tx_common::parse_fee_priority(priority); - let fee_priority = tx_common::adjust_priority(fee_priority, &ctx.pool).await; + let fee_ctx = tx_common::resolve_fee_context(&ctx.pool, fee_priority).await; // Get outputs for the specific account (and optional subaddress filter). let query = salvium_crypto::storage::OutputQuery { @@ -931,7 +957,15 @@ pub async fn sweep_account( } let total: u64 = filtered.iter().map(|o| o.amount.parse::().unwrap_or(0)).sum(); - let est_fee = salvium_tx::estimate_tx_fee(filtered.len(), 1, 16, true, 0x04, fee_priority); + let est_fee = salvium_tx::estimate_tx_fee( + filtered.len(), + 1, + 16, + true, + 0x04, + fee_ctx.fee_per_byte, + fee_ctx.priority, + ); if total <= est_fee { return Err("total of outputs in account doesn't cover the fee".into()); @@ -949,7 +983,7 @@ pub async fn sweep_account( return Ok(()); } - let pipeline = TxPipeline::new(&wallet, ctx, fee_priority); + let pipeline = TxPipeline::new(&wallet, ctx, &fee_ctx); let (inputs, actual_fee) = pipeline.select_and_prepare_inputs( sweep_amount, est_fee, diff --git a/crates/salvium-cli/src/tx_common.rs b/crates/salvium-cli/src/tx_common.rs index 71be26c..f47eef3 100644 --- a/crates/salvium-cli/src/tx_common.rs +++ b/crates/salvium-cli/src/tx_common.rs @@ -17,20 +17,32 @@ pub struct SignedResult { pub fee: u64, } +/// Resolved network fee context — adjusted priority + dynamic fee rate. +/// +/// Produced by [`resolve_fee_context()`] from a single RPC roundtrip (the same +/// block headers used for priority adjustment also provide `base_reward` for the +/// dynamic fee calculation). +pub struct FeeContext { + pub priority: salvium_tx::fee::FeePriority, + pub fee_per_byte: u64, +} + /// Shared pipeline for building, signing, and optionally submitting transactions. pub struct TxPipeline<'a> { pub wallet: &'a Wallet, pub pool: NodePool, pub fee_priority: salvium_tx::fee::FeePriority, + pub fee_per_byte: u64, } impl<'a> TxPipeline<'a> { - pub fn new( - wallet: &'a Wallet, - ctx: &AppContext, - fee_priority: salvium_tx::fee::FeePriority, - ) -> Self { - Self { wallet, pool: ctx.pool.clone(), fee_priority } + pub fn new(wallet: &'a Wallet, ctx: &AppContext, fee_ctx: &FeeContext) -> Self { + Self { + wallet, + pool: ctx.pool.clone(), + fee_priority: fee_ctx.priority, + fee_per_byte: fee_ctx.fee_per_byte, + } } /// Select UTXOs for the given amount + fee, derive spend keys, and return @@ -55,6 +67,7 @@ impl<'a> TxPipeline<'a> { salvium_tx::decoy::DEFAULT_RING_SIZE, true, 0x04, + self.fee_per_byte, self.fee_priority, ); @@ -252,73 +265,94 @@ pub fn parse_fee_priority(s: &str) -> salvium_tx::fee::FeePriority { } } -/// Adjust fee priority based on network conditions (matches wallet2::adjust_priority). +/// Adjust priority from network conditions AND compute the dynamic `fee_per_byte`. /// -/// When priority is `Default` (user didn't explicitly choose), queries the daemon: -/// 1. If mempool has pending transactions -> Normal (5x) -/// 2. If recent blocks are >80% full -> Normal (5x) -/// 3. Otherwise -> Low (1x) +/// Uses the same block-header data for both — **zero extra RPC calls** compared to +/// the old `adjust_priority()`: +/// - Priority: mempool check + block fullness from the last 10 headers. +/// - Fee rate: `BlockHeader.reward` + `BlockHeader.major_version` from the most +/// recent header, fed into [`salvium_tx::fee::dynamic_fee_per_byte()`]. /// -/// Explicit priorities (Low/Normal/High/Highest) pass through unchanged. -pub async fn adjust_priority( - priority: salvium_tx::fee::FeePriority, +/// Falls back to `Normal` priority and the static `FEE_PER_BYTE` on any RPC failure. +pub async fn resolve_fee_context( pool: &NodePool, -) -> salvium_tx::fee::FeePriority { - use salvium_tx::fee::FeePriority; - - if priority != FeePriority::Default { - return priority; - } - match try_adjust_priority(pool).await { - Ok(adjusted) => adjusted, + priority: salvium_tx::fee::FeePriority, +) -> FeeContext { + match try_resolve_fee_context(pool, priority).await { + Ok(ctx) => ctx, Err(e) => { - log::debug!("adjust_priority failed, using Normal: {e}"); - FeePriority::Normal + log::debug!("resolve_fee_context failed: {e}"); + // Safe fallback: Normal priority, base_reward=0 → FEE_PER_BYTE. + FeeContext { + priority: if priority == salvium_tx::fee::FeePriority::Default { + salvium_tx::fee::FeePriority::Normal + } else { + priority + }, + fee_per_byte: salvium_tx::fee::dynamic_fee_per_byte(0, 0), + } } } } -async fn try_adjust_priority( +async fn try_resolve_fee_context( pool: &NodePool, -) -> std::result::Result { + priority: salvium_tx::fee::FeePriority, +) -> std::result::Result { use salvium_tx::fee::FeePriority; let info = pool.get_info().await.map_err(|e| e.to_string())?; - - // 1. Mempool backlog -> Normal - if info.tx_pool_size > 0 { - log::info!("adjust_priority: mempool has {} txs, using Normal", info.tx_pool_size); - return Ok(FeePriority::Normal); - } - - // 2. Block fullness check - let block_weight_limit = info - .extra - .get("block_weight_limit") - .and_then(|v| v.as_u64()) - .ok_or("block_weight_limit not in get_info")?; - let full_reward_zone = block_weight_limit / 2; - if full_reward_zone == 0 { - return Ok(FeePriority::Normal); - } - let height = info.height; - if height < 10 { - return Ok(FeePriority::Normal); - } - let headers = - pool.get_block_headers_range(height - 10, height - 1).await.map_err(|e| e.to_string())?; - let weight_sum: u64 = headers.iter().map(|h| h.block_weight).sum(); - let fullness_pct = 100 * weight_sum / (10 * full_reward_zone); - - if fullness_pct > 80 { - log::info!("adjust_priority: blocks {fullness_pct}% full, using Normal"); - Ok(FeePriority::Normal) + // --- Priority adjustment (existing logic) --- + let adjusted = if priority != FeePriority::Default { + priority } else { - log::info!("adjust_priority: blocks {fullness_pct}% full, using Low"); - Ok(FeePriority::Low) - } + // 1. Mempool backlog -> Normal + if info.tx_pool_size > 0 { + log::info!("resolve_fee_context: mempool has {} txs, using Normal", info.tx_pool_size); + FeePriority::Normal + } else { + // 2. Block fullness check — needs headers + let block_weight_limit = info + .extra + .get("block_weight_limit") + .and_then(|v| v.as_u64()) + .ok_or("block_weight_limit not in get_info")?; + let full_reward_zone = block_weight_limit / 2; + + if full_reward_zone == 0 || height < 10 { + FeePriority::Normal + } else { + let headers = pool + .get_block_headers_range(height.saturating_sub(10), height - 1) + .await + .map_err(|e| e.to_string())?; + let weight_sum: u64 = headers.iter().map(|h| h.block_weight).sum(); + let fullness_pct = 100 * weight_sum / (10 * full_reward_zone); + + if fullness_pct > 80 { + log::info!("resolve_fee_context: blocks {fullness_pct}% full, using Normal"); + FeePriority::Normal + } else { + log::info!("resolve_fee_context: blocks {fullness_pct}% full, using Low"); + FeePriority::Low + } + } + } + }; + + // --- Fee per byte from last block header --- + // Fetch the most recent block header for base_reward + hf_version. + // If we already fetched headers above we could reuse them, but the + // mempool-early-return path may skip the header fetch, so do a + // targeted single-header fetch here (cheap). + let last_header = + pool.get_block_headers_range(height - 1, height - 1).await.map_err(|e| e.to_string())?; + let hdr = last_header.first().ok_or("no headers returned for last block")?; + let fee_per_byte = salvium_tx::fee::dynamic_fee_per_byte(hdr.reward, hdr.major_version); + + Ok(FeeContext { priority: adjusted, fee_per_byte }) } /// Confirm with the user before proceeding. diff --git a/crates/salvium-ffi/src/transfer.rs b/crates/salvium-ffi/src/transfer.rs index 42ac8ad..22c6d46 100644 --- a/crates/salvium-ffi/src/transfer.rs +++ b/crates/salvium-ffi/src/transfer.rs @@ -89,64 +89,84 @@ fn parse_priority(s: &str) -> FeePriority { } } -/// Adjust fee priority based on network conditions (matches wallet2::adjust_priority). +/// Resolved network fee context — adjusted priority + dynamic fee rate. +struct FeeContext { + priority: FeePriority, + fee_per_byte: u64, +} + +/// Adjust priority from network conditions AND compute dynamic `fee_per_byte`. /// -/// When priority is `Default` (user didn't explicitly choose), queries the daemon: -/// 1. If mempool has pending transactions -> Normal (5x) -/// 2. If recent blocks are >80% full -> Normal (5x) -/// 3. Otherwise -> Low (1x) -/// -/// Explicit priorities (Low/Normal/High/Highest) pass through unchanged. -async fn adjust_priority(priority: FeePriority, pool: &salvium_rpc::NodePool) -> FeePriority { - if priority != FeePriority::Default { - return priority; - } - match try_adjust_priority(pool).await { - Ok(adjusted) => adjusted, +/// Uses the same block-header data for both — zero extra RPC calls. +async fn resolve_fee_context(pool: &salvium_rpc::NodePool, priority: FeePriority) -> FeeContext { + match try_resolve_fee_context(pool, priority).await { + Ok(ctx) => ctx, Err(e) => { - log::debug!("adjust_priority failed, using Normal: {e}"); - FeePriority::Normal + log::debug!("resolve_fee_context failed: {e}"); + FeeContext { + priority: if priority == FeePriority::Default { + FeePriority::Normal + } else { + priority + }, + fee_per_byte: fee::dynamic_fee_per_byte(0, 0), + } } } } -async fn try_adjust_priority(pool: &salvium_rpc::NodePool) -> Result { +async fn try_resolve_fee_context( + pool: &salvium_rpc::NodePool, + priority: FeePriority, +) -> Result { let info = pool.get_info().await.map_err(|e| e.to_string())?; - - // 1. Mempool backlog -> Normal - if info.tx_pool_size > 0 { - log::info!("adjust_priority: mempool has {} txs, using Normal", info.tx_pool_size); - return Ok(FeePriority::Normal); - } - - // 2. Block fullness check - let block_weight_limit = info - .extra - .get("block_weight_limit") - .and_then(|v| v.as_u64()) - .ok_or("block_weight_limit not in get_info")?; - let full_reward_zone = block_weight_limit / 2; - if full_reward_zone == 0 { - return Ok(FeePriority::Normal); - } - let height = info.height; - if height < 10 { - return Ok(FeePriority::Normal); - } - let headers = - pool.get_block_headers_range(height - 10, height - 1).await.map_err(|e| e.to_string())?; - let weight_sum: u64 = headers.iter().map(|h| h.block_weight).sum(); - let fullness_pct = 100 * weight_sum / (10 * full_reward_zone); - - if fullness_pct > 80 { - log::info!("adjust_priority: blocks {fullness_pct}% full, using Normal"); - Ok(FeePriority::Normal) + // --- Priority adjustment --- + let adjusted = if priority != FeePriority::Default { + priority } else { - log::info!("adjust_priority: blocks {fullness_pct}% full, using Low"); - Ok(FeePriority::Low) - } + // 1. Mempool backlog -> Normal + if info.tx_pool_size > 0 { + log::info!("resolve_fee_context: mempool has {} txs, using Normal", info.tx_pool_size); + FeePriority::Normal + } else { + // 2. Block fullness check + let block_weight_limit = info + .extra + .get("block_weight_limit") + .and_then(|v| v.as_u64()) + .ok_or("block_weight_limit not in get_info")?; + let full_reward_zone = block_weight_limit / 2; + + if full_reward_zone == 0 || height < 10 { + FeePriority::Normal + } else { + let headers = pool + .get_block_headers_range(height.saturating_sub(10), height - 1) + .await + .map_err(|e| e.to_string())?; + let weight_sum: u64 = headers.iter().map(|h| h.block_weight).sum(); + let fullness_pct = 100 * weight_sum / (10 * full_reward_zone); + + if fullness_pct > 80 { + log::info!("resolve_fee_context: blocks {fullness_pct}% full, using Normal"); + FeePriority::Normal + } else { + log::info!("resolve_fee_context: blocks {fullness_pct}% full, using Low"); + FeePriority::Low + } + } + } + }; + + // --- Fee per byte from last block header --- + let last_header = + pool.get_block_headers_range(height - 1, height - 1).await.map_err(|e| e.to_string())?; + let hdr = last_header.first().ok_or("no headers returned for last block")?; + let fee_per_byte = fee::dynamic_fee_per_byte(hdr.reward, hdr.major_version); + + Ok(FeeContext { priority: adjusted, fee_per_byte }) } /// Transfer funds to one or more destinations. @@ -190,9 +210,9 @@ pub unsafe extern "C" fn salvium_wallet_transfer( let rt = crate::runtime(); rt.block_on(async { - let priority = adjust_priority(priority, &dh.pool).await; + let fee_ctx = resolve_fee_context(&dh.pool, priority).await; let daemon = dh.pool.active_daemon().await; - do_transfer(&wh.wallet, &daemon, ¶ms, priority).await + do_transfer(&wh.wallet, &daemon, ¶ms, fee_ctx.priority, fee_ctx.fee_per_byte).await }) }) } @@ -234,9 +254,10 @@ pub unsafe extern "C" fn salvium_wallet_stake( let rt = crate::runtime(); rt.block_on(async { - let priority = adjust_priority(priority, &dh.pool).await; + let fee_ctx = resolve_fee_context(&dh.pool, priority).await; let daemon = dh.pool.active_daemon().await; - do_stake(&wh.wallet, &daemon, ¶ms, priority, false).await + do_stake(&wh.wallet, &daemon, ¶ms, fee_ctx.priority, fee_ctx.fee_per_byte, false) + .await }) }) } @@ -271,9 +292,10 @@ pub unsafe extern "C" fn salvium_wallet_stake_dry_run( let rt = crate::runtime(); rt.block_on(async { - let priority = adjust_priority(priority, &dh.pool).await; + let fee_ctx = resolve_fee_context(&dh.pool, priority).await; let daemon = dh.pool.active_daemon().await; - do_stake(&wh.wallet, &daemon, ¶ms, priority, true).await + do_stake(&wh.wallet, &daemon, ¶ms, fee_ctx.priority, fee_ctx.fee_per_byte, true) + .await }) }) } @@ -317,9 +339,9 @@ pub unsafe extern "C" fn salvium_wallet_sweep( let rt = crate::runtime(); rt.block_on(async { - let priority = adjust_priority(priority, &dh.pool).await; + let fee_ctx = resolve_fee_context(&dh.pool, priority).await; let daemon = dh.pool.active_daemon().await; - do_sweep(&wh.wallet, &daemon, ¶ms, priority).await + do_sweep(&wh.wallet, &daemon, ¶ms, fee_ctx.priority, fee_ctx.fee_per_byte).await }) }) } @@ -358,9 +380,9 @@ pub unsafe extern "C" fn salvium_wallet_transfer_dry_run( let rt = crate::runtime(); rt.block_on(async { - let priority = adjust_priority(priority, &dh.pool).await; + let fee_ctx = resolve_fee_context(&dh.pool, priority).await; let daemon = dh.pool.active_daemon().await; - do_transfer(&wh.wallet, &daemon, ¶ms, priority).await + do_transfer(&wh.wallet, &daemon, ¶ms, fee_ctx.priority, fee_ctx.fee_per_byte).await }) }) } @@ -389,6 +411,7 @@ async fn do_transfer( daemon: &DaemonRpc, params: &TransferParams, priority: FeePriority, + fee_per_byte: u64, ) -> Result { let (fork_rct, is_carrot) = detect_fork_params(daemon).await?; @@ -423,8 +446,15 @@ async fn do_transfer( // 2. Estimate fee. let out_type = if is_carrot { output_type::CARROT_V1 } else { output_type::TAGGED_KEY }; let num_outputs = destinations.len() + 1; // +1 for change - let est_fee = - fee::estimate_tx_fee(2, num_outputs, params.ring_size, is_carrot, out_type, priority); + let est_fee = fee::estimate_tx_fee( + 2, + num_outputs, + params.ring_size, + is_carrot, + out_type, + fee_per_byte, + priority, + ); // 3. Select UTXOs — caller specifies asset type, no output format filter. let selection = wallet @@ -446,6 +476,7 @@ async fn do_transfer( tx_type::TRANSFER, params.ring_size, priority, + fee_per_byte, 0, // amount_burnt fork_rct, is_carrot, @@ -470,6 +501,7 @@ async fn do_stake( daemon: &DaemonRpc, params: &StakeParams, priority: FeePriority, + fee_per_byte: u64, dry_run: bool, ) -> Result { let (fork_rct, is_carrot) = detect_fork_params(daemon).await?; @@ -488,7 +520,8 @@ async fn do_stake( // 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, 1, params.ring_size, is_carrot, out_type, priority); + let est_fee = + fee::estimate_tx_fee(2, 1, params.ring_size, is_carrot, out_type, fee_per_byte, priority); let selection = wallet .select_outputs( @@ -508,6 +541,7 @@ async fn do_stake( tx_type::STAKE, params.ring_size, priority, + fee_per_byte, amount, // amount_burnt = staked amount fork_rct, is_carrot, @@ -529,6 +563,7 @@ async fn do_sweep( daemon: &DaemonRpc, params: &SweepParams, priority: FeePriority, + fee_per_byte: u64, ) -> Result { let (fork_rct, is_carrot) = detect_fork_params(daemon).await?; @@ -551,8 +586,15 @@ async fn do_sweep( // 2. Estimate fee with actual input count. let n_inputs = all_selection.selected.len(); let out_type = if is_carrot { output_type::CARROT_V1 } else { output_type::TAGGED_KEY }; - let actual_fee = - fee::estimate_tx_fee(n_inputs, 2, params.ring_size, is_carrot, out_type, priority); + let actual_fee = fee::estimate_tx_fee( + n_inputs, + 2, + params.ring_size, + is_carrot, + out_type, + fee_per_byte, + priority, + ); let sweep_amount = all_selection.total.checked_sub(actual_fee).ok_or("balance too low to cover fee")?; @@ -580,6 +622,7 @@ async fn do_sweep( tx_type::TRANSFER, params.ring_size, priority, + fee_per_byte, 0, fork_rct, is_carrot, @@ -618,6 +661,7 @@ async fn build_sign_maybe_broadcast( tt: u8, ring_size: usize, priority: FeePriority, + fee_per_byte: u64, amount_burnt: u64, fork_rct_type: u8, is_carrot: bool, @@ -774,6 +818,7 @@ async fn build_sign_maybe_broadcast( ring_size, is_carrot, out_type, + fee_per_byte, priority, ); diff --git a/crates/salvium-tx/src/builder.rs b/crates/salvium-tx/src/builder.rs index 6509957..fd4af30 100644 --- a/crates/salvium-tx/src/builder.rs +++ b/crates/salvium-tx/src/builder.rs @@ -265,6 +265,7 @@ impl TransactionBuilder { ring_size, use_tclsag, out_type, + salvium_types::consensus::FEE_PER_BYTE, self.priority, ) }); diff --git a/crates/salvium-tx/src/fee.rs b/crates/salvium-tx/src/fee.rs index 01dff8c..5b43793 100644 --- a/crates/salvium-tx/src/fee.rs +++ b/crates/salvium-tx/src/fee.rs @@ -4,10 +4,11 @@ //! output count, ring size) and computes fees using per-byte fee constants //! from salvium-consensus. -use salvium_types::consensus::minimum_fee; - -#[cfg(test)] -use salvium_types::consensus::FEE_PER_BYTE; +use salvium_types::consensus::{ + DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD, DYNAMIC_FEE_PER_KB_BASE_FEE, FEE_PER_BYTE, + PER_KB_FEE_QUANTIZATION_DECIMALS, +}; +use salvium_types::constants::HfVersion; use crate::types::{output_type, rct_type}; @@ -138,28 +139,65 @@ pub fn estimate_tx_weight( } } +/// Compute the dynamic per-byte fee rate from the current base block reward. +/// +/// Replicates the C++ formula from `blockchain.cpp get_dynamic_base_fee_estimate()`: +/// fee_per_byte = max((DYNAMIC_FEE_PER_KB_BASE_FEE / 1024 * DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD) / base_reward, FEE_PER_BYTE) +/// +/// This is a pure function — no RPC calls needed. Pass `BlockHeader.reward` and +/// `BlockHeader.major_version` from block headers already fetched for priority adjustment. +pub fn dynamic_fee_per_byte(base_reward: u64, hf_version: u8) -> u64 { + if hf_version >= HfVersion::SCALING_2021 { + let base_fee = DYNAMIC_FEE_PER_KB_BASE_FEE / 1024; + if base_reward > 0 { + let f = (base_fee * DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD) / base_reward; + f.max(FEE_PER_BYTE) + } else { + FEE_PER_BYTE + } + } else { + FEE_PER_BYTE + } +} + +/// Fee quantization mask: `10^PER_KB_FEE_QUANTIZATION_DECIMALS - 1`. +/// +/// Fees are rounded up to the next multiple of `(mask + 1)` to match the C++ daemon's +/// `fee_quantization_mask` logic. +pub fn fee_quantization_mask() -> u64 { + 10u64.pow(PER_KB_FEE_QUANTIZATION_DECIMALS) - 1 +} + /// Estimate the fee for a transaction. +/// +/// `fee_per_byte` should come from [`dynamic_fee_per_byte()`]. The fee is quantized +/// upward to match the daemon's rounding. pub fn estimate_tx_fee( num_inputs: usize, num_outputs: usize, ring_size: usize, use_tclsag: bool, out_type: u8, + fee_per_byte: u64, priority: FeePriority, ) -> u64 { let weight = estimate_tx_weight(num_inputs, num_outputs, ring_size, use_tclsag, out_type); - let base_fee = minimum_fee(weight as u64, 0, 2); - base_fee * priority.multiplier() + let mut fee = weight as u64 * fee_per_byte * priority.multiplier(); + // Quantize (matches C++ fee_quantization_mask logic). + let mask = fee_quantization_mask(); + fee = ((fee + mask) / (mask + 1)) * (mask + 1); + fee } /// Quick fee estimate using defaults (CARROT, TCLSAG, Normal priority). -pub fn estimate_fee_simple(num_inputs: usize, num_outputs: usize) -> u64 { +pub fn estimate_fee_simple(num_inputs: usize, num_outputs: usize, fee_per_byte: u64) -> u64 { estimate_tx_fee( num_inputs, num_outputs, DEFAULT_RING_SIZE, true, output_type::CARROT_V1, + fee_per_byte, FeePriority::Normal, ) } @@ -269,26 +307,67 @@ mod tests { #[test] fn test_fee_estimation_nonzero() { - let fee = estimate_tx_fee(2, 2, 16, true, output_type::CARROT_V1, FeePriority::Normal); + let fee = estimate_tx_fee( + 2, + 2, + 16, + true, + output_type::CARROT_V1, + FEE_PER_BYTE, + FeePriority::Normal, + ); assert!(fee > 0, "fee should be nonzero"); } #[test] fn test_fee_increases_with_priority() { - let low = estimate_tx_fee(2, 2, 16, true, output_type::CARROT_V1, FeePriority::Low); - let normal = estimate_tx_fee(2, 2, 16, true, output_type::CARROT_V1, FeePriority::Normal); - let high = estimate_tx_fee(2, 2, 16, true, output_type::CARROT_V1, FeePriority::High); - assert!(normal > low); - assert!(high > normal); + let low = + estimate_tx_fee(2, 2, 16, true, output_type::CARROT_V1, FEE_PER_BYTE, FeePriority::Low); + let normal = estimate_tx_fee( + 2, + 2, + 16, + true, + output_type::CARROT_V1, + FEE_PER_BYTE, + FeePriority::Normal, + ); + let high = estimate_tx_fee( + 2, + 2, + 16, + true, + output_type::CARROT_V1, + FEE_PER_BYTE, + FeePriority::High, + ); + let highest = estimate_tx_fee( + 2, + 2, + 16, + true, + output_type::CARROT_V1, + FEE_PER_BYTE, + FeePriority::Highest, + ); + // With quantization, low/normal may round to the same quantum. + // But the ordering must hold weakly (>=), and extreme priorities must differ. + assert!(normal >= low); + assert!(high >= normal); + assert!(highest > high, "Highest priority should exceed High"); + assert!(highest > low, "Highest priority should exceed Low"); } #[test] fn test_estimate_fee_simple() { - let fee = estimate_fee_simple(2, 2); + let fee = estimate_fee_simple(2, 2, FEE_PER_BYTE); assert!(fee > 0); - // Should be around FEE_PER_BYTE * weight * 5 (normal priority). + // Should be quantized: weight * FEE_PER_BYTE * 5 rounded up to quantization boundary. let weight = estimate_tx_weight(2, 2, 16, true, output_type::CARROT_V1); - assert_eq!(fee, (weight as u64) * FEE_PER_BYTE * 5); + let raw = (weight as u64) * FEE_PER_BYTE * 5; + let mask = fee_quantization_mask(); + let expected = ((raw + mask) / (mask + 1)) * (mask + 1); + assert_eq!(fee, expected); } #[test] @@ -332,4 +411,61 @@ mod tests { assert!(!uses_tclsag(rct_type::CLSAG)); assert!(!uses_tclsag(rct_type::BULLETPROOF_PLUS)); } + + // ─── dynamic_fee_per_byte tests ────────────────────────────────────────── + + #[test] + fn test_dynamic_fee_per_byte_zero_reward() { + // base_reward=0 should fall back to FEE_PER_BYTE. + assert_eq!(dynamic_fee_per_byte(0, HfVersion::SCALING_2021), FEE_PER_BYTE); + } + + #[test] + fn test_dynamic_fee_per_byte_pre_scaling() { + // Pre-SCALING_2021 hf_version should always return FEE_PER_BYTE. + assert_eq!(dynamic_fee_per_byte(500_000_000, 1), FEE_PER_BYTE); + assert_eq!(dynamic_fee_per_byte(500_000_000, 0), FEE_PER_BYTE); + } + + #[test] + fn test_dynamic_fee_per_byte_known_reward() { + // With base_reward = DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD (1e9), + // fee = (200000/1024 * 1e9) / 1e9 = 200000/1024 = 195 (integer division). + let fpb = + dynamic_fee_per_byte(DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD, HfVersion::SCALING_2021); + assert_eq!(fpb, DYNAMIC_FEE_PER_KB_BASE_FEE / 1024); + } + + #[test] + fn test_dynamic_fee_per_byte_large_reward() { + // Very large base_reward → dynamic fee drops but floors to FEE_PER_BYTE. + let fpb = dynamic_fee_per_byte(1_000_000_000_000, HfVersion::SCALING_2021); + assert_eq!(fpb, FEE_PER_BYTE); + } + + #[test] + fn test_dynamic_fee_per_byte_small_reward() { + // Small base_reward → high dynamic fee. + let fpb = dynamic_fee_per_byte(100_000_000, HfVersion::SCALING_2021); + // (200000/1024 * 1e9) / 1e8 = 195 * 10 = 1950 + let expected = (DYNAMIC_FEE_PER_KB_BASE_FEE / 1024 * DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD) + / 100_000_000; + assert_eq!(fpb, expected); + assert!(fpb > FEE_PER_BYTE); + } + + #[test] + fn test_fee_quantization_mask() { + // PER_KB_FEE_QUANTIZATION_DECIMALS = 8, so mask = 10^8 - 1 = 99_999_999. + assert_eq!(fee_quantization_mask(), 99_999_999); + } + + #[test] + fn test_fee_quantization_applied() { + // Fees should be rounded up to next multiple of (mask+1) = 100_000_000. + let fee = + estimate_tx_fee(2, 2, 16, true, output_type::CARROT_V1, FEE_PER_BYTE, FeePriority::Low); + let quantum = fee_quantization_mask() + 1; // 100_000_000 + assert_eq!(fee % quantum, 0, "fee {} should be a multiple of {}", fee, quantum); + } } diff --git a/crates/salvium-tx/tests/testnet.rs b/crates/salvium-tx/tests/testnet.rs index b1d0a5d..ce7288c 100644 --- a/crates/salvium-tx/tests/testnet.rs +++ b/crates/salvium-tx/tests/testnet.rs @@ -233,8 +233,10 @@ async fn test_address_generation_testnet() { async fn test_fee_estimation_realistic() { use salvium_tx::fee::{estimate_tx_fee, FeePriority}; + use salvium_types::consensus::FEE_PER_BYTE; + // Standard transfer: 2 inputs, 2 outputs, ring size 16, TCLSAG, CARROT. - let fee = estimate_tx_fee(2, 2, 16, true, 0x04, FeePriority::Normal); + let fee = estimate_tx_fee(2, 2, 16, true, 0x04, FEE_PER_BYTE, FeePriority::Normal); assert!(fee > 0, "fee should be positive"); let fee_sal = fee as f64 / 1e9; @@ -242,9 +244,9 @@ async fn test_fee_estimation_realistic() { assert!(fee_sal < 1.0, "fee should be less than 1 SAL for a normal TX"); // Compare priorities. - let fee_low = estimate_tx_fee(2, 2, 16, true, 0x04, FeePriority::Low); - let fee_high = estimate_tx_fee(2, 2, 16, true, 0x04, FeePriority::High); - let fee_highest = estimate_tx_fee(2, 2, 16, true, 0x04, FeePriority::Highest); + let fee_low = estimate_tx_fee(2, 2, 16, true, 0x04, FEE_PER_BYTE, FeePriority::Low); + let fee_high = estimate_tx_fee(2, 2, 16, true, 0x04, FEE_PER_BYTE, FeePriority::High); + let fee_highest = estimate_tx_fee(2, 2, 16, true, 0x04, FEE_PER_BYTE, FeePriority::Highest); assert!(fee_low <= fee, "low priority fee should be <= normal"); assert!(fee_high >= fee, "high priority fee should be >= normal"); @@ -319,6 +321,7 @@ async fn test_build_and_sign_with_real_decoys() { DEFAULT_RING_SIZE, true, 0x04, + salvium_types::consensus::FEE_PER_BYTE, salvium_tx::fee::FeePriority::Normal, ); let change = amount - send_amount - fee; diff --git a/crates/salvium-wallet/tests/testnet_burn.rs b/crates/salvium-wallet/tests/testnet_burn.rs index d7512e6..e014b70 100644 --- a/crates/salvium-wallet/tests/testnet_burn.rs +++ b/crates/salvium-wallet/tests/testnet_burn.rs @@ -96,6 +96,7 @@ async fn test_burn_transaction_build() { DEFAULT_RING_SIZE, true, output_type::CARROT_V1, + salvium_types::consensus::FEE_PER_BYTE, FeePriority::Normal, ); diff --git a/crates/salvium-wallet/tests/testnet_stake.rs b/crates/salvium-wallet/tests/testnet_stake.rs index 7fa5f6b..160cafe 100644 --- a/crates/salvium-wallet/tests/testnet_stake.rs +++ b/crates/salvium-wallet/tests/testnet_stake.rs @@ -108,6 +108,7 @@ async fn test_stake_transaction_build() { DEFAULT_RING_SIZE, true, output_type::CARROT_V1, + salvium_types::consensus::FEE_PER_BYTE, FeePriority::Normal, ); let selection = wallet