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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, ¶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<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,
|
||||
);
|
||||
|
||||
|
||||
@@ -265,6 +265,7 @@ impl TransactionBuilder {
|
||||
ring_size,
|
||||
use_tclsag,
|
||||
out_type,
|
||||
salvium_types::consensus::FEE_PER_BYTE,
|
||||
self.priority,
|
||||
)
|
||||
});
|
||||
|
||||
+152
-16
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user