diff --git a/src/verifications/async_funcs/verify_swap.rs b/src/verifications/async_funcs/verify_swap.rs index 78e623e..fb72cf9 100644 --- a/src/verifications/async_funcs/verify_swap.rs +++ b/src/verifications/async_funcs/verify_swap.rs @@ -17,6 +17,23 @@ use crate::verifications::async_funcs::checks::verify_db::{ use crate::wallets::structures::Wallet; 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 { pub async fn verify(&self, db: &Db) -> Result { // 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; - if !is_canonical_padded_asset_or_base(&self.unsigned_swap.ticker1, &network_base_coin_padded) - || !is_canonical_padded_asset_or_base( - &self.unsigned_swap.ticker2, - &network_base_coin_padded, - ) - { + if !is_canonical_padded_asset_or_base( + &self.unsigned_swap.ticker1, + &network_base_coin_padded, + ) || !is_canonical_padded_asset_or_base( + &self.unsigned_swap.ticker2, + &network_base_coin_padded, + ) { return Err( "Swap assets must be the base coin or canonical alphanumeric asset ids." .to_string(), @@ -100,33 +118,43 @@ impl SwapTransaction { // Numbered NFT swaps are single-item transfers, while non-series // 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 { return Err("Series NFTs must swap exactly 1 item.".to_string()); } if !db_bytes_verification(db, "nfts", &asset1).await { return Err("Ticker1 NFT item does not exist.".to_string()); } - } else if !(self.unsigned_swap.ticker1.trim().to_lowercase() == network_base_coin - || db_bytes_verification(db, "tokens", &self.unsigned_swap.ticker1).await - || db_bytes_verification(db, "nfts", &self.unsigned_swap.ticker1).await) - { - return Err("Ticker1 does not exist.".to_string()); - } + true + } else { + 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()); + } + 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 { return Err("Series NFTs must swap exactly 1 item.".to_string()); } if !db_bytes_verification(db, "nfts", &asset2).await { return Err("Ticker2 NFT item does not exist.".to_string()); } - } else if !(self.unsigned_swap.ticker2.trim().to_lowercase() == network_base_coin - || db_bytes_verification(db, "tokens", &self.unsigned_swap.ticker2).await - || db_bytes_verification(db, "nfts", &self.unsigned_swap.ticker2).await) - { - return Err("Ticker2 does not exist.".to_string()); - } + true + } else { + 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()); + } + is_nft + }; // Each signer pays the fixed swap fee for their side of the trade. 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 // asset-denominated tip and the base-coin transaction fee. let full_value1 = self.unsigned_swap.value1 + self.unsigned_swap.tip1; @@ -211,3 +255,21 @@ impl SwapTransaction { 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()); + } +}