swap tip consensus fix

This commit is contained in:
viraladmin 2026-06-24 13:15:31 -06:00
parent a85e9aab4c
commit fe83bd50dc
1 changed files with 82 additions and 20 deletions

View File

@ -17,6 +17,23 @@ use crate::verifications::async_funcs::checks::verify_db::{
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::Utc; use crate::Utc;
fn validate_swap_tip(value: u64, tip: u64, is_nft: bool, sender: &str) -> Result<(), String> {
if is_nft {
if tip != 0 {
return Err(format!("Swap {sender} NFT tip must be zero."));
}
return Ok(());
}
let minimum_tip = value.div_ceil(100);
if tip < minimum_tip {
return Err(format!(
"Swap {sender} tip must be at least 1% of the offered amount."
));
}
Ok(())
}
impl SwapTransaction { impl SwapTransaction {
pub async fn verify(&self, db: &Db) -> Result<String, String> { pub async fn verify(&self, db: &Db) -> Result<String, String> {
// Transactions already present in the mempool can short-circuit // Transactions already present in the mempool can short-circuit
@ -77,12 +94,13 @@ impl SwapTransaction {
); );
} }
let network_base_coin_padded = block_extension_and_paths().1; let network_base_coin_padded = block_extension_and_paths().1;
if !is_canonical_padded_asset_or_base(&self.unsigned_swap.ticker1, &network_base_coin_padded) if !is_canonical_padded_asset_or_base(
|| !is_canonical_padded_asset_or_base( &self.unsigned_swap.ticker1,
&network_base_coin_padded,
) || !is_canonical_padded_asset_or_base(
&self.unsigned_swap.ticker2, &self.unsigned_swap.ticker2,
&network_base_coin_padded, &network_base_coin_padded,
) ) {
{
return Err( return Err(
"Swap assets must be the base coin or canonical alphanumeric asset ids." "Swap assets must be the base coin or canonical alphanumeric asset ids."
.to_string(), .to_string(),
@ -100,33 +118,43 @@ impl SwapTransaction {
// Numbered NFT swaps are single-item transfers, while non-series // Numbered NFT swaps are single-item transfers, while non-series
// assets must already exist as a token, NFT, or base coin. // assets must already exist as a token, NFT, or base coin.
if self.unsigned_swap.nft_series1 > 0 { let ticker1_is_nft = if self.unsigned_swap.nft_series1 > 0 {
if self.unsigned_swap.value1 != 1 { if self.unsigned_swap.value1 != 1 {
return Err("Series NFTs must swap exactly 1 item.".to_string()); return Err("Series NFTs must swap exactly 1 item.".to_string());
} }
if !db_bytes_verification(db, "nfts", &asset1).await { if !db_bytes_verification(db, "nfts", &asset1).await {
return Err("Ticker1 NFT item does not exist.".to_string()); return Err("Ticker1 NFT item does not exist.".to_string());
} }
} else if !(self.unsigned_swap.ticker1.trim().to_lowercase() == network_base_coin true
|| db_bytes_verification(db, "tokens", &self.unsigned_swap.ticker1).await } else {
|| db_bytes_verification(db, "nfts", &self.unsigned_swap.ticker1).await) let is_base_coin =
{ self.unsigned_swap.ticker1.trim().to_lowercase() == network_base_coin;
let is_token = db_bytes_verification(db, "tokens", &self.unsigned_swap.ticker1).await;
let is_nft = db_bytes_verification(db, "nfts", &self.unsigned_swap.ticker1).await;
if !(is_base_coin || is_token || is_nft) {
return Err("Ticker1 does not exist.".to_string()); return Err("Ticker1 does not exist.".to_string());
} }
is_nft
};
if self.unsigned_swap.nft_series2 > 0 { let ticker2_is_nft = if self.unsigned_swap.nft_series2 > 0 {
if self.unsigned_swap.value2 != 1 { if self.unsigned_swap.value2 != 1 {
return Err("Series NFTs must swap exactly 1 item.".to_string()); return Err("Series NFTs must swap exactly 1 item.".to_string());
} }
if !db_bytes_verification(db, "nfts", &asset2).await { if !db_bytes_verification(db, "nfts", &asset2).await {
return Err("Ticker2 NFT item does not exist.".to_string()); return Err("Ticker2 NFT item does not exist.".to_string());
} }
} else if !(self.unsigned_swap.ticker2.trim().to_lowercase() == network_base_coin true
|| db_bytes_verification(db, "tokens", &self.unsigned_swap.ticker2).await } else {
|| db_bytes_verification(db, "nfts", &self.unsigned_swap.ticker2).await) let is_base_coin =
{ self.unsigned_swap.ticker2.trim().to_lowercase() == network_base_coin;
let is_token = db_bytes_verification(db, "tokens", &self.unsigned_swap.ticker2).await;
let is_nft = db_bytes_verification(db, "nfts", &self.unsigned_swap.ticker2).await;
if !(is_base_coin || is_token || is_nft) {
return Err("Ticker2 does not exist.".to_string()); return Err("Ticker2 does not exist.".to_string());
} }
is_nft
};
// Each signer pays the fixed swap fee for their side of the trade. // Each signer pays the fixed swap fee for their side of the trade.
if self.unsigned_swap.txfee1 < SWAP_FEE { if self.unsigned_swap.txfee1 < SWAP_FEE {
@ -141,6 +169,22 @@ impl SwapTransaction {
)); ));
} }
// Fungible swap sides pay an asset-denominated miner tip of at
// least 1% of the offered amount. NFT sides cannot pay a
// fractional NFT tip, so their tip must be zero.
validate_swap_tip(
self.unsigned_swap.value1,
self.unsigned_swap.tip1,
ticker1_is_nft,
"sender1",
)?;
validate_swap_tip(
self.unsigned_swap.value2,
self.unsigned_swap.tip2,
ticker2_is_nft,
"sender2",
)?;
// Each side must be able to cover the offered amount plus any // Each side must be able to cover the offered amount plus any
// asset-denominated tip and the base-coin transaction fee. // asset-denominated tip and the base-coin transaction fee.
let full_value1 = self.unsigned_swap.value1 + self.unsigned_swap.tip1; let full_value1 = self.unsigned_swap.value1 + self.unsigned_swap.tip1;
@ -211,3 +255,21 @@ impl SwapTransaction {
Ok(sign.to_string()) Ok(sign.to_string())
} }
} }
#[cfg(test)]
mod tests {
use super::validate_swap_tip;
#[test]
fn fungible_swap_tip_requires_one_percent_rounded_up() {
assert!(validate_swap_tip(100, 1, false, "sender1").is_ok());
assert!(validate_swap_tip(101, 2, false, "sender1").is_ok());
assert!(validate_swap_tip(101, 1, false, "sender1").is_err());
}
#[test]
fn nft_swap_tip_must_be_zero() {
assert!(validate_swap_tip(1, 0, true, "sender1").is_ok());
assert!(validate_swap_tip(1, 1, true, "sender1").is_err());
}
}