diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3752e7..bd8ddd2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: 1.89.0 - target: wasm32-unknown-unknown + target: wasm32v1-none override: true # rustfmt + clippy are required by the Check format and Run # clippy steps below; actions-rs/toolchain@v1 only installs diff --git a/contracts/ephemeral_account/src/lib.rs b/contracts/ephemeral_account/src/lib.rs index 60e3056..ce6922d 100644 --- a/contracts/ephemeral_account/src/lib.rs +++ b/contracts/ephemeral_account/src/lib.rs @@ -3,7 +3,6 @@ mod errors; mod events; mod storage; -#[cfg(test)] mod test; use soroban_sdk::{contract, contractimpl, xdr::ToXdr, Address, BytesN, Env, Vec}; diff --git a/contracts/ephemeral_account/src/test.rs b/contracts/ephemeral_account/src/test.rs index 0708fcd..7ef06de 100644 --- a/contracts/ephemeral_account/src/test.rs +++ b/contracts/ephemeral_account/src/test.rs @@ -1,395 +1,382 @@ -#[cfg(test)] -mod test { - extern crate std; - - use std::println; - - use crate::{ - storage, AccountStatus, EphemeralAccountContract, EphemeralAccountContractClient, Error, - ReserveReclaimed, - }; - use soroban_sdk::{ - testutils::{Address as _, Events as _, Ledger as _}, - Address, BytesN, Env, IntoVal, InvokeError, TryFromVal, - }; - - const BASE_RESERVE_STROOPS: i128 = 1_000_000_000; - - fn latest_reserve_event(client: &EphemeralAccountContractClient) -> ReserveReclaimed { - client - .get_last_reserve_event() - .expect("reserve event was not emitted") - } - - #[test] - fn test_initialize() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); - let controller = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1000; - - client.initialize( - &creator, - &expiry_ledger, - &recovery, - &controller, - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - - assert_eq!(client.get_status(), AccountStatus::Active); - assert!(!client.is_expired()); - assert_eq!(client.get_reserve_remaining(), BASE_RESERVE_STROOPS); - assert_eq!(client.get_reserve_available(), BASE_RESERVE_STROOPS); - assert!(!client.is_reserve_reclaimed()); - } - - #[test] - fn test_record_payment() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); - let controller = Address::generate(&env); - let asset = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1000; - - client.initialize( - &creator, - &expiry_ledger, - &recovery, - &controller, - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - client.record_payment(&100, &asset); - - assert_eq!(client.get_status(), AccountStatus::PaymentReceived); - } - - #[test] - fn test_multiple_payments() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); - let controller = Address::generate(&env); - let asset1 = Address::generate(&env); - let asset2 = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1000; - - client.initialize( - &creator, - &expiry_ledger, - &recovery, - &controller, - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - - client.record_payment(&100, &asset1); - let info = client.get_info(); - assert_eq!(info.payment_count, 1); - - client.record_payment(&50, &asset2); - let info = client.get_info(); - assert_eq!(info.payment_count, 2); - - assert_eq!(client.get_status(), AccountStatus::PaymentReceived); - } - - #[test] - fn test_sweep_single_asset() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); - let controller = Address::generate(&env); - let asset = Address::generate(&env); - let destination = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1000; - - client.initialize( - &creator, - &expiry_ledger, - &recovery, - &controller, - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - client.record_payment(&100, &asset); - - - client.sweep_claim(&destination); - - assert_eq!(client.get_status(), AccountStatus::Swept); - assert_eq!(client.get_reserve_remaining(), 0); - assert!(client.is_reserve_reclaimed()); - - let reserve_event = latest_reserve_event(&client); - assert_eq!(reserve_event.destination, destination); - assert_eq!(reserve_event.amount, BASE_RESERVE_STROOPS); - assert_eq!(reserve_event.remaining_reserve, 0); - assert!(reserve_event.fully_reclaimed); - assert_eq!(reserve_event.sweep_id, env.ledger().sequence() as u64); - assert_eq!(client.get_reserve_reclaim_event_count(), 1); - } - - #[test] - fn test_duplicate_asset_returns_expected_error_code() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); - let controller = Address::generate(&env); - let asset = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1000; - - client.initialize( - &creator, - &expiry_ledger, - &recovery, - &controller, - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - client.record_payment(&100, &asset); - let result = client.try_record_payment(&50, &asset); - - assert!(matches!(result, Err(Ok(Error::DuplicateAsset)))); - } - - #[test] - fn test_too_many_assets_returns_expected_error_code() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); - let controller = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1000; - - client.initialize( - &creator, - &expiry_ledger, - &recovery, - &controller, - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - - for i in 0..10 { - let asset = Address::generate(&env); - client.record_payment(&(100 + i as i128), &asset); - } - - let asset = Address::generate(&env); - let result = client.try_record_payment(&200, &asset); - - assert!(matches!(result, Err(Ok(Error::TooManyPayments)))); - } - - #[test] - fn test_record_payment_returns_not_initialized_error() { - let env = Env::default(); - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let asset = Address::generate(&env); - let result = client.try_record_payment(&100, &asset); - - assert!(matches!(result, Err(Ok(Error::NotInitialized)))); - } - - #[test] - fn test_record_payment_returns_invalid_amount_error() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); - let asset = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1000; - - client.initialize( - &creator, - &expiry_ledger, - &recovery, - &Address::generate(&env), - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - - assert!(matches!(result, Err(Ok(Error::InvalidExpiry)))); - } - - #[test] - fn test_expire_returns_not_expired_error() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1000; - - client.initialize( - &creator, - &expiry_ledger, - &recovery, - &Address::generate(&env), - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - - let result = client.try_sweep_claim(&destination); - - assert!(matches!(result, Err(Ok(Error::NoPaymentReceived)))); - } - - #[test] - fn test_sweep_returns_account_expired_error() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); +#![cfg(test)] +extern crate std; + +use crate::{ + AccountStatus, EphemeralAccountContract, EphemeralAccountContractClient, Error, + ReserveReclaimed, +}; +use soroban_sdk::{ + testutils::{Address as _, Ledger as _}, + Address, BytesN, Env, +}; + +const BASE_RESERVE_STROOPS: i128 = 1_000_000_000; + +/// Build a test `Env` with mock auth enabled and the standalone network +/// passphrase, matching what `initialize` enforces via `require_network`. +fn test_env() -> Env { + let env = Env::default(); + env.mock_all_auths(); + env.ledger() + .set_network_id(bridgelet_shared::passphrase::standalone_network_id(&env)); + env +} + +fn latest_reserve_event(client: &EphemeralAccountContractClient) -> ReserveReclaimed { + client + .get_last_reserve_event() + .expect("reserve event was not emitted") +} + +#[test] +fn test_initialize() { + let env = test_env(); + + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let controller = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1000; + + client.initialize( + &creator, + &expiry_ledger, + &recovery, + &controller, + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + + assert_eq!(client.get_status(), AccountStatus::Active); + assert!(!client.is_expired()); + assert_eq!(client.get_reserve_remaining(), BASE_RESERVE_STROOPS); + assert_eq!(client.get_reserve_available(), BASE_RESERVE_STROOPS); + assert!(!client.is_reserve_reclaimed()); +} + +#[test] +fn test_record_payment() { + let env = test_env(); + + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let controller = Address::generate(&env); + let asset = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1000; + + client.initialize( + &creator, + &expiry_ledger, + &recovery, + &controller, + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + client.record_payment(&100, &asset); + + assert_eq!(client.get_status(), AccountStatus::PaymentReceived); +} + +#[test] +fn test_multiple_payments() { + let env = test_env(); + + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let controller = Address::generate(&env); + let asset1 = Address::generate(&env); + let asset2 = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1000; + + client.initialize( + &creator, + &expiry_ledger, + &recovery, + &controller, + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + + client.record_payment(&100, &asset1); + let info = client.get_info(); + assert_eq!(info.payment_count, 1); + + client.record_payment(&50, &asset2); + let info = client.get_info(); + assert_eq!(info.payment_count, 2); + + assert_eq!(client.get_status(), AccountStatus::PaymentReceived); +} + +#[test] +fn test_sweep_single_asset() { + let env = test_env(); + + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let controller = Address::generate(&env); + let asset = Address::generate(&env); + let destination = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1000; + + client.initialize( + &creator, + &expiry_ledger, + &recovery, + &controller, + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + client.record_payment(&100, &asset); + + client.sweep_claim(&destination); + + assert_eq!(client.get_status(), AccountStatus::Swept); + assert_eq!(client.get_reserve_remaining(), 0); + assert!(client.is_reserve_reclaimed()); + + let reserve_event = latest_reserve_event(&client); + assert_eq!(reserve_event.destination, destination); + assert_eq!(reserve_event.amount, BASE_RESERVE_STROOPS); + assert_eq!(reserve_event.remaining_reserve, 0); + assert!(reserve_event.fully_reclaimed); + assert_eq!(reserve_event.sweep_id, env.ledger().sequence() as u64); + assert_eq!(client.get_reserve_reclaim_event_count(), 1); +} + +#[test] +fn test_duplicate_asset_returns_expected_error_code() { + let env = test_env(); + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let controller = Address::generate(&env); + let asset = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1000; + + client.initialize( + &creator, + &expiry_ledger, + &recovery, + &controller, + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + client.record_payment(&100, &asset); + let result = client.try_record_payment(&50, &asset); + + assert!(matches!(result, Err(Ok(Error::DuplicateAsset)))); +} + +#[test] +fn test_too_many_assets_returns_expected_error_code() { + let env = test_env(); + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let controller = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1000; + + client.initialize( + &creator, + &expiry_ledger, + &recovery, + &controller, + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + + for i in 0..10 { let asset = Address::generate(&env); - let destination = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1; - - client.initialize( - &creator, - &expiry_ledger, - &recovery, - &Address::generate(&env), - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - client.record_payment(&100, &asset); - - - client.sweep_claim(&destination); - let replay_result = client.try_sweep_claim(&destination); - - assert!(matches!(replay_result, Err(Ok(Error::AlreadySwept)))); + client.record_payment(&(100 + i as i128), &asset); } - #[test] - fn test_sweep_accepts_placeholder_authorization_and_succeeds() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); - let asset = Address::generate(&env); - let destination = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1000; - - client.initialize( - &creator, - &expiry_ledger, - &recovery, - &Address::generate(&env), - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - client.initialize( - &creator, - &(expiry_ledger + 1), - &recovery, - &Address::generate(&env), - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - client.record_payment(&100, &asset); - client.record_payment(&50, &asset); - } - - #[test] - #[should_panic(expected = "Error(Contract, #1010)")] - fn test_sweep_after_expiry_is_rejected() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); - let asset = Address::generate(&env); - let destination = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1; - - client.initialize( - &creator, - &expiry_ledger, - &recovery, - &Address::generate(&env), - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - client.record_payment(&100, &asset); - - env.ledger().set_sequence_number(expiry_ledger); - client.expire(); - - let info = client.get_info(); - assert_eq!(info.status, AccountStatus::Expired); - assert_eq!(info.swept_to, Some(recovery)); - assert_eq!(client.get_reserve_remaining(), 0); - assert!(client.is_reserve_reclaimed()); - assert_eq!(client.get_reserve_reclaim_event_count(), 1); - } - - #[test] - fn test_initialize_requires_creator_authorization() { - let env = Env::default(); - - let contract_id = env.register(EphemeralAccountContract, ()); - let client = EphemeralAccountContractClient::new(&env, &contract_id); - - let creator = Address::generate(&env); - let recovery = Address::generate(&env); - let expiry_ledger = env.ledger().sequence() + 1000; - - let result = client.try_initialize( - &creator, - &expiry_ledger, - &recovery, - &Address::generate(&env), - &BytesN::from_array(&env, &[0u8; 32]), - &Address::generate(&env), - ); - - assert!(result.is_err()); + let asset = Address::generate(&env); + let result = client.try_record_payment(&200, &asset); + + assert!(matches!(result, Err(Ok(Error::TooManyPayments)))); +} + +#[test] +fn test_record_payment_returns_not_initialized_error() { + let env = Env::default(); + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let asset = Address::generate(&env); + let result = client.try_record_payment(&100, &asset); + + assert!(matches!(result, Err(Ok(Error::NotInitialized)))); +} + +#[test] +fn test_record_payment_returns_invalid_amount_error() { + let env = test_env(); + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let asset = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1000; + + client.initialize( + &creator, + &expiry_ledger, + &recovery, + &Address::generate(&env), + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + let result = client.try_record_payment(&0, &asset); + + assert!(matches!(result, Err(Ok(Error::InvalidAmount)))); +} + +#[test] +fn test_expire_returns_not_expired_error() { + let env = test_env(); + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1000; + + client.initialize( + &creator, + &expiry_ledger, + &recovery, + &Address::generate(&env), + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + + let result = client.try_expire(); + + assert!(matches!(result, Err(Ok(Error::NotExpired)))); +} + +#[test] +fn test_sweep_returns_already_swept_error() { + let env = test_env(); + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let asset = Address::generate(&env); + let destination = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1; + + client.initialize( + &creator, + &expiry_ledger, + &recovery, + &Address::generate(&env), + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + client.record_payment(&100, &asset); + + client.sweep_claim(&destination); + let replay_result = client.try_sweep_claim(&destination); + + assert!(matches!(replay_result, Err(Ok(Error::AlreadySwept)))); +} + +#[test] +fn test_sweep_claim_authorized_controller_succeeds() { + let env = test_env(); + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let asset = Address::generate(&env); + let destination = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1000; + + client.initialize( + &creator, + &expiry_ledger, + &recovery, + &Address::generate(&env), + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + client.record_payment(&100, &asset); + + client.sweep_claim(&destination); + + let info = client.get_info(); + assert_eq!(info.status, AccountStatus::Swept); + assert_eq!(info.swept_to, Some(destination)); + assert!(client.is_reserve_reclaimed()); +} + +#[test] +#[should_panic(expected = "Error(Contract, #1010)")] +fn test_sweep_after_expiry_is_rejected() { + let env = test_env(); + + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let asset = Address::generate(&env); + let destination = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1; + + client.initialize( + &creator, + &expiry_ledger, + &recovery, + &Address::generate(&env), + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + client.record_payment(&100, &asset); + + env.ledger().set_sequence_number(expiry_ledger); + client.sweep_claim(&destination); +} + +#[test] +fn test_initialize_requires_creator_authorization() { + let env = Env::default(); + + let contract_id = env.register(EphemeralAccountContract, ()); + let client = EphemeralAccountContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let recovery = Address::generate(&env); + let expiry_ledger = env.ledger().sequence() + 1000; + + let result = client.try_initialize( + &creator, + &expiry_ledger, + &recovery, + &Address::generate(&env), + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + + assert!(result.is_err()); +} diff --git a/contracts/ephemeral_account/tests/property_tests.rs b/contracts/ephemeral_account/tests/property_tests.rs index 2a7e778..7814744 100644 --- a/contracts/ephemeral_account/tests/property_tests.rs +++ b/contracts/ephemeral_account/tests/property_tests.rs @@ -27,6 +27,16 @@ use soroban_sdk::{ Address, BytesN, Env, }; +/// Build a test `Env` with mock auth enabled and the standalone network +/// passphrase, matching what `initialize` enforces via `require_network`. +fn test_env() -> Env { + let env = Env::default(); + env.mock_all_auths(); + env.ledger() + .set_network_id(bridgelet_shared::passphrase::standalone_network_id(&env)); + env +} + proptest! { #![proptest_config(ProptestConfig { cases: 48, failure_persistence: None, ..ProptestConfig::default() })] @@ -36,8 +46,7 @@ proptest! { fn amount_never_negative_after_sweep( amounts in prop::collection::vec(1i128..=1_000_000_000_000i128, 1..=10) ) { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let contract_id = env.register(EphemeralAccountContract, ()); let client = EphemeralAccountContractClient::new(&env, &contract_id); @@ -77,8 +86,7 @@ proptest! { past in 0u32..=50u32, amount in 1i128..=1_000_000_000_000i128, ) { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let contract_id = env.register(EphemeralAccountContract, ()); let client = EphemeralAccountContractClient::new(&env, &contract_id); @@ -116,8 +124,7 @@ proptest! { // with Error::AlreadyInitialized, for any valid future expiry. #[test] fn double_initialize_always_fails(offset in 1u32..=1_000_000u32) { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let contract_id = env.register(EphemeralAccountContract, ()); let client = EphemeralAccountContractClient::new(&env, &contract_id); @@ -153,8 +160,7 @@ proptest! { fn past_expiry_always_rejects_init( past_offset in 0u32..=1_000_000u32, ) { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let contract_id = env.register(EphemeralAccountContract, ()); let client = EphemeralAccountContractClient::new(&env, &contract_id); @@ -185,8 +191,7 @@ proptest! { // (first call), regardless of the specific future ledger number. #[test] fn future_expiry_always_succeeds(offset in 1u32..=1_000_000u32) { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let contract_id = env.register(EphemeralAccountContract, ()); let client = EphemeralAccountContractClient::new(&env, &contract_id); @@ -217,8 +222,7 @@ proptest! { fn random_addresses_do_not_panic( offset in 1u32..=10_000u32, ) { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let contract_id = env.register(EphemeralAccountContract, ()); let client = EphemeralAccountContractClient::new(&env, &contract_id); @@ -249,8 +253,7 @@ proptest! { fn record_payment_various_amounts( amounts in prop::collection::vec(1i128..=i128::MAX, 1..=5), ) { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let contract_id = env.register(EphemeralAccountContract, ()); let client = EphemeralAccountContractClient::new(&env, &contract_id); diff --git a/contracts/shared/src/events.rs b/contracts/shared/src/events.rs index e2d2c5c..6a775ff 100644 --- a/contracts/shared/src/events.rs +++ b/contracts/shared/src/events.rs @@ -1,8 +1,8 @@ use crate::types::Payment; use soroban_sdk::{contracttype, Address, Vec}; -/// Issue #40: contract event definitions live in the shared crate so the SDK -/// and every contract reference identical event schemas. +// Issue #40: contract event definitions live in the shared crate so the SDK +// and every contract reference identical event schemas. /// Emitted when an ephemeral account is created. #[contracttype] diff --git a/contracts/shared/src/passphrase.rs b/contracts/shared/src/passphrase.rs index 44a8b84..a3b6cd0 100644 --- a/contracts/shared/src/passphrase.rs +++ b/contracts/shared/src/passphrase.rs @@ -1,8 +1,8 @@ use soroban_sdk::{Bytes, Env}; -/// well-known network passphrase constants (32-byte SHA-256 hashes are -/// stored on-chain as `Bytes<32>`; the human-readable strings are what -/// wallets sign against — we hash them here for comparison). +// Well-known network passphrase constants (32-byte SHA-256 hashes are +// stored on-chain as `Bytes<32>`; the human-readable strings are what +// wallets sign against — we hash them here for comparison). /// Stellar Public Network passphrase pub const PUBLIC_NETWORK_PASSPHRASE: &str = "Public Global Stellar Network ; September 2015"; @@ -38,9 +38,20 @@ pub fn require_network(env: &Env, expected_passphrase: &str) -> Result<(), sorob } } +/// Return the 32-byte ledger network id for the standalone passphrase. +/// +/// `Env::default()` in the Soroban test host initializes the ledger with an +/// all-zero network id rather than the standalone passphrase, so tests that +/// exercise `require_network` (which `initialize` enforces) must set the +/// ledger network id via `env.ledger().set_network_id(...)` before calling. +pub fn standalone_network_id(env: &Env) -> [u8; 32] { + hash_passphrase(env, STANDALONE_PASSPHRASE).to_array() +} + #[cfg(test)] mod tests { use super::*; + use soroban_sdk::testutils::Ledger as _; #[test] fn test_hash_passphrase_deterministic() { @@ -59,9 +70,11 @@ mod tests { } #[test] - fn test_require_network_passes_in_default_env() { - // Env::default() uses the standalone passphrase. + fn test_require_network_passes_with_standalone_network_id() { + // Env::default() starts with an all-zero network id, so tests must + // opt in to the standalone passphrase before require_network passes. let env = Env::default(); + env.ledger().set_network_id(standalone_network_id(&env)); assert!(require_network(&env, STANDALONE_PASSPHRASE).is_ok()); } diff --git a/contracts/sweep_controller/src/lib.rs b/contracts/sweep_controller/src/lib.rs index 84d4b12..56b0f88 100644 --- a/contracts/sweep_controller/src/lib.rs +++ b/contracts/sweep_controller/src/lib.rs @@ -4,14 +4,13 @@ mod authorization; mod errors; mod migration; mod storage; -mod transfers; -#[cfg(test)] mod test; +mod transfers; use ephemeral_account::EphemeralAccountContractClient as EphemeralAccountClient; use soroban_sdk::{ auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation}, - contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, IntoVal, Symbol, Vec, + contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, IntoVal, Vec, }; use authorization::AuthContext; @@ -145,6 +144,17 @@ impl SweepController { let account_client = EphemeralAccountClient::new(&env, &ephemeral_account); let info = account_client.get_info(); + + let mut payments_vec = Vec::new(&env); + for payment in info.payments.iter() { + payments_vec.push_back(Payment { + asset: payment.asset.clone(), + amount: payment.amount, + timestamp: payment.timestamp, + }); + } + emit_sweep_executed_multi(&env, recipient.clone(), payments_vec); + let amount = info.payments.iter().map(|p| p.amount).sum(); emit_sweep_completed(&env, ephemeral_account, recipient, amount); @@ -228,7 +238,7 @@ impl SweepController { }); } - transfers::execute_transfers(env, &ephemeral_account, &destination, &payments_vec); + transfers::execute_transfers(env, &ephemeral_account, &destination, &payments_vec)?; // Emit the per-asset breakdown, then the summed completion event. emit_sweep_executed_multi(env, destination.clone(), payments_vec); @@ -242,13 +252,33 @@ impl SweepController { ephemeral_account: &Address, recipient: &Address, ) -> Result<(), Error> { - let auth_signature = BytesN::from_array(env, &[0; 64]); - Self::authorize_ephemeral_sweep(env, ephemeral_account, recipient, &auth_signature); + Self::authorize_ephemeral_sweep_claim(env, ephemeral_account, recipient); let account_client = EphemeralAccountClient::new(env, ephemeral_account); - account_client.sweep(recipient, &auth_signature); + account_client.sweep_claim(recipient); Ok(()) } + + fn authorize_ephemeral_sweep_claim( + env: &Env, + ephemeral_account: &Address, + recipient: &Address, + ) { + let args = (recipient.clone(),).into_val(env); + let context = ContractContext { + contract: ephemeral_account.clone(), + fn_name: soroban_sdk::Symbol::new(env, "sweep_claim"), + args, + }; + let auth_entries = Vec::from_array( + env, + [InvokerContractAuthEntry::Contract(SubContractInvocation { + context, + sub_invocations: Vec::new(env), + })], + ); + env.authorize_as_current_contract(auth_entries); + } /// Check if an account is ready for sweep pub fn can_sweep(env: Env, ephemeral_account: Address) -> bool { storage::extend_instance_ttl(&env); @@ -257,8 +287,7 @@ impl SweepController { let info = account_client.get_info(); - info.status as u32 == AccountStatus::PaymentReceived as u32 - && !account_client.is_expired() + info.status as u32 == AccountStatus::PaymentReceived as u32 && !account_client.is_expired() } /// Return the current sweep nonce for this controller. diff --git a/contracts/sweep_controller/src/migration.rs b/contracts/sweep_controller/src/migration.rs index 8f693dd..0bb0087 100644 --- a/contracts/sweep_controller/src/migration.rs +++ b/contracts/sweep_controller/src/migration.rs @@ -1,6 +1,4 @@ -use soroban_sdk::{contracttype, BytesN, Env}; - -use crate::storage; +use soroban_sdk::{contracttype, Env}; /// Storage schema version. Stored on-chain under `StorageKey::StorageVersion`. /// Bump this whenever the storage layout changes in a backwards-incompatible @@ -40,7 +38,9 @@ pub const CURRENT_VERSION: StorageVersion = StorageVersion { /// Read the stored schema version from instance storage. /// Returns `None` if no version has been stored yet (pre-migration contract). pub fn get_storage_version(env: &Env) -> Option { - env.storage().instance().get(&crate::storage::DataKey::StorageVersion) + env.storage() + .instance() + .get(&crate::storage::DataKey::StorageVersion) } /// Write the schema version to instance storage. @@ -104,7 +104,8 @@ mod tests { #[test] fn get_version_returns_none_before_migration() { let env = Env::default(); - let v = get_storage_version(&env); + let contract_id = env.register(crate::SweepController, ()); + let v = env.as_contract(&contract_id, || get_storage_version(&env)); assert!(v.is_none()); } } diff --git a/contracts/sweep_controller/src/storage.rs b/contracts/sweep_controller/src/storage.rs index 486c0c9..03be1ae 100644 --- a/contracts/sweep_controller/src/storage.rs +++ b/contracts/sweep_controller/src/storage.rs @@ -185,9 +185,7 @@ pub fn get_pending_signer_effective_ledger(env: &Env) -> Option { /// Clear pending signer state (call after applying the new signer) pub fn clear_pending_signer(env: &Env) { - env.storage() - .instance() - .remove(&DataKey::PendingSigner); + env.storage().instance().remove(&DataKey::PendingSigner); env.storage() .instance() .remove(&DataKey::PendingSignerEffectiveLedger); diff --git a/contracts/sweep_controller/src/test.rs b/contracts/sweep_controller/src/test.rs index 5bb4605..344972c 100644 --- a/contracts/sweep_controller/src/test.rs +++ b/contracts/sweep_controller/src/test.rs @@ -2,12 +2,20 @@ extern crate std; +use crate::{Error, SweepController, SweepControllerClient}; use ephemeral_account::{AccountStatus, EphemeralAccountContract, EphemeralAccountContractClient}; use soroban_sdk::{ - testutils::{Address as _, Events}, - Address, BytesN, Env, + testutils::{Address as _, Events, Ledger as _}, + Address, BytesN, Env, Symbol, TryFromVal, }; -use sweep_controller::{Error, SweepController, SweepControllerClient}; + +fn test_env() -> Env { + let env = Env::default(); + env.mock_all_auths(); + env.ledger() + .set_network_id(bridgelet_shared::passphrase::standalone_network_id(&env)); + env +} fn setup_controller_and_account( env: &Env, @@ -42,7 +50,8 @@ fn setup_controller_and_account( &expiry, &recovery, &controller_id, - &account_creator, + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), ); let asset = Address::generate(env); @@ -50,12 +59,7 @@ fn setup_controller_and_account( ephemeral_client.record_payment(&500, &asset); env.set_auths(&[]); - ( - controller_client, - ephemeral_client, - ephemeral_id, - creator, - ) + (controller_client, ephemeral_client, ephemeral_id, creator) } // ── Issue #155: Verify SweepExecutedMulti event fields ────────────────── @@ -64,8 +68,7 @@ fn setup_controller_and_account( /// (asset, amount) pairs, and the ledger sequence. #[test] fn test_sweep_executed_multi_event_includes_all_fields() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let ephemeral_id = env.register(EphemeralAccountContract, ()); let ephemeral_client = EphemeralAccountContractClient::new(&env, &ephemeral_id); @@ -84,27 +87,28 @@ fn test_sweep_executed_multi_event_includes_all_fields() { &expiry, &recovery, &Address::generate(&env), // controller (not used for direct sweep) + &BytesN::from_array(&env, &[0u8; 32]), &Address::generate(&env), ); ephemeral_client.record_payment(&100, &asset1); ephemeral_client.record_payment(&200, &asset2); - let auth_sig = BytesN::from_array(&env, &[0u8; 64]); env.mock_all_auths(); - ephemeral_client.sweep(&destination, &auth_sig); + ephemeral_client.sweep_claim(&destination); // Verify the events were emitted with correct structure let events = env.events(); let all_events: std::vec::Vec<_> = events.all().iter().collect(); // Find the SweepExecutedMulti event (topic: "swept_mul") - let sweep_event = all_events + all_events .iter() .find(|e| { - let topic = &e.0; - // Check if the topic symbol matches "swept_mul" - soroban_sdk::symbol_short!("swept_mul") == *topic + // The first topic element is the symbol; e.0 is the emitter address. + e.1.first() + .and_then(|v| Symbol::try_from_val(&env, &v).ok()) + == Some(soroban_sdk::symbol_short!("swept_mul")) }) .expect("SweepExecutedMulti event not found"); @@ -120,8 +124,7 @@ fn test_sweep_executed_multi_event_includes_all_fields() { /// Verify SweepExecutedMulti event is emitted with correct payment data #[test] fn test_sweep_event_records_payment_amounts() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let ephemeral_id = env.register(EphemeralAccountContract, ()); let client = EphemeralAccountContractClient::new(&env, &ephemeral_id); @@ -137,12 +140,12 @@ fn test_sweep_event_records_payment_amounts() { &expiry, &recovery, &Address::generate(&env), + &BytesN::from_array(&env, &[0u8; 32]), &Address::generate(&env), ); client.record_payment(&777, &asset); - let auth_sig = BytesN::from_array(&env, &[0u8; 64]); - client.sweep(&destination, &auth_sig); + client.sweep_claim(&destination); let info = client.get_info(); assert_eq!(info.payments.len(), 1); @@ -157,8 +160,7 @@ fn test_sweep_event_records_payment_amounts() { /// would serve as the upgrade authority (same pattern as EphemeralAccount). #[test] fn test_sweep_controller_creator_is_stored() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let controller_id = env.register(SweepController, ()); let client = SweepControllerClient::new(&env, &controller_id); @@ -176,8 +178,7 @@ fn test_sweep_controller_creator_is_stored() { /// Test successful sweep via execute_sweep (with mocked auth) #[test] fn test_execute_sweep_unauthorized_signer_fails() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let (controller_client, _ephemeral_client, ephemeral_id, _creator) = setup_controller_and_account(&env); @@ -194,8 +195,7 @@ fn test_execute_sweep_unauthorized_signer_fails() { /// Test that sweep of account with no payment fails #[test] fn test_sweep_account_not_ready_without_payment() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let controller_id = env.register(SweepController, ()); let controller_client = SweepControllerClient::new(&env, &controller_id); @@ -215,7 +215,8 @@ fn test_sweep_account_not_ready_without_payment() { &expiry, &recovery, &controller_id, - &account_creator, + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), ); // No record_payment called @@ -231,8 +232,7 @@ fn test_sweep_account_not_ready_without_payment() { /// Test can_sweep returns true when account has payment and is not expired #[test] fn test_can_sweep_returns_true_for_ready_account() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let (_controller_client, _ephemeral_client, ephemeral_id, _creator) = setup_controller_and_account(&env); @@ -240,11 +240,10 @@ fn test_can_sweep_returns_true_for_ready_account() { assert!(_controller_client.can_sweep(&ephemeral_id)); } -/// Test can_sweep returns false for uninitialized account +/// Test can_sweep returns false when no payment has been recorded #[test] -fn test_can_sweep_returns_false_for_uninitialized() { - let env = Env::default(); - env.mock_all_auths(); +fn test_can_sweep_returns_false_without_payment() { + let env = test_env(); let controller_id = env.register(SweepController, ()); let controller_client = SweepControllerClient::new(&env, &controller_id); @@ -252,15 +251,27 @@ fn test_can_sweep_returns_false_for_uninitialized() { let signer_pub = BytesN::from_array(&env, &[1u8; 32]); controller_client.initialize(&creator, &signer_pub, &None); - let fake_account = Address::generate(&env); - assert!(!controller_client.can_sweep(&fake_account)); + let ephemeral_id = env.register(EphemeralAccountContract, ()); + let ephemeral_client = EphemeralAccountContractClient::new(&env, &ephemeral_id); + let account_creator = Address::generate(&env); + let recovery = Address::generate(&env); + let expiry = env.ledger().sequence() + 1000; + ephemeral_client.initialize( + &account_creator, + &expiry, + &recovery, + &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), + ); + // No payment recorded — account is not sweepable yet + assert!(!controller_client.can_sweep(&ephemeral_id)); } /// Test get_nonce returns initial value #[test] fn test_get_nonce_initial() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let controller_id = env.register(SweepController, ()); let client = SweepControllerClient::new(&env, &controller_id); @@ -275,7 +286,7 @@ fn test_get_nonce_initial() { /// Test claim requires recipient auth #[test] fn test_claim_rejects_unauthorized_recipient() { - let env = Env::default(); + let env = test_env(); let recipient = Address::generate(&env); let (controller_client, _ephemeral_client, ephemeral_id) = { @@ -295,7 +306,8 @@ fn test_claim_rejects_unauthorized_recipient() { &expiry, &recovery, &controller_id, - &account_creator, + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), ); let asset = Address::generate(&env); env.mock_all_auths_allowing_non_root_auth(); @@ -316,8 +328,7 @@ fn test_claim_rejects_unauthorized_recipient() { /// invalid state, the entire operation reverts (no partial state changes). #[test] fn test_atomic_sweep_reverts_on_invalid_state() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let controller_id = env.register(SweepController, ()); let controller_client = SweepControllerClient::new(&env, &controller_id); @@ -335,7 +346,8 @@ fn test_atomic_sweep_reverts_on_invalid_state() { &expiry, &recovery, &controller_id, - &account_creator, + &BytesN::from_array(&env, &[0u8; 32]), + &Address::generate(&env), ); // No payment recorded — sweep should fail atomically @@ -354,22 +366,22 @@ fn test_atomic_sweep_reverts_on_invalid_state() { #[test] fn test_error_variant_codes() { - assert_eq!(Error::InvalidAccount as u32, 1); - assert_eq!(Error::TransferFailed as u32, 2); - assert_eq!(Error::AuthorizationFailed as u32, 3); - assert_eq!(Error::InsufficientBalance as u32, 4); - assert_eq!(Error::AccountNotReady as u32, 5); - assert_eq!(Error::AccountExpired as u32, 6); - assert_eq!(Error::AccountAlreadySwept as u32, 7); - assert_eq!(Error::InvalidSignature as u32, 8); - assert_eq!(Error::SignatureVerificationFailed as u32, 9); - assert_eq!(Error::AuthorizedSignerNotSet as u32, 10); - assert_eq!(Error::InvalidNonce as u32, 11); - assert_eq!(Error::UnauthorizedDestination as u32, 13); - assert_eq!(Error::NotAdmin as u32, 14); - assert_eq!(Error::Overflow as u32, 15); - assert_eq!(Error::InvalidEstimateInput as u32, 16); - assert_eq!(Error::TimeLockNotElapsed as u32, 17); - assert_eq!(Error::NoPendingSignerUpdate as u32, 18); - assert_eq!(Error::NotInitialized as u32, 19); + assert_eq!(Error::InvalidAccount as u32, 2000); + assert_eq!(Error::TransferFailed as u32, 2001); + assert_eq!(Error::AuthorizationFailed as u32, 2002); + assert_eq!(Error::InsufficientBalance as u32, 2003); + assert_eq!(Error::AccountNotReady as u32, 2004); + assert_eq!(Error::AccountExpired as u32, 2005); + assert_eq!(Error::AccountAlreadySwept as u32, 2006); + assert_eq!(Error::InvalidSignature as u32, 2007); + assert_eq!(Error::SignatureVerificationFailed as u32, 2008); + assert_eq!(Error::AuthorizedSignerNotSet as u32, 2009); + assert_eq!(Error::InvalidNonce as u32, 2010); + assert_eq!(Error::UnauthorizedDestination as u32, 2012); + assert_eq!(Error::NotAdmin as u32, 2013); + assert_eq!(Error::Overflow as u32, 2014); + assert_eq!(Error::InvalidEstimateInput as u32, 2015); + assert_eq!(Error::TimeLockNotElapsed as u32, 2016); + assert_eq!(Error::NoPendingSignerUpdate as u32, 2017); + assert_eq!(Error::NotInitialized as u32, 2018); } diff --git a/contracts/sweep_controller/src/transfers.rs b/contracts/sweep_controller/src/transfers.rs index d23d100..d91681d 100644 --- a/contracts/sweep_controller/src/transfers.rs +++ b/contracts/sweep_controller/src/transfers.rs @@ -70,6 +70,7 @@ pub fn execute_transfers( /// /// Pure function — makes no on-chain calls. Useful for SDK-level /// pre-sweep validation and fee estimation. +#[cfg(test)] pub fn estimate_total(payments: &Vec) -> Result { let mut total: i128 = 0; for payment in payments.iter() { @@ -86,12 +87,20 @@ pub fn estimate_total(payments: &Vec) -> Result { #[cfg(test)] mod tests { use super::*; + use soroban_sdk::testutils::Address as _; #[test] fn test_estimate_total_single_payment() { let env = Env::default(); let asset = Address::generate(&env); - let payments = Vec::from_array(&env, [Payment { asset, amount: 100, timestamp: 0 }]); + let payments = Vec::from_array( + &env, + [Payment { + asset, + amount: 100, + timestamp: 0, + }], + ); assert_eq!(estimate_total(&payments).unwrap(), 100); } @@ -100,10 +109,21 @@ mod tests { let env = Env::default(); let a1 = Address::generate(&env); let a2 = Address::generate(&env); - let payments = Vec::from_array(&env, [ - Payment { asset: a1, amount: 100, timestamp: 0 }, - Payment { asset: a2, amount: 200, timestamp: 0 }, - ]); + let payments = Vec::from_array( + &env, + [ + Payment { + asset: a1, + amount: 100, + timestamp: 0, + }, + Payment { + asset: a2, + amount: 200, + timestamp: 0, + }, + ], + ); assert_eq!(estimate_total(&payments).unwrap(), 300); } @@ -111,7 +131,14 @@ mod tests { fn test_estimate_total_rejects_zero_amount() { let env = Env::default(); let asset = Address::generate(&env); - let payments = Vec::from_array(&env, [Payment { asset, amount: 0, timestamp: 0 }]); + let payments = Vec::from_array( + &env, + [Payment { + asset, + amount: 0, + timestamp: 0, + }], + ); assert!(estimate_total(&payments).is_err()); } @@ -119,7 +146,14 @@ mod tests { fn test_estimate_total_rejects_negative_amount() { let env = Env::default(); let asset = Address::generate(&env); - let payments = Vec::from_array(&env, [Payment { asset, amount: -1, timestamp: 0 }]); + let payments = Vec::from_array( + &env, + [Payment { + asset, + amount: -1, + timestamp: 0, + }], + ); assert!(estimate_total(&payments).is_err()); } } diff --git a/contracts/sweep_controller/tests/integration.rs b/contracts/sweep_controller/tests/integration.rs index aca5804..0308f54 100644 --- a/contracts/sweep_controller/tests/integration.rs +++ b/contracts/sweep_controller/tests/integration.rs @@ -9,6 +9,13 @@ use soroban_sdk::{ }; use sweep_controller::{SweepController, SweepControllerClient}; +fn test_env() -> Env { + let env = Env::default(); + env.ledger() + .set_network_id(bridgelet_shared::passphrase::standalone_network_id(&env)); + env +} + fn generate_test_keypair(env: &Env) -> (BytesN<32>, BytesN<64>) { let public_key = BytesN::from_array( env, @@ -93,7 +100,7 @@ fn setup_ready_account( /// Test that re-initialization is prevented #[test] fn test_initialize_prevents_double_init() { - let env = Env::default(); + let env = test_env(); env.mock_all_auths(); let _creator = Address::generate(&env); @@ -116,7 +123,7 @@ fn test_initialize_prevents_double_init() { /// Test that valid signatures are accepted #[test] fn test_execute_sweep_with_valid_signature() { - let env = Env::default(); + let env = test_env(); env.mock_all_auths(); let _creator = Address::generate(&env); @@ -140,7 +147,14 @@ fn test_execute_sweep_with_valid_signature() { let expiry = env.ledger().sequence() + 1000; // Initialize ephemeral account, authorizing this SweepController to call sweep() - ephemeral_client.initialize(&creator, &expiry, &recovery, &controller_id, &BytesN::from_array(&env, &[0u8; 32]), &creator); + ephemeral_client.initialize( + &creator, + &expiry, + &recovery, + &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), + &creator, + ); // Create an invalid signature (all zeros - different from valid signature) let invalid_sig = BytesN::from_array(&env, &[0u8; 64]); @@ -161,7 +175,7 @@ fn test_execute_sweep_with_valid_signature() { #[test] #[should_panic] fn test_sweep_without_payment() { - let env = Env::default(); + let env = test_env(); env.mock_all_auths(); let ephemeral_id = env.register(EphemeralAccountContract, ()); @@ -192,7 +206,7 @@ fn test_sweep_without_payment() { #[test] #[ignore = "TODO(unignore-as-followup): SweepController::claim -> authorize_claim -> sub-invoke auth chain broken on this branch; restore once Soroban auth-tree mock covers nested InvokerContractAuthEntry"] fn test_claim_succeeds_with_recipient_auth_and_relayable_flow() { - let env = Env::default(); + let env = test_env(); let recipient = Address::generate(&env); let (controller_client, ephemeral_client, ephemeral_id) = @@ -218,7 +232,7 @@ fn test_claim_succeeds_with_recipient_auth_and_relayable_flow() { #[test] #[ignore = "TODO(unignore-as-followup): SweepController::claim -> authorize_claim -> sub-invoke auth chain broken on this branch; restore once Soroban auth-tree mock covers nested InvokerContractAuthEntry"] fn test_claim_records_recipient_authorization_context() { - let env = Env::default(); + let env = test_env(); let recipient = Address::generate(&env); let (controller_client, _, ephemeral_id) = setup_ready_account(&env, Some(recipient.clone())); @@ -254,7 +268,7 @@ fn test_claim_records_recipient_authorization_context() { #[test] #[ignore = "TODO(unignore-as-followup): SweepController::claim -> authorize_claim -> sub-invoke auth chain broken on this branch; restore once Soroban auth-tree mock covers nested InvokerContractAuthEntry"] fn test_claim_rejects_wrong_recipient_for_locked_destination() { - let env = Env::default(); + let env = test_env(); let locked_destination = Address::generate(&env); let recipient = Address::generate(&env); @@ -278,7 +292,7 @@ fn test_claim_rejects_wrong_recipient_for_locked_destination() { #[test] fn test_unauthorized_signer_not_set() { - let env = Env::default(); + let env = test_env(); env.mock_all_auths(); // Deploy controller without initialization @@ -297,7 +311,14 @@ fn test_unauthorized_signer_not_set() { let expiry = env.ledger().sequence() + 1000; // Initialize ephemeral account, authorizing this SweepController to call sweep() - ephemeral_client.initialize(&creator, &expiry, &recovery, &controller_id, &BytesN::from_array(&env, &[0u8; 32]), &creator); + ephemeral_client.initialize( + &creator, + &expiry, + &recovery, + &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), + &creator, + ); // Record payment ephemeral_client.record_payment(&100, &asset); @@ -322,7 +343,7 @@ fn test_unauthorized_signer_not_set() { #[test] #[ignore = "TODO(unignore-as-followup): SweepController::claim -> authorize_claim -> sub-invoke auth chain broken on this branch; restore once Soroban auth-tree mock covers nested InvokerContractAuthEntry"] fn test_initialize_with_authorized_destination() { - let env = Env::default(); + let env = test_env(); let controller_id = env.register(SweepController, ()); let controller_client = SweepControllerClient::new(&env, &controller_id); @@ -398,7 +419,7 @@ fn test_initialize_with_authorized_destination() { #[test] fn test_second_sweep_rejected_after_successful_claim() { - let env = Env::default(); + let env = test_env(); let (controller_client, _ephemeral_client, ephemeral_id) = setup_ready_account(&env, None); @@ -490,6 +511,7 @@ fn setup_full_lifecycle( &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ) .into_val(env), @@ -501,6 +523,7 @@ fn setup_full_lifecycle( &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ); @@ -521,7 +544,7 @@ fn setup_full_lifecycle( /// Deploy → init → record → claim → verify full state including reserve. #[test] fn test_full_lifecycle_deploy_init_record_claim_verify_state() { - let env = Env::default(); + let env = test_env(); let (controller_client, ephemeral_client, ephemeral_id, recipient, _asset) = setup_full_lifecycle(&env); @@ -567,7 +590,7 @@ fn test_full_lifecycle_deploy_init_record_claim_verify_state() { /// Full lifecycle with multiple assets. #[test] fn test_full_lifecycle_multi_asset_claim() { - let env = Env::default(); + let env = test_env(); let (controller_client, controller_id, ephemeral_client, ephemeral_id) = deploy_contracts(&env); @@ -611,6 +634,7 @@ fn test_full_lifecycle_multi_asset_claim() { &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ) .into_val(&env), @@ -622,6 +646,7 @@ fn test_full_lifecycle_multi_asset_claim() { &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ); @@ -664,7 +689,7 @@ fn test_full_lifecycle_multi_asset_claim() { /// Expire flow: funds route to recovery_address. #[test] fn test_full_expire_flow_funds_to_recovery() { - let env = Env::default(); + let env = test_env(); env.mock_all_auths(); let (_, _, ephemeral_client, _) = deploy_contracts(&env); @@ -678,6 +703,7 @@ fn test_full_expire_flow_funds_to_recovery() { &expiry, &recovery, &Address::generate(&env), + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ); @@ -698,7 +724,7 @@ fn test_full_expire_flow_funds_to_recovery() { /// Recover flow: creator triggers recovery after expiry. #[test] fn test_full_recover_flow_creator_after_expiry() { - let env = Env::default(); + let env = test_env(); env.mock_all_auths(); let (_, _, ephemeral_client, _) = deploy_contracts(&env); @@ -712,6 +738,7 @@ fn test_full_recover_flow_creator_after_expiry() { &expiry, &recovery, &Address::generate(&env), + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ); @@ -730,7 +757,7 @@ fn test_full_recover_flow_creator_after_expiry() { /// Sweep rejected after expiry via claim. #[test] fn test_sweep_rejected_after_expiry_via_claim() { - let env = Env::default(); + let env = test_env(); let recipient = Address::generate(&env); let (controller_client, _ephemeral_client, ephemeral_id, _, _) = setup_full_lifecycle(&env); @@ -745,7 +772,7 @@ fn test_sweep_rejected_after_expiry_via_claim() { /// Sweep rejected when no payment recorded. #[test] fn test_sweep_rejected_when_no_payment_recorded() { - let env = Env::default(); + let env = test_env(); let (_, controller_id, ephemeral_client, ephemeral_id) = deploy_contracts(&env); @@ -790,6 +817,7 @@ fn test_sweep_rejected_when_no_payment_recorded() { &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ) .into_val(&env), @@ -801,6 +829,7 @@ fn test_sweep_rejected_when_no_payment_recorded() { &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ); @@ -811,7 +840,7 @@ fn test_sweep_rejected_when_no_payment_recorded() { /// Double claim rejected. #[test] fn test_double_claim_rejected() { - let env = Env::default(); + let env = test_env(); let (controller_client, ephemeral_client, ephemeral_id, recipient, _) = setup_full_lifecycle(&env); @@ -836,7 +865,7 @@ fn test_double_claim_rejected() { /// Locked destination rejects wrong address. #[test] fn test_locked_destination_rejects_wrong_address() { - let env = Env::default(); + let env = test_env(); let (controller_client, _, _, _, _) = setup_full_lifecycle(&env); @@ -850,7 +879,7 @@ fn test_locked_destination_rejects_wrong_address() { /// CanSweep returns correct values for different account states. #[test] fn test_can_sweep_reflects_account_state() { - let env = Env::default(); + let env = test_env(); env.mock_all_auths(); let (controller_client, controller_id, ephemeral_client, ephemeral_id) = deploy_contracts(&env); @@ -868,6 +897,7 @@ fn test_can_sweep_reflects_account_state() { &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ); @@ -907,6 +937,7 @@ fn test_can_sweep_reflects_account_state() { &expiry2, &recovery2, &controller_id2, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator2, ); ephemeral_client2.record_payment(&100, &Address::generate(&env)); @@ -931,7 +962,7 @@ fn test_can_sweep_reflects_account_state() { /// SweepCompleted event emitted during claim. #[test] fn test_claim_emits_sweep_completed_event() { - let env = Env::default(); + let env = test_env(); let (controller_client, _ephemeral_client, ephemeral_id, recipient, _asset) = setup_full_lifecycle(&env); @@ -967,7 +998,7 @@ fn test_claim_emits_sweep_completed_event() { /// Nonce starts at 0 after initialization. #[test] fn test_nonce_starts_at_zero() { - let env = Env::default(); + let env = test_env(); env.mock_all_auths(); let (controller_client, _, _, _, _) = setup_full_lifecycle(&env); @@ -978,7 +1009,7 @@ fn test_nonce_starts_at_zero() { /// Multiple ephemeral accounts can be managed by the same controller. #[test] fn test_single_controller_manages_multiple_accounts() { - let env = Env::default(); + let env = test_env(); let (controller_client, controller_id, _, _) = deploy_contracts(&env); @@ -1027,6 +1058,7 @@ fn test_single_controller_manages_multiple_accounts() { &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ) .into_val(&env), @@ -1038,6 +1070,7 @@ fn test_single_controller_manages_multiple_accounts() { &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ); @@ -1073,7 +1106,7 @@ fn test_single_controller_manages_multiple_accounts() { /// Expire via recovery_address (not creator) after expiry. #[test] fn test_recovery_address_can_expire_account() { - let env = Env::default(); + let env = test_env(); env.mock_all_auths(); let (_, _, ephemeral_client, _) = deploy_contracts(&env); @@ -1087,6 +1120,7 @@ fn test_recovery_address_can_expire_account() { &expiry, &recovery, &Address::generate(&env), + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ); @@ -1105,7 +1139,7 @@ fn test_recovery_address_can_expire_account() { /// get_info returns correct state at each lifecycle stage. #[test] fn test_get_info_reflects_lifecycle_stages() { - let env = Env::default(); + let env = test_env(); env.mock_all_auths(); let (_, _, ephemeral_client, _) = deploy_contracts(&env); @@ -1119,6 +1153,7 @@ fn test_get_info_reflects_lifecycle_stages() { &expiry, &recovery, &Address::generate(&env), + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ); @@ -1144,7 +1179,7 @@ fn test_get_info_reflects_lifecycle_stages() { /// Claim with flexible controller (no locked destination) succeeds for any recipient. #[test] fn test_claim_with_flexible_destination() { - let env = Env::default(); + let env = test_env(); let (controller_client, controller_id, ephemeral_client, ephemeral_id) = deploy_contracts(&env); @@ -1179,6 +1214,7 @@ fn test_claim_with_flexible_destination() { &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ) .into_val(&env), @@ -1190,6 +1226,7 @@ fn test_claim_with_flexible_destination() { &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ); @@ -1219,7 +1256,7 @@ fn test_claim_with_flexible_destination() { /// multi-asset claim, not just the summed SweepCompleted event. #[test] fn test_claim_emits_sweep_executed_multi_with_all_assets() { - let env = Env::default(); + let env = test_env(); let (controller_client, controller_id, ephemeral_client, ephemeral_id) = deploy_contracts(&env); @@ -1263,6 +1300,7 @@ fn test_claim_emits_sweep_executed_multi_with_all_assets() { &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ) .into_val(&env), @@ -1274,6 +1312,7 @@ fn test_claim_emits_sweep_executed_multi_with_all_assets() { &expiry, &recovery, &controller_id, + &BytesN::from_array(&env, &[0u8; 32]), &account_creator, ); @@ -1318,4 +1357,3 @@ fn test_claim_emits_sweep_executed_multi_with_all_assets() { "SweepExecutedMulti event should be emitted with all payments" ); } -