267 lines
10 KiB
Rust
267 lines
10 KiB
Rust
use crate::blocks::swap::SwapTransaction;
|
|
use crate::common::asset_names::is_canonical_padded_asset_or_base;
|
|
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
|
use crate::common::nft_assets::{nft_asset_name, nft_ownership_type, validate_nft_amount};
|
|
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_at;
|
|
use crate::verifications::async_funcs::checks::verify_db::{
|
|
db_bytes_verification, db_hex_verification,
|
|
};
|
|
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<String, String> {
|
|
self.verify_at(db, Utc::now().timestamp() as u32).await
|
|
}
|
|
|
|
pub async fn verify_at(&self, db: &Db, as_of_timestamp: u32) -> 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(),
|
|
);
|
|
}
|
|
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,
|
|
) {
|
|
return Err(
|
|
"Swap assets must be the base coin or canonical alphanumeric asset ids."
|
|
.to_string(),
|
|
);
|
|
}
|
|
|
|
// 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.
|
|
let network_base_coin = network_base_coin_padded.trim().to_lowercase();
|
|
|
|
// Resolve NFT/RWA ownership rules from the concrete asset record.
|
|
let ticker1_ownership = nft_ownership_type(db, &asset1)?;
|
|
let ticker1_is_nft = if let Some(ownership_type) = ticker1_ownership {
|
|
validate_nft_amount(ownership_type, self.unsigned_swap.value1, "swap")?;
|
|
true
|
|
} else {
|
|
if self.unsigned_swap.nft_series1 > 0 {
|
|
return Err("Ticker1 NFT/RWA collection item does not exist.".to_string());
|
|
}
|
|
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;
|
|
if !(is_base_coin || is_token) {
|
|
return Err("Ticker1 does not exist.".to_string());
|
|
}
|
|
false
|
|
};
|
|
|
|
let ticker2_ownership = nft_ownership_type(db, &asset2)?;
|
|
let ticker2_is_nft = if let Some(ownership_type) = ticker2_ownership {
|
|
validate_nft_amount(ownership_type, self.unsigned_swap.value2, "swap")?;
|
|
true
|
|
} else {
|
|
if self.unsigned_swap.nft_series2 > 0 {
|
|
return Err("Ticker2 NFT/RWA collection item does not exist.".to_string());
|
|
}
|
|
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;
|
|
if !(is_base_coin || is_token) {
|
|
return Err("Ticker2 does not exist.".to_string());
|
|
}
|
|
false
|
|
};
|
|
|
|
// 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}."
|
|
));
|
|
}
|
|
|
|
// 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;
|
|
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_at(self.unsigned_swap.timestamp, as_of_timestamp) {
|
|
return Err(
|
|
"Timestamp is to old. Transactions must be broadcast within 30 days of signing."
|
|
.to_string(),
|
|
);
|
|
}
|
|
|
|
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 as_of_timestamp > 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())
|
|
}
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
}
|