diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 80330de..57e7f62 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -5,5 +5,5 @@ set -e # Cargo syntax checks dirs=("lib" "cli") for dir in ${dirs[@]}; do - (cd $dir; exec cargo fmt; exec cargo clippy -- -D warnings) + (cd $dir; exec cargo fmt; exec cargo clippy --all-targets -- -D warnings) done diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8e20463..71b6deb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,11 +48,11 @@ jobs: - name: Clippy bindings working-directory: lib/bindings - run: cargo clippy -- -D warnings + run: cargo clippy --all-targets -- -D warnings - name: Clippy core working-directory: lib/core - run: cargo clippy -- -D warnings + run: cargo clippy --all-targets -- -D warnings - name: Clippy cli working-directory: cli diff --git a/Makefile b/Makefile index 3f4e85c..aa01ae2 100644 --- a/Makefile +++ b/Makefile @@ -12,10 +12,8 @@ fmt: clippy: cargo-clippy wasm-clippy cargo-clippy: - cd lib/bindings && cargo clippy -- -D warnings - cd lib/bindings && cargo clippy --tests -- -D warnings - cd lib/core && cargo clippy -- -D warnings - cd lib/core && cargo clippy --tests -- -D warnings + cd lib/bindings && cargo clippy --all-targets -- -D warnings + cd lib/core && cargo clippy --all-targets -- -D warnings cd cli && cargo clippy -- -D warnings wasm-clippy: diff --git a/lib/core/Makefile b/lib/core/Makefile index 7e2feeb..afe4186 100644 --- a/lib/core/Makefile +++ b/lib/core/Makefile @@ -18,7 +18,7 @@ test: cargo-test wasm-test regtest-test: cargo-regtest-test wasm-regtest-test cargo-clippy: - cargo clippy -- -D warnings + cargo clippy --all-targets -- -D warnings cargo-test: cargo test @@ -40,7 +40,7 @@ cargo-regtest-test: check-regtest $(REGTEST_PREFIX) cargo test $(REGTEST_TESTS) --features "regtest" wasm-clippy: - $(CLANG_PREFIX) cargo clippy --target=wasm32-unknown-unknown -- -D warnings + $(CLANG_PREFIX) cargo clippy --all-targets --target=wasm32-unknown-unknown -- -D warnings BROWSER ?= firefox diff --git a/lib/core/src/payjoin/pset/tests.rs b/lib/core/src/payjoin/pset/tests.rs index e97d1b1..1942999 100644 --- a/lib/core/src/payjoin/pset/tests.rs +++ b/lib/core/src/payjoin/pset/tests.rs @@ -1,200 +1,191 @@ -#[cfg(test)] -mod tests { - use anyhow::Result; - use bip39::rand::{self, RngCore}; - use lwk_wollet::bitcoin; - use lwk_wollet::elements::address::AddressParams; - use lwk_wollet::elements::confidential::{ - Asset, AssetBlindingFactor, Value, ValueBlindingFactor, +#![cfg(test)] +use anyhow::Result; +use bip39::rand::{self, RngCore}; +use lwk_wollet::bitcoin; +use lwk_wollet::elements::address::AddressParams; +use lwk_wollet::elements::confidential::{Asset, AssetBlindingFactor, Value, ValueBlindingFactor}; +use lwk_wollet::elements::secp256k1_zkp::SecretKey; +use lwk_wollet::elements::{secp256k1_zkp, Address, AssetId, Script, TxOutSecrets, Txid}; +use std::str::FromStr; + +use crate::payjoin::pset::{construct_pset, ConstructPsetRequest, PsetInput, PsetOutput}; + +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +fn create_test_secret_key() -> SecretKey { + let mut rng = rand::thread_rng(); + let mut buf = [0u8; 32]; + rng.fill_bytes(&mut buf); + SecretKey::from_slice(&buf).expect("Expected valid secret key") +} + +fn create_test_input(asset_id: AssetId, sk: &SecretKey) -> PsetInput { + // Create a dummy txid + let txid = + Txid::from_str("0000000000000000000000000000000000000000000000000000000000000001").unwrap(); + + // Create a dummy script pubkey + let script_pub_key = + Script::from_str("76a914000000000000000000000000000000000000000088ac").unwrap(); + + // Create dummy asset and value commitments + let secp = secp256k1_zkp::Secp256k1::new(); + + let asset_bf = AssetBlindingFactor::from_slice(&sk.secret_bytes()).unwrap(); + let asset_gen = + secp256k1_zkp::Generator::new_blinded(&secp, asset_id.into_tag(), asset_bf.into_inner()); + let asset_commitment = Asset::Confidential(asset_gen); + + // Create a Pedersen commitment for the value + let value_bf = ValueBlindingFactor::from_slice(&sk.secret_bytes()).unwrap(); + let value_commit = + secp256k1_zkp::PedersenCommitment::new(&secp, 10000, value_bf.into_inner(), asset_gen); + let value_commitment = Value::Confidential(value_commit); + + // Create dummy txout secrets + let tx_out_sec = TxOutSecrets { + asset: asset_id, + value: 10000, + asset_bf, + value_bf, }; - use lwk_wollet::elements::secp256k1_zkp::SecretKey; - use lwk_wollet::elements::{secp256k1_zkp, Address, AssetId, Script, TxOutSecrets, Txid}; - use std::str::FromStr; - use crate::payjoin::pset::{construct_pset, ConstructPsetRequest, PsetInput, PsetOutput}; - - #[cfg(all(target_family = "wasm", target_os = "unknown"))] - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - fn create_test_secret_key() -> SecretKey { - let mut rng = rand::thread_rng(); - let mut buf = [0u8; 32]; - rng.fill_bytes(&mut buf); - SecretKey::from_slice(&buf).expect("Expected valid secret key") - } - - fn create_test_input(asset_id: AssetId, sk: &SecretKey) -> PsetInput { - // Create a dummy txid - let txid = - Txid::from_str("0000000000000000000000000000000000000000000000000000000000000001") - .unwrap(); - - // Create a dummy script pubkey - let script_pub_key = - Script::from_str("76a914000000000000000000000000000000000000000088ac").unwrap(); - - // Create dummy asset and value commitments - let secp = secp256k1_zkp::Secp256k1::new(); - - let asset_bf = AssetBlindingFactor::from_slice(&sk.secret_bytes()).unwrap(); - let asset_gen = secp256k1_zkp::Generator::new_blinded( - &secp, - asset_id.into_tag(), - asset_bf.into_inner(), - ); - let asset_commitment = Asset::Confidential(asset_gen); - - // Create a Pedersen commitment for the value - let value_bf = ValueBlindingFactor::from_slice(&sk.secret_bytes()).unwrap(); - let value_commit = - secp256k1_zkp::PedersenCommitment::new(&secp, 10000, value_bf.into_inner(), asset_gen); - let value_commitment = Value::Confidential(value_commit); - - // Create dummy txout secrets - let tx_out_sec = TxOutSecrets { - asset: asset_id, - value: 10000, - asset_bf, - value_bf, - }; - - PsetInput { - txid, - vout: 0, - script_pub_key, - asset_commitment, - value_commitment, - tx_out_sec, - } - } - - fn create_test_output(asset_id: AssetId, sk: &SecretKey) -> PsetOutput { - // Create a dummy blinded address - let secp = secp256k1_zkp::Secp256k1::new(); - let blinding_key = - bitcoin::PublicKey::new(secp256k1_zkp::PublicKey::from_secret_key(&secp, sk)); - let address_pk = - bitcoin::PublicKey::new(secp256k1_zkp::PublicKey::from_secret_key(&secp, sk)); - - let address = Address::p2pkh( - &address_pk, - Some(blinding_key.inner), - &AddressParams::LIQUID, - ); - - PsetOutput { - address, - asset_id, - amount: 5000, - } - } - - #[sdk_macros::test_all] - fn test_construct_pset_basic() -> Result<()> { - // Create test data - let asset_id = AssetId::from_slice(&[2; 32]).unwrap(); - let secret_key = create_test_secret_key(); - - let policy_asset = AssetId::from_slice(&[8; 32]).unwrap(); - let inputs = vec![ - create_test_input(asset_id, &secret_key), - create_test_input(asset_id, &secret_key), - ]; - let outputs = vec![create_test_output(asset_id, &secret_key)]; - let network_fee = 1000; - - let request = ConstructPsetRequest { - policy_asset, - inputs, - outputs, - network_fee, - }; - - // Call the function - let pset = construct_pset(request)?; - - // Validate the result - assert_eq!(pset.inputs().len(), 2); - assert_eq!(pset.outputs().len(), 2); // 1 regular output + 1 fee output - - Ok(()) - } - - #[sdk_macros::test_all] - fn test_construct_pset_multiple_outputs() -> Result<()> { - // Create test data - let asset_id = AssetId::from_slice(&[3; 32]).unwrap(); - let secret_key = create_test_secret_key(); - - let policy_asset = AssetId::from_slice(&[8; 32]).unwrap(); - let inputs = vec![create_test_input(asset_id, &secret_key)]; - let outputs = vec![ - create_test_output(asset_id, &secret_key), - create_test_output(asset_id, &secret_key), - create_test_output(asset_id, &secret_key), - ]; - let network_fee = 1000; - - let request = ConstructPsetRequest { - policy_asset, - inputs, - outputs, - network_fee, - }; - - // Call the function - let pset = construct_pset(request)?; - - // Validate the result - assert_eq!(pset.inputs().len(), 1); - assert_eq!(pset.outputs().len(), 4); // 3 regular outputs + 1 fee output - - Ok(()) - } - - #[sdk_macros::test_all] - fn test_construct_pset_empty_inputs() { - // Create test data - let asset_id = AssetId::from_slice(&[4; 32]).unwrap(); - let secret_key = create_test_secret_key(); - - let policy_asset = AssetId::from_slice(&[8; 32]).unwrap(); - let inputs = vec![]; - let outputs = vec![create_test_output(asset_id, &secret_key)]; - let network_fee = 1000; - - let request = ConstructPsetRequest { - policy_asset, - inputs, - outputs, - network_fee, - }; - - // Blinding should fail with empty inputs - let result = construct_pset(request); - assert!(result.is_err()); - } - - #[sdk_macros::test_all] - fn test_construct_pset_empty_outputs() { - // Create test data - let asset_id = AssetId::from_slice(&[5; 32]).unwrap(); - let secret_key = create_test_secret_key(); - - let policy_asset = AssetId::from_slice(&[8; 32]).unwrap(); - let inputs = vec![create_test_input(asset_id, &secret_key)]; - let outputs = vec![]; - let network_fee = 1000; - - let request = ConstructPsetRequest { - policy_asset, - inputs, - outputs, - network_fee, - }; - - // Call the function - let result = construct_pset(request); - assert!(result.is_err()); + PsetInput { + txid, + vout: 0, + script_pub_key, + asset_commitment, + value_commitment, + tx_out_sec, } } + +fn create_test_output(asset_id: AssetId, sk: &SecretKey) -> PsetOutput { + // Create a dummy blinded address + let secp = secp256k1_zkp::Secp256k1::new(); + let blinding_key = + bitcoin::PublicKey::new(secp256k1_zkp::PublicKey::from_secret_key(&secp, sk)); + let address_pk = bitcoin::PublicKey::new(secp256k1_zkp::PublicKey::from_secret_key(&secp, sk)); + + let address = Address::p2pkh( + &address_pk, + Some(blinding_key.inner), + &AddressParams::LIQUID, + ); + + PsetOutput { + address, + asset_id, + amount: 5000, + } +} + +#[sdk_macros::test_all] +fn test_construct_pset_basic() -> Result<()> { + // Create test data + let asset_id = AssetId::from_slice(&[2; 32]).unwrap(); + let secret_key = create_test_secret_key(); + + let policy_asset = AssetId::from_slice(&[8; 32]).unwrap(); + let inputs = vec![ + create_test_input(asset_id, &secret_key), + create_test_input(asset_id, &secret_key), + ]; + let outputs = vec![create_test_output(asset_id, &secret_key)]; + let network_fee = 1000; + + let request = ConstructPsetRequest { + policy_asset, + inputs, + outputs, + network_fee, + }; + + // Call the function + let pset = construct_pset(request)?; + + // Validate the result + assert_eq!(pset.inputs().len(), 2); + assert_eq!(pset.outputs().len(), 2); // 1 regular output + 1 fee output + + Ok(()) +} + +#[sdk_macros::test_all] +fn test_construct_pset_multiple_outputs() -> Result<()> { + // Create test data + let asset_id = AssetId::from_slice(&[3; 32]).unwrap(); + let secret_key = create_test_secret_key(); + + let policy_asset = AssetId::from_slice(&[8; 32]).unwrap(); + let inputs = vec![create_test_input(asset_id, &secret_key)]; + let outputs = vec![ + create_test_output(asset_id, &secret_key), + create_test_output(asset_id, &secret_key), + create_test_output(asset_id, &secret_key), + ]; + let network_fee = 1000; + + let request = ConstructPsetRequest { + policy_asset, + inputs, + outputs, + network_fee, + }; + + // Call the function + let pset = construct_pset(request)?; + + // Validate the result + assert_eq!(pset.inputs().len(), 1); + assert_eq!(pset.outputs().len(), 4); // 3 regular outputs + 1 fee output + + Ok(()) +} + +#[sdk_macros::test_all] +fn test_construct_pset_empty_inputs() { + // Create test data + let asset_id = AssetId::from_slice(&[4; 32]).unwrap(); + let secret_key = create_test_secret_key(); + + let policy_asset = AssetId::from_slice(&[8; 32]).unwrap(); + let inputs = vec![]; + let outputs = vec![create_test_output(asset_id, &secret_key)]; + let network_fee = 1000; + + let request = ConstructPsetRequest { + policy_asset, + inputs, + outputs, + network_fee, + }; + + // Blinding should fail with empty inputs + let result = construct_pset(request); + assert!(result.is_err()); +} + +#[sdk_macros::test_all] +fn test_construct_pset_empty_outputs() { + // Create test data + let asset_id = AssetId::from_slice(&[5; 32]).unwrap(); + let secret_key = create_test_secret_key(); + + let policy_asset = AssetId::from_slice(&[8; 32]).unwrap(); + let inputs = vec![create_test_input(asset_id, &secret_key)]; + let outputs = vec![]; + let network_fee = 1000; + + let request = ConstructPsetRequest { + policy_asset, + inputs, + outputs, + network_fee, + }; + + // Call the function + let result = construct_pset(request); + assert!(result.is_err()); +} diff --git a/lib/core/src/payjoin/utxo_select/tests.rs b/lib/core/src/payjoin/utxo_select/tests.rs index cb751e3..19a8cdb 100644 --- a/lib/core/src/payjoin/utxo_select/tests.rs +++ b/lib/core/src/payjoin/utxo_select/tests.rs @@ -1,388 +1,386 @@ -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; +#![cfg(test)] +use std::collections::BTreeMap; - use lwk_wollet::elements::AssetId; +use lwk_wollet::elements::AssetId; - use crate::payjoin::{ - model::InOut, - utxo_select::{ - utxo_select, utxo_select_basic, utxo_select_best, utxo_select_fixed, - utxo_select_in_range, UtxoSelectRequest, +use crate::payjoin::{ + model::InOut, + utxo_select::{ + utxo_select, utxo_select_basic, utxo_select_best, utxo_select_fixed, utxo_select_in_range, + UtxoSelectRequest, + }, +}; + +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[sdk_macros::test_all] +fn test_utxo_select_basic() { + // Basic case - should select UTXOs in order until target is met + let utxos = vec![100, 200, 300, 400]; + let selected = utxo_select_basic(300, &utxos); + assert_eq!(selected, Some(vec![100, 200])); + + // Exact match with one UTXO + let selected = utxo_select_basic(300, &[300, 400, 500]); + assert_eq!(selected, Some(vec![300])); + + // First UTXO is enough + let selected = utxo_select_basic(50, &[100, 200, 300]); + assert_eq!(selected, Some(vec![100])); + + // Need all UTXOs + let selected = utxo_select_basic(590, &[100, 200, 300]); + assert_eq!(selected, Some(vec![100, 200, 300])); + + // Not enough UTXOs available + let selected = utxo_select_basic(1000, &[100, 200, 300]); + assert_eq!(selected, None); + + // Empty UTXO list + let selected = utxo_select_basic(100, &[]); + assert_eq!(selected, None); + + // Zero target amount + let selected = utxo_select_basic(0, &[100, 200]); + assert_eq!(selected, Some(vec![])); + + // Large values to check for overflow + let large_value = u64::MAX / 3; + let utxos = vec![large_value, large_value, large_value]; + let selected = utxo_select_basic(large_value * 2, &utxos); + assert_eq!(selected, Some(vec![large_value, large_value])); + + // UTXO order matters - should take in original order + let utxos = vec![400, 100, 300, 200]; + let selected = utxo_select_basic(450, &utxos); + assert_eq!(selected, Some(vec![400, 100])); + + // With just-enough UTXOs + let utxos = vec![100, 200, 300, 400]; + let selected = utxo_select_basic(1000, &utxos); + assert_eq!(selected, Some(vec![100, 200, 300, 400])); +} + +#[sdk_macros::test_all] +fn test_utxo_select_fixed() { + let utxos = vec![100, 200, 300, 400]; + + // Should take first two UTXOs (100 + 200 = 300) + let selected = utxo_select_fixed(300, 2, &utxos); + assert_eq!(selected, Some(vec![100, 200])); + + // Not enough with just one UTXO + let selected = utxo_select_fixed(150, 1, &utxos); + assert_eq!(selected, None); + + // Target exceeds available in requested count + let selected = utxo_select_fixed(350, 2, &utxos); + assert_eq!(selected, None); + + // With exactly the required amount + let selected = utxo_select_fixed(300, 1, &[300]); + assert_eq!(selected, Some(vec![300])); + + // With empty utxos + let selected = utxo_select_fixed(100, 1, &[]); + assert_eq!(selected, None); + + // With zero target value + let selected = utxo_select_fixed(0, 2, &utxos); + assert_eq!(selected, Some(vec![100, 200])); + + // With zero target count + let selected = utxo_select_fixed(100, 0, &utxos); + assert_eq!(selected, None); + + // With more UTXOs than requested count but still not enough value + let selected = utxo_select_fixed(1000, 3, &utxos); + assert_eq!(selected, None); + + // With exactly enough UTXOs to meet the target + let selected = utxo_select_fixed(600, 3, &utxos); + assert_eq!(selected, Some(vec![100, 200, 300])); + + // With large values to test for potential overflow issues + let large_value = u64::MAX / 2; + let utxos = vec![large_value, large_value / 2]; + let selected = utxo_select_fixed(large_value, 1, &utxos); + assert_eq!(selected, Some(vec![large_value])); +} + +#[sdk_macros::test_all] +fn test_utxo_select_best() { + let utxos = vec![100, 200, 300, 400]; + + // Should find optimal solution + let selected = utxo_select_best(300, &utxos); + assert_eq!(selected, Some(vec![300])); + + // Should fallback to basic selection as no exact utxo set can be found + let selected: Option> = utxo_select_best(450, &utxos); + assert!(selected.is_some()); + assert_eq!(selected.unwrap().iter().sum::(), 600); + + // Should use all UTXOs as fallback when needed + let selected = utxo_select_best(950, &utxos); + assert_eq!(selected, Some(vec![100, 200, 300, 400])); +} + +#[sdk_macros::test_all] +fn test_utxo_select_in_range() { + let utxos = vec![50, 100, 200, 300, 400]; + + // Exact match + let selected = utxo_select_in_range(300, 0, 0, &utxos); + assert_eq!(selected, Some(vec![300])); + + // Within range + let selected = utxo_select_in_range(350, 50, 0, &utxos); + assert_eq!(selected, Some(vec![400])); + + // Multiple UTXOs needed + let selected = utxo_select_in_range(350, 0, 0, &utxos); + assert_eq!(selected, Some(vec![300, 50])); + + // With target count + let selected = utxo_select_in_range(250, 0, 2, &utxos); + assert_eq!(selected, Some(vec![200, 50])); +} + +#[sdk_macros::test_all] +fn test_utxo_select_success() { + let policy_asset = AssetId::from_slice(&[1; 32]).unwrap(); + let fee_asset = AssetId::from_slice(&[2; 32]).unwrap(); + + // Create wallet UTXOs with both policy and fee assets + let wallet_utxos = vec![ + InOut { + asset_id: policy_asset, + value: 100000000, }, + InOut { + asset_id: policy_asset, + value: 200000000, + }, + InOut { + asset_id: fee_asset, + value: 50000000, + }, + InOut { + asset_id: fee_asset, + value: 80000000, + }, + ]; + + // Create server UTXOs (only policy asset) + let server_utxos = vec![ + InOut { + asset_id: policy_asset, + value: 150000000, + }, + InOut { + asset_id: policy_asset, + value: 250000000, + }, + ]; + + // User outputs (both assets) + let user_outputs = vec![ + InOut { + asset_id: policy_asset, + value: 150000000, + }, + InOut { + asset_id: fee_asset, + value: 20000000, + }, + ]; + + let req = UtxoSelectRequest { + policy_asset, + fee_asset, + price: 84896.5, + fixed_fee: 4000000, + wallet_utxos, + server_utxos, + user_outputs, }; - #[cfg(all(target_family = "wasm", target_os = "unknown"))] - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + let result = utxo_select(req); + assert!(result.is_ok()); - #[sdk_macros::test_all] - fn test_utxo_select_basic() { - // Basic case - should select UTXOs in order until target is met - let utxos = vec![100, 200, 300, 400]; - let selected = utxo_select_basic(300, &utxos); - assert_eq!(selected, Some(vec![100, 200])); + let selection = result.unwrap(); - // Exact match with one UTXO - let selected = utxo_select_basic(300, &[300, 400, 500]); - assert_eq!(selected, Some(vec![300])); + // Verify network fee is covered by server inputs + assert!(selection.network_fee.value > 0); + assert_eq!(selection.network_fee.asset_id, policy_asset); - // First UTXO is enough - let selected = utxo_select_basic(50, &[100, 200, 300]); - assert_eq!(selected, Some(vec![100])); + // Verify server fee is in fee_asset and reasonable + assert!(selection.server_fee.value >= 100); // at least fixed fee + assert_eq!(selection.server_fee.asset_id, fee_asset); - // Need all UTXOs - let selected = utxo_select_basic(590, &[100, 200, 300]); - assert_eq!(selected, Some(vec![100, 200, 300])); + // Verify all user outputs are present + assert_eq!(selection.user_outputs.len(), 2); - // Not enough UTXOs available - let selected = utxo_select_basic(1000, &[100, 200, 300]); - assert_eq!(selected, None); + // Check input/output balance + let mut input_sum_by_asset = BTreeMap::::new(); + let mut output_sum_by_asset = BTreeMap::::new(); - // Empty UTXO list - let selected = utxo_select_basic(100, &[]); - assert_eq!(selected, None); - - // Zero target amount - let selected = utxo_select_basic(0, &[100, 200]); - assert_eq!(selected, Some(vec![])); - - // Large values to check for overflow - let large_value = u64::MAX / 3; - let utxos = vec![large_value, large_value, large_value]; - let selected = utxo_select_basic(large_value * 2, &utxos); - assert_eq!(selected, Some(vec![large_value, large_value])); - - // UTXO order matters - should take in original order - let utxos = vec![400, 100, 300, 200]; - let selected = utxo_select_basic(450, &utxos); - assert_eq!(selected, Some(vec![400, 100])); - - // With just-enough UTXOs - let utxos = vec![100, 200, 300, 400]; - let selected = utxo_select_basic(1000, &utxos); - assert_eq!(selected, Some(vec![100, 200, 300, 400])); + // Sum all inputs + for input in selection + .user_inputs + .iter() + .chain(selection.client_inputs.iter()) + .chain(selection.server_inputs.iter()) + { + *input_sum_by_asset.entry(input.asset_id).or_default() += input.value; } - #[sdk_macros::test_all] - fn test_utxo_select_fixed() { - let utxos = vec![100, 200, 300, 400]; - - // Should take first two UTXOs (100 + 200 = 300) - let selected = utxo_select_fixed(300, 2, &utxos); - assert_eq!(selected, Some(vec![100, 200])); - - // Not enough with just one UTXO - let selected = utxo_select_fixed(150, 1, &utxos); - assert_eq!(selected, None); - - // Target exceeds available in requested count - let selected = utxo_select_fixed(350, 2, &utxos); - assert_eq!(selected, None); - - // With exactly the required amount - let selected = utxo_select_fixed(300, 1, &[300]); - assert_eq!(selected, Some(vec![300])); - - // With empty utxos - let selected = utxo_select_fixed(100, 1, &[]); - assert_eq!(selected, None); - - // With zero target value - let selected = utxo_select_fixed(0, 2, &utxos); - assert_eq!(selected, Some(vec![100, 200])); - - // With zero target count - let selected = utxo_select_fixed(100, 0, &utxos); - assert_eq!(selected, None); - - // With more UTXOs than requested count but still not enough value - let selected = utxo_select_fixed(1000, 3, &utxos); - assert_eq!(selected, None); - - // With exactly enough UTXOs to meet the target - let selected = utxo_select_fixed(600, 3, &utxos); - assert_eq!(selected, Some(vec![100, 200, 300])); - - // With large values to test for potential overflow issues - let large_value = u64::MAX / 2; - let utxos = vec![large_value, large_value / 2]; - let selected = utxo_select_fixed(large_value, 1, &utxos); - assert_eq!(selected, Some(vec![large_value])); + // Sum all outputs + for output in selection + .user_outputs + .iter() + .chain(selection.change_outputs.iter()) + .chain(std::iter::once(&selection.server_fee)) + .chain(selection.server_change.iter()) + .chain(selection.fee_change.iter()) + .chain(std::iter::once(&selection.network_fee)) + { + *output_sum_by_asset.entry(output.asset_id).or_default() += output.value; } - #[sdk_macros::test_all] - fn test_utxo_select_best() { - let utxos = vec![100, 200, 300, 400]; + // Input and output sums should match for each asset + assert_eq!(input_sum_by_asset, output_sum_by_asset); +} - // Should find optimal solution - let selected = utxo_select_best(300, &utxos); - assert_eq!(selected, Some(vec![300])); +#[sdk_macros::test_all] +fn test_utxo_select_error_cases() { + let policy_asset = AssetId::from_slice(&[1; 32]).unwrap(); + let fee_asset = AssetId::from_slice(&[2; 32]).unwrap(); - // Should fallback to basic selection as no exact utxo set can be found - let selected: Option> = utxo_select_best(450, &utxos); - assert!(selected.is_some()); - assert_eq!(selected.unwrap().iter().sum::(), 600); - - // Should use all UTXOs as fallback when needed - let selected = utxo_select_best(950, &utxos); - assert_eq!(selected, Some(vec![100, 200, 300, 400])); - } - - #[sdk_macros::test_all] - fn test_utxo_select_in_range() { - let utxos = vec![50, 100, 200, 300, 400]; - - // Exact match - let selected = utxo_select_in_range(300, 0, 0, &utxos); - assert_eq!(selected, Some(vec![300])); - - // Within range - let selected = utxo_select_in_range(350, 50, 0, &utxos); - assert_eq!(selected, Some(vec![400])); - - // Multiple UTXOs needed - let selected = utxo_select_in_range(350, 0, 0, &utxos); - assert_eq!(selected, Some(vec![300, 50])); - - // With target count - let selected = utxo_select_in_range(250, 0, 2, &utxos); - assert_eq!(selected, Some(vec![200, 50])); - } - - #[sdk_macros::test_all] - fn test_utxo_select_success() { - let policy_asset = AssetId::from_slice(&[1; 32]).unwrap(); - let fee_asset = AssetId::from_slice(&[2; 32]).unwrap(); - - // Create wallet UTXOs with both policy and fee assets - let wallet_utxos = vec![ - InOut { - asset_id: policy_asset, - value: 100000000, - }, - InOut { - asset_id: policy_asset, - value: 200000000, - }, - InOut { - asset_id: fee_asset, - value: 50000000, - }, - InOut { - asset_id: fee_asset, - value: 80000000, - }, - ]; - - // Create server UTXOs (only policy asset) - let server_utxos = vec![ - InOut { - asset_id: policy_asset, - value: 150000000, - }, - InOut { - asset_id: policy_asset, - value: 250000000, - }, - ]; - - // User outputs (both assets) - let user_outputs = vec![ - InOut { - asset_id: policy_asset, - value: 150000000, - }, - InOut { - asset_id: fee_asset, - value: 20000000, - }, - ]; - - let req = UtxoSelectRequest { - policy_asset, - fee_asset, - price: 84896.5, - fixed_fee: 4000000, - wallet_utxos, - server_utxos, - user_outputs, - }; - - let result = utxo_select(req); - assert!(result.is_ok()); - - let selection = result.unwrap(); - - // Verify network fee is covered by server inputs - assert!(selection.network_fee.value > 0); - assert_eq!(selection.network_fee.asset_id, policy_asset); - - // Verify server fee is in fee_asset and reasonable - assert!(selection.server_fee.value >= 100); // at least fixed fee - assert_eq!(selection.server_fee.asset_id, fee_asset); - - // Verify all user outputs are present - assert_eq!(selection.user_outputs.len(), 2); - - // Check input/output balance - let mut input_sum_by_asset = BTreeMap::::new(); - let mut output_sum_by_asset = BTreeMap::::new(); - - // Sum all inputs - for input in selection - .user_inputs - .iter() - .chain(selection.client_inputs.iter()) - .chain(selection.server_inputs.iter()) - { - *input_sum_by_asset.entry(input.asset_id).or_default() += input.value; - } - - // Sum all outputs - for output in selection - .user_outputs - .iter() - .chain(selection.change_outputs.iter()) - .chain(std::iter::once(&selection.server_fee)) - .chain(selection.server_change.iter()) - .chain(selection.fee_change.iter()) - .chain(std::iter::once(&selection.network_fee)) - { - *output_sum_by_asset.entry(output.asset_id).or_default() += output.value; - } - - // Input and output sums should match for each asset - assert_eq!(input_sum_by_asset, output_sum_by_asset); - } - - #[sdk_macros::test_all] - fn test_utxo_select_error_cases() { - let policy_asset = AssetId::from_slice(&[1; 32]).unwrap(); - let fee_asset = AssetId::from_slice(&[2; 32]).unwrap(); - - // Base valid request - let valid_req = UtxoSelectRequest { - policy_asset, - fee_asset, - price: 84896.5, - fixed_fee: 4000000, - wallet_utxos: vec![ - InOut { - asset_id: policy_asset, - value: 1000, - }, - InOut { - asset_id: fee_asset, - value: 500, - }, - ], - server_utxos: vec![InOut { - asset_id: policy_asset, - value: 1500, - }], - user_outputs: vec![InOut { - asset_id: policy_asset, - value: 500, - }], - }; - - // Same asset for policy and fee - should error - let mut bad_req = valid_req.clone(); - bad_req.fee_asset = policy_asset; - assert!(utxo_select(bad_req).is_err()); - - // Zero price - should error - let mut bad_req = valid_req.clone(); - bad_req.price = 0.0; - assert!(utxo_select(bad_req).is_err()); - - // Zero fixed fee - should error - let mut bad_req = valid_req.clone(); - bad_req.fixed_fee = 0; - assert!(utxo_select(bad_req).is_err()); - - // Invalid server UTXO asset - should error - let mut bad_req = valid_req.clone(); - bad_req.server_utxos = vec![InOut { - asset_id: fee_asset, - value: 1500, - }]; - assert!(utxo_select(bad_req).is_err()); - - // Insufficient fee assets - should error - let mut bad_req = valid_req.clone(); - bad_req.wallet_utxos = vec![ + // Base valid request + let valid_req = UtxoSelectRequest { + policy_asset, + fee_asset, + price: 84896.5, + fixed_fee: 4000000, + wallet_utxos: vec![ InOut { asset_id: policy_asset, value: 1000, }, InOut { asset_id: fee_asset, - value: 10, - }, // Too small - ]; - let result = utxo_select(bad_req); - assert!(result.is_err()); - } - - #[sdk_macros::test_all] - fn test_utxo_select_with_change() { - let policy_asset = AssetId::from_slice(&[1; 32]).unwrap(); - let fee_asset = AssetId::from_slice(&[2; 32]).unwrap(); - - // Create a scenario where change is needed - let wallet_utxos = vec![ - InOut { - asset_id: policy_asset, - value: 50000000, + value: 500, }, - InOut { - asset_id: fee_asset, - value: 20000000, - }, - ]; - - let server_utxos = vec![ - InOut { - asset_id: policy_asset, - value: 10000000, - }, - InOut { - asset_id: policy_asset, - value: 20000000, - }, - ]; - - let user_outputs = vec![InOut { + ], + server_utxos: vec![InOut { asset_id: policy_asset, - value: 30000000, - }]; + value: 1500, + }], + user_outputs: vec![InOut { + asset_id: policy_asset, + value: 500, + }], + }; - let req = UtxoSelectRequest { - policy_asset, - fee_asset, - price: 84896.5, - fixed_fee: 4000000, - wallet_utxos, - server_utxos, - user_outputs, - }; + // Same asset for policy and fee - should error + let mut bad_req = valid_req.clone(); + bad_req.fee_asset = policy_asset; + assert!(utxo_select(bad_req).is_err()); - let result = utxo_select(req); - assert!(result.is_ok()); + // Zero price - should error + let mut bad_req = valid_req.clone(); + bad_req.price = 0.0; + assert!(utxo_select(bad_req).is_err()); - let selection = result.unwrap(); + // Zero fixed fee - should error + let mut bad_req = valid_req.clone(); + bad_req.fixed_fee = 0; + assert!(utxo_select(bad_req).is_err()); - // Either policy asset change or fee asset change should exist - assert!(selection.fee_change.is_some() || !selection.change_outputs.is_empty()); + // Invalid server UTXO asset - should error + let mut bad_req = valid_req.clone(); + bad_req.server_utxos = vec![InOut { + asset_id: fee_asset, + value: 1500, + }]; + assert!(utxo_select(bad_req).is_err()); - // Verify change amounts are reasonable - if let Some(fee_change) = &selection.fee_change { - assert_eq!(fee_change.asset_id, fee_asset); - assert!(fee_change.value > 0); - } - - // Check that we're not wasting fees unnecessarily - assert!(selection.cost <= selection.server_fee.value); - } + // Insufficient fee assets - should error + let mut bad_req = valid_req.clone(); + bad_req.wallet_utxos = vec![ + InOut { + asset_id: policy_asset, + value: 1000, + }, + InOut { + asset_id: fee_asset, + value: 10, + }, // Too small + ]; + let result = utxo_select(bad_req); + assert!(result.is_err()); +} + +#[sdk_macros::test_all] +fn test_utxo_select_with_change() { + let policy_asset = AssetId::from_slice(&[1; 32]).unwrap(); + let fee_asset = AssetId::from_slice(&[2; 32]).unwrap(); + + // Create a scenario where change is needed + let wallet_utxos = vec![ + InOut { + asset_id: policy_asset, + value: 50000000, + }, + InOut { + asset_id: fee_asset, + value: 20000000, + }, + ]; + + let server_utxos = vec![ + InOut { + asset_id: policy_asset, + value: 10000000, + }, + InOut { + asset_id: policy_asset, + value: 20000000, + }, + ]; + + let user_outputs = vec![InOut { + asset_id: policy_asset, + value: 30000000, + }]; + + let req = UtxoSelectRequest { + policy_asset, + fee_asset, + price: 84896.5, + fixed_fee: 4000000, + wallet_utxos, + server_utxos, + user_outputs, + }; + + let result = utxo_select(req); + assert!(result.is_ok()); + + let selection = result.unwrap(); + + // Either policy asset change or fee asset change should exist + assert!(selection.fee_change.is_some() || !selection.change_outputs.is_empty()); + + // Verify change amounts are reasonable + if let Some(fee_change) = &selection.fee_change { + assert_eq!(fee_change.asset_id, fee_asset); + assert!(fee_change.value > 0); + } + + // Check that we're not wasting fees unnecessarily + assert!(selection.cost <= selection.server_fee.value); } diff --git a/lib/core/src/test_utils/chain.rs b/lib/core/src/test_utils/chain.rs index d9b9eff..822a26e 100644 --- a/lib/core/src/test_utils/chain.rs +++ b/lib/core/src/test_utils/chain.rs @@ -68,7 +68,7 @@ impl LiquidChainService for MockLiquidChainService { _script: &ElementsScript, _retries: u64, ) -> Result> { - Ok(self.get_history().into_iter().map(Into::into).collect()) + Ok(self.get_history().into_iter().collect()) } async fn get_script_history(&self, _script: &ElementsScript) -> Result> { @@ -156,14 +156,7 @@ impl BitcoinChainService for MockBitcoinChainService { _script: &Script, _retries: u64, ) -> Result> { - Ok(self - .history - .lock() - .unwrap() - .clone() - .into_iter() - .map(Into::into) - .collect()) + Ok(self.history.lock().unwrap().clone().into_iter().collect()) } async fn get_script_history(&self, _scripts: &Script) -> Result> { diff --git a/lib/wasm/Makefile b/lib/wasm/Makefile index 9841747..b584906 100644 --- a/lib/wasm/Makefile +++ b/lib/wasm/Makefile @@ -9,7 +9,7 @@ init: rustup target add wasm32-unknown-unknown clippy: - $(CLANG_PREFIX) cargo clippy --target=wasm32-unknown-unknown -- -D warnings + $(CLANG_PREFIX) cargo clippy --all-targets --target=wasm32-unknown-unknown -- -D warnings build: build-bundle build-deno build-node build-web