Replicate C++ dynamic fee calculation locally to fix fee_too_low rejections

The daemon rejects transactions with fee_too_low because the wallet was
  using a hardcoded FEE_PER_BYTE=30, while the daemon dynamically computes
  its minimum from the block reward. This replicates the C++ formula
  (blockchain.cpp get_dynamic_base_fee_estimate) as a pure local function
  and applies fee quantization matching the daemon's rounding.

  - Add dynamic_fee_per_byte(base_reward, hf_version) and
    fee_quantization_mask() to salvium-tx/fee.rs
  - Update estimate_tx_fee() to accept explicit fee_per_byte with
    quantization (round up to 10^8 boundary)
  - Replace adjust_priority() with resolve_fee_context() in both CLI
    (tx_common.rs) and FFI (transfer.rs), combining priority adjustment
    and fee rate computation in a single RPC roundtrip using
    BlockHeader.reward and BlockHeader.major_version
  - Add fee_per_byte field to TxPipeline
  - Thread fee_per_byte through all 13 CLI transfer commands, multisig
    transfer, and 5 FFI entry points (transfer, stake, stake_dry_run,
    sweep, transfer_dry_run)
  - Update all test files to pass explicit fee_per_byte values
This commit is contained in:
Matt Hess
2026-02-28 15:58:25 +00:00
parent 272fbfffde
commit 68e2dc2a41
9 changed files with 442 additions and 186 deletions
+5 -4
View File
@@ -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,
+75 -41
View File
@@ -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::<u64>().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::<u64>().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,
+92 -58
View File
@@ -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<salvium_tx::fee::FeePriority, String> {
priority: salvium_tx::fee::FeePriority,
) -> std::result::Result<FeeContext, String> {
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.
+108 -63
View File
@@ -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<FeePriority, String> {
async fn try_resolve_fee_context(
pool: &salvium_rpc::NodePool,
priority: FeePriority,
) -> Result<FeeContext, String> {
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, &params, priority).await
do_transfer(&wh.wallet, &daemon, &params, 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, &params, priority, false).await
do_stake(&wh.wallet, &daemon, &params, 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, &params, priority, true).await
do_stake(&wh.wallet, &daemon, &params, 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, &params, priority).await
do_sweep(&wh.wallet, &daemon, &params, 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, &params, priority).await
do_transfer(&wh.wallet, &daemon, &params, 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<String, String> {
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<String, String> {
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<String, String> {
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,
);
+1
View File
@@ -265,6 +265,7 @@ impl TransactionBuilder {
ring_size,
use_tclsag,
out_type,
salvium_types::consensus::FEE_PER_BYTE,
self.priority,
)
});
+152 -16
View File
@@ -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);
}
}
+7 -4
View File
@@ -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;
@@ -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,
);
@@ -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