Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reqwest blocking feature #5

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
259 changes: 251 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ pub struct ProjectedFees {
pub minimum_fee: (u64, u64),
}

pub async fn calculate_fee(utxos: Option<Vec<Utxo>>, amount: u64) -> Result<ProjectedFees, FestivusError> {
// 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();
let mut rand = rand::thread_rng();
Expand Down Expand Up @@ -87,13 +89,101 @@ 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),
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),
})
}

// 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),
Expand Down Expand Up @@ -148,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;
Expand All @@ -163,12 +254,13 @@ 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]
#[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;
Expand All @@ -184,19 +276,21 @@ 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]
#[cfg(not(feature = "blocking"))]
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)
}

#[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;
Expand All @@ -213,12 +307,13 @@ 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)
}

#[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;
Expand All @@ -234,8 +329,156 @@ 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]
#[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;

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

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