Contractless/src/verifications/async_funcs/verify_swap.rs

269 lines
10 KiB
Rust
Raw Normal View History

2026-05-24 17:56:57 +00:00
use crate::blocks::swap::SwapTransaction;
2026-06-23 17:24:35 +00:00
use crate::common::asset_names::is_canonical_padded_asset_or_base;
2026-05-24 17:56:57 +00:00
use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::common::nft_assets::nft_asset_name;
use crate::common::types::{COIN_LENGTH, SWAP_FEE};
use crate::encode;
use crate::records::wallet_registry::{
require_canonical_registered_short_address, resolve_pubkey_from_short_address,
};
use crate::sled::Db;
use crate::verifications::async_funcs::checks::balance_check::balance_checkup;
use crate::verifications::async_funcs::checks::time_checks::is_within_30_days;
use crate::verifications::async_funcs::checks::verify_db::{
db_bytes_verification, db_hex_verification,
};
use crate::wallets::structures::Wallet;
use crate::Utc;
2026-06-24 19:15:31 +00:00
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(())
}
2026-05-24 17:56:57 +00:00
impl SwapTransaction {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
let hash = self.unsigned_swap.hash().await;
// Both swap participants must provide valid wallet addresses and
// matching signatures over the shared unsigned swap payload.
if !(Wallet::short_address_validation(&self.unsigned_swap.sender1)
&& Wallet::short_address_validation(&self.unsigned_swap.sender2))
{
return Err("Sender1 or Sender2 Wallet Address is Invalid.".to_string());
}
require_canonical_registered_short_address(
db,
&self.unsigned_swap.sender1,
"Sender1 Wallet Address",
)?;
require_canonical_registered_short_address(
db,
&self.unsigned_swap.sender2,
"Sender2 Wallet Address",
)?;
// Sender1 must sign the same unsigned swap payload that sender2
// signs, proving agreement to the offered side of the swap.
let sender1_pubkey = resolve_pubkey_from_short_address(db, &self.unsigned_swap.sender1)
.map_err(|_| "Sender1 Wallet Address is not registered.".to_string())?
.ok_or_else(|| "Sender1 Wallet Address is not registered.".to_string())?;
let sender1_pubkey_hex = encode(&sender1_pubkey);
if !Wallet::verify_transaction_with_public_key(&hash, &self.signature1, &sender1_pubkey_hex)
.await
{
return Err("Invalid signature1 the RewardsTransaction.".to_string());
}
// Sender2's registered public key verifies the counterparty
// signature on the same swap hash.
let sender2_pubkey = resolve_pubkey_from_short_address(db, &self.unsigned_swap.sender2)
.map_err(|_| "Sender2 Wallet Address is not registered.".to_string())?
.ok_or_else(|| "Sender2 Wallet Address is not registered.".to_string())?;
let sender2_pubkey_hex = encode(&sender2_pubkey);
if !Wallet::verify_transaction_with_public_key(&hash, &self.signature2, &sender2_pubkey_hex)
.await
{
return Err("Invalid signature2 the RewardsTransaction.".to_string());
}
// Asset names use the fixed padded coin-length format.
if self.unsigned_swap.ticker1.len() != COIN_LENGTH
|| self.unsigned_swap.ticker2.len() != COIN_LENGTH
{
return Err(
"Ticker lengths are invalid. Consider padding with empty spaces".to_string(),
);
}
2026-06-23 17:24:35 +00:00
let network_base_coin_padded = block_extension_and_paths().1;
2026-06-24 19:15:31 +00:00
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,
) {
2026-06-23 17:24:35 +00:00
return Err(
"Swap assets must be the base coin or canonical alphanumeric asset ids."
.to_string(),
);
}
2026-05-24 17:56:57 +00:00
// Build concrete asset names for NFT series swaps before checking
// balances and registry existence.
let asset1 = nft_asset_name(&self.unsigned_swap.ticker1, self.unsigned_swap.nft_series1);
let asset2 = nft_asset_name(&self.unsigned_swap.ticker2, self.unsigned_swap.nft_series2);
// The base coin name comes from the active network settings so
// mainnet and testnet validate against their own ticker.
2026-06-23 17:24:35 +00:00
let network_base_coin = network_base_coin_padded.trim().to_lowercase();
2026-05-24 17:56:57 +00:00
// Numbered NFT swaps are single-item transfers, while non-series
// assets must already exist as a token, NFT, or base coin.
2026-06-24 19:15:31 +00:00
let ticker1_is_nft = if self.unsigned_swap.nft_series1 > 0 {
2026-05-24 17:56:57 +00:00
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());
}
2026-06-24 19:15:31 +00:00
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
};
2026-05-24 17:56:57 +00:00
2026-06-24 19:15:31 +00:00
let ticker2_is_nft = if self.unsigned_swap.nft_series2 > 0 {
2026-05-24 17:56:57 +00:00
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());
}
2026-06-24 19:15:31 +00:00
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
};
2026-05-24 17:56:57 +00:00
// Each signer pays the fixed swap fee for their side of the trade.
if self.unsigned_swap.txfee1 < SWAP_FEE {
return Err(format!(
"Swap sender1 transaction fee is below the minimum required fee of {SWAP_FEE}."
));
}
if self.unsigned_swap.txfee2 < SWAP_FEE {
return Err(format!(
"Swap sender2 transaction fee is below the minimum required fee of {SWAP_FEE}."
));
}
2026-06-24 19:15:31 +00:00
// 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",
)?;
2026-05-24 17:56:57 +00:00
// 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;
if !balance_checkup(
db,
full_value1,
self.unsigned_swap.txfee1,
asset1.clone(),
&self.unsigned_swap.sender1,
)
.await
{
return Err("Insuficient funds for this Swap Transaction!".to_string());
}
let full_value2 = self.unsigned_swap.value2 + self.unsigned_swap.tip2;
if !balance_checkup(
db,
full_value2,
self.unsigned_swap.txfee2,
asset2,
&self.unsigned_swap.sender2,
)
.await
{
return Err("Insuficient funds for this Swap Transaction!".to_string());
}
// Swap offers are bounded both by signature age and by the
// explicit offer-expiration window carried in the transaction.
if !is_within_30_days(self.unsigned_swap.timestamp).await {
return Err(
"Timestamp is to old. Transactions must be broadcast within 30 days of signing."
.to_string(),
);
}
let now = Utc::now().timestamp() as u32;
let offer_expiration = self.unsigned_swap.offer_expiration;
let timestamp = self.unsigned_swap.timestamp;
if offer_expiration < timestamp {
return Err(
"Offer expiration cannot be earlier than the transaction timestamp.".to_string(),
);
}
if offer_expiration > timestamp.saturating_add(30 * 24 * 60 * 60) {
return Err(
"Offer expiration cannot be more than 30 days after the transaction timestamp."
.to_string(),
);
}
if now > offer_expiration {
return Err("This swap offer has expired.".to_string());
}
// Saved-chain duplicates are rejected by txid even if the mempool
// did not already contain the transaction.
let tree = "txid";
if !db_hex_verification(db, tree, &hash).await {
return Err("This transaction already exists.".to_string());
}
// Verification returns no auxiliary cleanup marker for this transaction type.
let sign = "";
Ok(sign.to_string())
}
}
2026-06-24 19:15:31 +00:00
#[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());
}
}