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); + } }