From 09c8b650944f5a181967acf2dd38f270244cc533 Mon Sep 17 00:00:00 2001
From: ColinRhys <31706927+ColinRhys@users.noreply.github.com>
Date: Tue, 19 Dec 2023 09:25:13 -0600
Subject: [PATCH 1/2] initial commit

---
 src/lib.rs | 52 +++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 43 insertions(+), 9 deletions(-)

diff --git a/src/lib.rs b/src/lib.rs
index e199349..f662210 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -38,7 +38,7 @@ pub struct ProjectedFees {
     pub minimum_fee: (u64, u64),
 }
 
-pub async fn calculate_fee(utxos: Option<Vec<Utxo>>, amount: u64) -> Result<ProjectedFees, FestivusError> {
+pub async fn calculate_fee(utxos: Option<Vec<Utxo>>, amount: u64, user_fee_rate: Option<u64>) -> Result<ProjectedFees, FestivusError> {
     // Create a random taproot keypair for the ouput.
     let secp = Secp256k1::new();
     let mut rand = rand::thread_rng();
@@ -87,13 +87,24 @@ pub async fn calculate_fee(utxos: Option<Vec<Utxo>>, amount: u64) -> Result<Proj
     let virtual_bytes = weight.to_vbytes_ceil();
 
     // Get fees
-    let fees = reqwest::get("https://mempool.space/api/v1/fees/recommended")
+    let fees = if let Some(fee_rate) = user_fee_rate {
+        // User provides a fee rate
+        RecommendedFess {
+            fastest_fee: fee_rate,
+            half_hour_fee: fee_rate,
+            hour_fee: fee_rate,
+            economy_fee: fee_rate,
+            minimum_fee: fee_rate,
+        }
+    } else {
+        // User does not provide a fee rate
+        reqwest::get("https://mempool.space/api/v1/fees/recommended")
         .await
         .map_err(|_| FestivusError::ReqwestError)?
         .json::<RecommendedFess>()
         .await
-        .map_err(|_| FestivusError::ReqwestError)?;
-
+        .map_err(|_| FestivusError::ReqwestError)?
+    };
     // Calc total amount
     Ok(ProjectedFees {
         fastest_fee: (virtual_bytes * fees.fastest_fee, fees.fastest_fee),
@@ -163,7 +174,7 @@ mod tests {
 
         let utxos = vec![utxo_one, utxo_two];
 
-        let fees = calculate_fee(Some(utxos), 19_000).await;
+        let fees = calculate_fee(Some(utxos), 19_000, None).await;
 
         assert_eq!(fees.is_ok(), true)
     }
@@ -184,14 +195,14 @@ mod tests {
 
         let utxos = vec![utxo_one, utxo_two];
 
-        let fees = calculate_fee(Some(utxos), 19_000).await;
+        let fees = calculate_fee(Some(utxos), 19_000, None).await;
 
         assert_eq!(fees.is_ok(), true)
     }
 
     #[tokio::test]
     async fn no_utxos() {
-        let fees = calculate_fee(None, 19_000).await;
+        let fees = calculate_fee(None, 19_000, None).await;
 
         assert_eq!(fees.is_ok(), true)
     }
@@ -213,7 +224,7 @@ mod tests {
 
         let utxos = vec![utxo_one, utxo_two];
 
-        let fees = calculate_fee(Some(utxos), 125_000_000).await;
+        let fees = calculate_fee(Some(utxos), 125_000_000, None).await;
 
         assert_eq!(fees.is_ok(), true)
     }
@@ -234,8 +245,31 @@ mod tests {
 
         let utxos = vec![utxo_one, utxo_two];
 
-        let fees = calculate_fee(Some(utxos), 19_000).await;
+        let fees = calculate_fee(Some(utxos), 19_000, None).await;
 
         assert_eq!(fees.is_err(), true)
     }
+
+    #[tokio::test]
+    async fn calculate_fee_with_user_provided_rate() {
+        let mut utxo_one = Utxo::default();
+        utxo_one.amount_sat = Amount::from_btc(1.0).unwrap().to_sat() as i64;
+
+        let mut utxo_two = Utxo::default();
+        utxo_two.amount_sat = Amount::from_btc(0.5).unwrap().to_sat() as i64;
+
+        let utxos = vec![utxo_one, utxo_two];
+
+        let user_fee_rate = 50;
+
+        let fees = calculate_fee(Some(utxos), 125_000_000, Some(user_fee_rate)).await.unwrap();
+
+        // Testing the second item in each fee pair of the ProjectedFees struct 
+        // which is just the fee rate not the math of each fee * virtual_bytes
+        assert_eq!(fees.fastest_fee.1, user_fee_rate);
+        assert_eq!(fees.half_hour_fee.1, user_fee_rate);
+        assert_eq!(fees.hour_fee.1, user_fee_rate);
+        assert_eq!(fees.economy_fee.1, user_fee_rate);
+        assert_eq!(fees.minimum_fee.1, user_fee_rate);
+    }
 }

From 0371ae7a6b4e888ccd63734127c474750256264e Mon Sep 17 00:00:00 2001
From: ColinRhys <31706927+ColinRhys@users.noreply.github.com>
Date: Wed, 20 Dec 2023 13:13:33 -0600
Subject: [PATCH 2/2] initial commit - feature flag - tests - code change for
 async vs no async

---
 Cargo.toml |   3 +-
 README.md  |  18 +++++
 src/lib.rs | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 229 insertions(+), 1 deletion(-)

diff --git a/Cargo.toml b/Cargo.toml
index bb7afe6..5cd9189 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,11 +9,12 @@ description = "Calculate a projected fee for a channel open with LND UTXOs"
 
 [features]
 default = []
+blocking = []
 
 [dependencies]
 bitcoin = { version = "0.31.0", features = ["serde", "rand"] }
 rand = "0.8.5"
-reqwest = { version = "0.11.22", features = ["json"] }
+reqwest = { version = "0.11.22", features = ["blocking", "json"] }
 serde = { version = "1.0.193", features = ["derive"] }
 serde_json = "1.0.108"
 thiserror = "1.0.50"
diff --git a/README.md b/README.md
index d3be378..0903a4b 100644
--- a/README.md
+++ b/README.md
@@ -10,3 +10,21 @@ let projected_fees = calculate_fees(utxos, amount);
 
 let (total_fees, sat_vbyte) = projected_fees.fastest_fee;
 ```
+
+Festivus now supports both asynchronous and synchronous execution modes.
+
+By default, the calculate_fee function operates asynchronously. This is suitable for most applications that require non-blocking operations.
+
+If your application requires synchronous execution, you can enable the synchronous feature of festivus. To do this, add the following to your Cargo.toml:
+
+[dependencies]
+festivus = { git = "https://github.com/BlockSpaces/festivus", features = ["blocking"] }
+This enables the calculate_fee function to operate in a blocking manner.
+
+Testing:
+
+The festivus crate includes tests for both asynchronous and synchronous modes. You can run tests for the default asynchronous mode using:
+cargo test
+
+To run tests for the synchronous mode, use:
+cargo test --features "blocking"
\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index f662210..ff625a0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -38,6 +38,8 @@ pub struct ProjectedFees {
     pub minimum_fee: (u64, u64),
 }
 
+// Asynchronous version ( default )
+#[cfg(not(feature = "blocking"))]
 pub async fn calculate_fee(utxos: Option<Vec<Utxo>>, amount: u64, user_fee_rate: Option<u64>) -> Result<ProjectedFees, FestivusError> {
     // Create a random taproot keypair for the ouput.
     let secp = Secp256k1::new();
@@ -115,6 +117,83 @@ pub async fn calculate_fee(utxos: Option<Vec<Utxo>>, amount: u64, user_fee_rate:
     })
 }
 
+// Synchronous version ( when "blocking" feature is enabled )
+#[cfg(feature = "blocking")]
+pub fn calculate_fee(utxos: Option<Vec<Utxo>>, amount: u64, user_fee_rate: Option<u64>) -> Result<ProjectedFees, FestivusError> {
+    // Create a random taproot keypair for the ouput.
+    let secp = Secp256k1::new();
+    let mut rand = rand::thread_rng();
+    let (secret_key, _) = secp.generate_keypair(&mut rand);
+    let keypair = Keypair::from_secret_key(&secp, &secret_key);
+    let (pubkey, _) = XOnlyPublicKey::from_keypair(&keypair);
+
+    // The channel open output, P2WSH
+    let funding_output = TxOut {
+        value: Amount::from_sat(336),
+        script_pubkey: ScriptBuf::new_p2wsh(&WScriptHash::hash(&[0u8; 43])),
+    };
+
+    // The change output, LND defaults to P2TR.
+    let change_output = TxOut {
+        value: Amount::from_sat(256),
+        script_pubkey: ScriptBuf::new_p2tr(&secp, pubkey, None),
+    };
+
+    // The final valid transaction.
+    let txn = Transaction {
+        version: transaction::Version::TWO,
+        lock_time: LockTime::ZERO,
+        input: Vec::with_capacity(2),
+        output: vec![funding_output, change_output],
+    };
+
+    let utxos = match utxos {
+        Some(u) => u,
+        None => {
+            let mut utxo = Utxo::default();
+            utxo.amount_sat = amount as i64;
+            utxo.outpoint = Some(tonic_lnd::lnrpc::OutPoint {
+                txid_bytes: Txid::all_zeros().to_string().as_bytes().to_owned(),
+                txid_str: Txid::all_zeros().to_string(),
+                output_index: 1,
+            });
+            utxo.address_type = 1;
+            vec![utxo]
+        }
+    };
+    let inputs = predict_weight_for_inputs(utxos, amount)?;
+
+    let weight = transaction::predict_weight(inputs, txn.script_pubkey_lens());
+
+    let virtual_bytes = weight.to_vbytes_ceil();
+
+    // Get fees
+    let fees = if let Some(fee_rate) = user_fee_rate {
+        // User provides a fee rate
+        RecommendedFess {
+            fastest_fee: fee_rate,
+            half_hour_fee: fee_rate,
+            hour_fee: fee_rate,
+            economy_fee: fee_rate,
+            minimum_fee: fee_rate,
+        }
+    } else {
+        // User does not provide a fee rate
+        reqwest::blocking::get("https://mempool.space/api/v1/fees/recommended")
+        .map_err(|_| FestivusError::ReqwestError)?
+        .json::<RecommendedFess>()
+        .map_err(|_| FestivusError::ReqwestError)?
+    };
+    // Calc total amount
+    Ok(ProjectedFees {
+        fastest_fee: (virtual_bytes * fees.fastest_fee, fees.fastest_fee),
+        half_hour_fee: (virtual_bytes * fees.half_hour_fee, fees.half_hour_fee),
+        hour_fee: (virtual_bytes * fees.hour_fee, fees.hour_fee),
+        economy_fee: (virtual_bytes * fees.economy_fee, fees.economy_fee),
+        minimum_fee: (virtual_bytes * fees.minimum_fee, fees.minimum_fee),
+    })
+}
+
 fn predict_weight_for_inputs(mut utxos: Vec<Utxo>, amount: u64) -> Result<Vec<InputWeightPrediction>, FestivusError> {
     // Sort the UTXO's for largest first selection.
     // This is the default coin selection algorithm for LND
@@ -159,6 +238,7 @@ mod tests {
     use bitcoin::Txid;
 
     #[tokio::test]
+    #[cfg(not(feature = "blocking"))]
     async fn calc_fee_p2tr_inputs() {
         let mut utxo_one = Utxo::default();
         utxo_one.amount_sat = Amount::from_btc(3.6).unwrap().to_sat() as i64;
@@ -180,6 +260,7 @@ mod tests {
     }
 
     #[tokio::test]
+    #[cfg(not(feature = "blocking"))]
     async fn calc_fee_p2wkh_inputs() {
         let mut utxo_one = Utxo::default();
         utxo_one.amount_sat = Amount::from_btc(3.6).unwrap().to_sat() as i64;
@@ -201,6 +282,7 @@ mod tests {
     }
 
     #[tokio::test]
+    #[cfg(not(feature = "blocking"))]
     async fn no_utxos() {
         let fees = calculate_fee(None, 19_000, None).await;
 
@@ -208,6 +290,7 @@ mod tests {
     }
 
     #[tokio::test]
+    #[cfg(not(feature = "blocking"))]
     async fn calc_fee_two_inputs() {
         let mut utxo_one = Utxo::default();
         utxo_one.amount_sat = Amount::from_btc(1.0).unwrap().to_sat() as i64;
@@ -230,6 +313,7 @@ mod tests {
     }
 
     #[tokio::test]
+    #[cfg(not(feature = "blocking"))]
     async fn not_enough_btc() {
         let mut utxo_one = Utxo::default();
         utxo_one.amount_sat = Amount::from_sat(10_000).to_sat() as i64;
@@ -251,6 +335,7 @@ mod tests {
     }
 
     #[tokio::test]
+    #[cfg(not(feature = "blocking"))]
     async fn calculate_fee_with_user_provided_rate() {
         let mut utxo_one = Utxo::default();
         utxo_one.amount_sat = Amount::from_btc(1.0).unwrap().to_sat() as i64;
@@ -272,4 +357,128 @@ mod tests {
         assert_eq!(fees.economy_fee.1, user_fee_rate);
         assert_eq!(fees.minimum_fee.1, user_fee_rate);
     }
+
+    // Start tests using "blocking"
+    // Run with ----- cargo test --features "blocking"
+
+    #[test]
+    #[cfg(feature = "blocking")]
+    fn calc_fee_p2tr_inputs_non_async() {
+        let mut utxo_one = Utxo::default();
+        utxo_one.amount_sat = Amount::from_btc(3.6).unwrap().to_sat() as i64;
+        utxo_one.outpoint = Some(tonic_lnd::lnrpc::OutPoint {
+            txid_bytes: Txid::all_zeros().to_string().as_bytes().to_owned(),
+            txid_str: Txid::all_zeros().to_string(),
+            output_index: 1,
+        });
+        utxo_one.address_type = 4;
+
+        let mut utxo_two = Utxo::default();
+        utxo_two.amount_sat = Amount::from_btc(1.2).unwrap().to_sat() as i64;
+
+        let utxos = vec![utxo_one, utxo_two];
+
+        let fees = calculate_fee(Some(utxos), 19_000, None);
+
+        assert_eq!(fees.is_ok(), true)
+    }
+
+    #[test]
+    #[cfg(feature = "blocking")]
+    fn calc_fee_p2wkh_inputs_not_async() {
+        let mut utxo_one = Utxo::default();
+        utxo_one.amount_sat = Amount::from_btc(3.6).unwrap().to_sat() as i64;
+        utxo_one.outpoint = Some(tonic_lnd::lnrpc::OutPoint {
+            txid_bytes: Txid::all_zeros().to_string().as_bytes().to_owned(),
+            txid_str: Txid::all_zeros().to_string(),
+            output_index: 1,
+        });
+        utxo_one.address_type = 1;
+
+        let mut utxo_two = Utxo::default();
+        utxo_two.amount_sat = Amount::from_btc(1.2).unwrap().to_sat() as i64;
+
+        let utxos = vec![utxo_one, utxo_two];
+
+        let fees = calculate_fee(Some(utxos), 19_000, None);
+
+        assert_eq!(fees.is_ok(), true)
+    }
+
+    #[test]
+    #[cfg(feature = "blocking")]
+    fn no_utxo_not_asyncs() {
+        let fees = calculate_fee(None, 19_000, None);
+
+        assert_eq!(fees.is_ok(), true)
+    }
+
+    #[test]
+    #[cfg(feature = "blocking")]
+    fn calc_fee_two_inputs_not_async() {
+        let mut utxo_one = Utxo::default();
+        utxo_one.amount_sat = Amount::from_btc(1.0).unwrap().to_sat() as i64;
+        utxo_one.outpoint = Some(tonic_lnd::lnrpc::OutPoint {
+            txid_bytes: Txid::all_zeros().to_string().as_bytes().to_owned(),
+            txid_str: Txid::all_zeros().to_string(),
+            output_index: 1,
+        });
+        utxo_one.address_type = 1;
+
+        let mut utxo_two = Utxo::default();
+        utxo_two.amount_sat = Amount::from_btc(0.5).unwrap().to_sat() as i64;
+        utxo_two.address_type = 1;
+
+        let utxos = vec![utxo_one, utxo_two];
+
+        let fees = calculate_fee(Some(utxos), 125_000_000, None);
+
+        assert_eq!(fees.is_ok(), true)
+    }
+
+    #[test]
+    #[cfg(feature = "blocking")]
+    fn not_enough_btc_not_async() {
+        let mut utxo_one = Utxo::default();
+        utxo_one.amount_sat = Amount::from_sat(10_000).to_sat() as i64;
+        utxo_one.outpoint = Some(tonic_lnd::lnrpc::OutPoint {
+            txid_bytes: Txid::all_zeros().to_string().as_bytes().to_owned(),
+            txid_str: Txid::all_zeros().to_string(),
+            output_index: 1,
+        });
+        utxo_one.address_type = 4;
+
+        let mut utxo_two = Utxo::default();
+        utxo_two.amount_sat = Amount::from_sat(5_000).to_sat() as i64;
+
+        let utxos = vec![utxo_one, utxo_two];
+
+        let fees = calculate_fee(Some(utxos), 19_000, None);
+
+        assert_eq!(fees.is_err(), true)
+    }
+
+    #[test]
+    #[cfg(feature = "blocking")]
+    fn calculate_fee_with_user_provided_rate_non_async() {
+        let mut utxo_one = Utxo::default();
+        utxo_one.amount_sat = Amount::from_btc(1.0).unwrap().to_sat() as i64;
+
+        let mut utxo_two = Utxo::default();
+        utxo_two.amount_sat = Amount::from_btc(0.5).unwrap().to_sat() as i64;
+
+        let utxos = vec![utxo_one, utxo_two];
+
+        let user_fee_rate = 50;
+
+        let fees = calculate_fee(Some(utxos), 125_000_000, Some(user_fee_rate)).unwrap();
+
+        // Testing the second item in each fee pair of the ProjectedFees struct 
+        // which is just the fee rate not the math of each fee * virtual_bytes
+        assert_eq!(fees.fastest_fee.1, user_fee_rate);
+        assert_eq!(fees.half_hour_fee.1, user_fee_rate);
+        assert_eq!(fees.hour_fee.1, user_fee_rate);
+        assert_eq!(fees.economy_fee.1, user_fee_rate);
+        assert_eq!(fees.minimum_fee.1, user_fee_rate);
+    }
 }