From b95964cd0784b69a07f80a68ce4b77ee0706b497 Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Thu, 22 Jan 2026 17:57:02 -0600 Subject: [PATCH 1/3] feat(payment-vault): add contract events --- contracts/payment-vault-contract/src/events.rs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 contracts/payment-vault-contract/src/events.rs diff --git a/contracts/payment-vault-contract/src/events.rs b/contracts/payment-vault-contract/src/events.rs new file mode 100644 index 0000000..ac8407c --- /dev/null +++ b/contracts/payment-vault-contract/src/events.rs @@ -0,0 +1,6 @@ +use soroban_sdk::{Env, symbol_short}; + +pub fn session_finalized(env: &Env, booking_id: u64, actual_duration: u64, total_cost: i128) { + let topics = (symbol_short!("finalized"), booking_id); + env.events().publish(topics, (actual_duration, total_cost)); +} From aa8276d80dd0a8c16a869f4ee45525e3dd67d277 Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Thu, 22 Jan 2026 17:57:06 -0600 Subject: [PATCH 2/3] feat(payment-vault): emit events and integrate with contract logic --- .../payment-vault-contract/src/contract.rs | 111 +++++++++++++++++- contracts/payment-vault-contract/src/error.rs | 3 + contracts/payment-vault-contract/src/lib.rs | 29 ++++- .../payment-vault-contract/src/storage.rs | 53 +++++++++ 4 files changed, 188 insertions(+), 8 deletions(-) diff --git a/contracts/payment-vault-contract/src/contract.rs b/contracts/payment-vault-contract/src/contract.rs index 4e6f891..7783eed 100644 --- a/contracts/payment-vault-contract/src/contract.rs +++ b/contracts/payment-vault-contract/src/contract.rs @@ -1,11 +1,12 @@ -use soroban_sdk::{Address, Env}; -use crate::storage; +use soroban_sdk::{Address, Env, token}; +use crate::storage::{self, Booking, BookingStatus}; use crate::error::VaultError; +use crate::events; pub fn initialize_vault( - env: &Env, - admin: &Address, - token: &Address, + env: &Env, + admin: &Address, + token: &Address, oracle: &Address ) -> Result<(), VaultError> { // 1. Check if already initialized @@ -18,5 +19,105 @@ pub fn initialize_vault( storage::set_token(env, token); storage::set_oracle(env, oracle); + Ok(()) +} + +pub fn create_booking( + env: &Env, + user: &Address, + expert: &Address, + rate: i128, + booked_duration: u64, +) -> Result { + // Require authorization from the user creating the booking + user.require_auth(); + + // Validate rate + if rate <= 0 { + return Err(VaultError::InvalidAmount); + } + + // Calculate total deposit + let total_deposit = rate * (booked_duration as i128); + + if total_deposit <= 0 { + return Err(VaultError::InvalidAmount); + } + + // Get the token contract + let token_address = storage::get_token(env); + let token_client = token::Client::new(env, &token_address); + + // Transfer tokens from user to this contract + let contract_address = env.current_contract_address(); + token_client.transfer(user, &contract_address, &total_deposit); + + // Generate booking ID and create booking + let booking_id = storage::get_next_booking_id(env); + let booking = Booking { + id: booking_id, + expert: expert.clone(), + user: user.clone(), + rate, + total_deposit, + booked_duration, + status: BookingStatus::Pending, + }; + + // Save booking + storage::save_booking(env, &booking); + + Ok(booking_id) +} + +pub fn finalize_session( + env: &Env, + booking_id: u64, + actual_duration: u64, +) -> Result<(), VaultError> { + // 1. Require Oracle authorization + let oracle = storage::get_oracle(env); + oracle.require_auth(); + + // 2. Get booking and verify it exists + let booking = storage::get_booking(env, booking_id) + .ok_or(VaultError::BookingNotFound)?; + + // 3. Verify booking is in Pending status + if booking.status != BookingStatus::Pending { + return Err(VaultError::BookingNotPending); + } + + // 4. Calculate payments + let expert_pay = booking.rate * (actual_duration as i128); + let refund = booking.total_deposit - expert_pay; + + // Ensure calculations are valid + if expert_pay < 0 || refund < 0 { + return Err(VaultError::InvalidAmount); + } + + // 5. Get token contract + let token_address = storage::get_token(env); + let token_client = token::Client::new(env, &token_address); + let contract_address = env.current_contract_address(); + + // 6. Execute transfers + // Pay expert + if expert_pay > 0 { + token_client.transfer(&contract_address, &booking.expert, &expert_pay); + } + + // Refund user + if refund > 0 { + token_client.transfer(&contract_address, &booking.user, &refund); + } + + // 7. Update booking status to Complete + storage::update_booking_status(env, booking_id, BookingStatus::Complete); + + // 8. Emit SessionFinalized event + events::session_finalized(env, booking_id, actual_duration, expert_pay); + Ok(()) } \ No newline at end of file diff --git a/contracts/payment-vault-contract/src/error.rs b/contracts/payment-vault-contract/src/error.rs index 57768a3..9803897 100644 --- a/contracts/payment-vault-contract/src/error.rs +++ b/contracts/payment-vault-contract/src/error.rs @@ -7,4 +7,7 @@ pub enum VaultError { NotInitialized = 1, AlreadyInitialized = 2, NotAuthorized = 3, + BookingNotFound = 4, + BookingNotPending = 5, + InvalidAmount = 6, } \ No newline at end of file diff --git a/contracts/payment-vault-contract/src/lib.rs b/contracts/payment-vault-contract/src/lib.rs index c7bbb42..0402cef 100644 --- a/contracts/payment-vault-contract/src/lib.rs +++ b/contracts/payment-vault-contract/src/lib.rs @@ -2,6 +2,7 @@ mod contract; mod error; +mod events; mod storage; #[cfg(test)] mod test; @@ -16,11 +17,33 @@ pub struct PaymentVaultContract; impl PaymentVaultContract { /// Initialize the vault with the Admin, the Payment Token, and the Oracle (Backend) pub fn init( - env: Env, - admin: Address, - token: Address, + env: Env, + admin: Address, + token: Address, oracle: Address ) -> Result<(), VaultError> { contract::initialize_vault(&env, &admin, &token, &oracle) } + + /// Create a booking for a consultation session + /// User deposits tokens upfront based on rate * booked_duration + pub fn create_booking( + env: Env, + user: Address, + expert: Address, + rate: i128, + booked_duration: u64, + ) -> Result { + contract::create_booking(&env, &user, &expert, rate, booked_duration) + } + + /// Finalize a session (Oracle-only) + /// Calculates payments based on actual duration and processes refunds + pub fn finalize_session( + env: Env, + booking_id: u64, + actual_duration: u64, + ) -> Result<(), VaultError> { + contract::finalize_session(&env, booking_id, actual_duration) + } } \ No newline at end of file diff --git a/contracts/payment-vault-contract/src/storage.rs b/contracts/payment-vault-contract/src/storage.rs index bdb1b00..8ba49a3 100644 --- a/contracts/payment-vault-contract/src/storage.rs +++ b/contracts/payment-vault-contract/src/storage.rs @@ -6,6 +6,27 @@ pub enum DataKey { Admin, Token, Oracle, + Booking(u64), // Booking ID -> Booking + BookingCounter, // Counter for generating unique booking IDs +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum BookingStatus { + Pending, + Complete, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct Booking { + pub id: u64, + pub expert: Address, + pub user: Address, + pub rate: i128, // Payment per second + pub total_deposit: i128, // Total amount deposited by user + pub booked_duration: u64, // Booked duration in seconds + pub status: BookingStatus, } // --- Admin --- @@ -37,4 +58,36 @@ pub fn set_oracle(env: &Env, oracle: &Address) { pub fn get_oracle(env: &Env) -> Address { env.storage().instance().get(&DataKey::Oracle).unwrap() +} + +// --- Booking Counter --- +pub fn get_next_booking_id(env: &Env) -> u64 { + let current: u64 = env + .storage() + .instance() + .get(&DataKey::BookingCounter) + .unwrap_or(0); + let next = current + 1; + env.storage().instance().set(&DataKey::BookingCounter, &next); + next +} + +// --- Bookings --- +pub fn save_booking(env: &Env, booking: &Booking) { + env.storage() + .persistent() + .set(&DataKey::Booking(booking.id), booking); +} + +pub fn get_booking(env: &Env, booking_id: u64) -> Option { + env.storage() + .persistent() + .get(&DataKey::Booking(booking_id)) +} + +pub fn update_booking_status(env: &Env, booking_id: u64, status: BookingStatus) { + if let Some(mut booking) = get_booking(env, booking_id) { + booking.status = status; + save_booking(env, &booking); + } } \ No newline at end of file From b278be4d20894f2c5d2087b3cd23567133d7b872 Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Thu, 22 Jan 2026 17:57:10 -0600 Subject: [PATCH 3/3] test(payment-vault): cover event emission and vault logic --- contracts/payment-vault-contract/src/test.rs | 205 ++++++++++++++++++- 1 file changed, 200 insertions(+), 5 deletions(-) diff --git a/contracts/payment-vault-contract/src/test.rs b/contracts/payment-vault-contract/src/test.rs index 03f46d3..c4e0851 100644 --- a/contracts/payment-vault-contract/src/test.rs +++ b/contracts/payment-vault-contract/src/test.rs @@ -1,20 +1,30 @@ #![cfg(test)] use crate::{PaymentVaultContract, PaymentVaultContractClient}; -use soroban_sdk::{Env, testutils::Address as _}; +use soroban_sdk::{ + testutils::Address as _, + token, Address, Env, +}; + +extern crate std; fn create_client<'a>(env: &'a Env) -> PaymentVaultContractClient<'a> { - let contract_id = env.register_contract(None, PaymentVaultContract); + let contract_id = env.register(PaymentVaultContract, ()); PaymentVaultContractClient::new(env, &contract_id) } +fn create_token_contract<'a>(env: &'a Env, admin: &Address) -> token::StellarAssetClient<'a> { + let contract = env.register_stellar_asset_contract_v2(admin.clone()); + token::StellarAssetClient::new(env, &contract.address()) +} + #[test] fn test_initialization() { let env = Env::default(); let client = create_client(&env); - let admin = soroban_sdk::Address::generate(&env); - let token = soroban_sdk::Address::generate(&env); // Mock token address - let oracle = soroban_sdk::Address::generate(&env); // Mock oracle address + let admin = Address::generate(&env); + let token = Address::generate(&env); + let oracle = Address::generate(&env); // 1. Successful Init let res = client.try_init(&admin, &token, &oracle); @@ -23,5 +33,190 @@ fn test_initialization() { // 2. Double Init (Should Fail) let res_duplicate = client.try_init(&admin, &token, &oracle); assert!(res_duplicate.is_err()); +} + +#[test] +fn test_partial_duration_scenario() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + // Create token contract and mint tokens to user + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + // Initialize vault + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + // Create booking: rate = 10 tokens/second, duration = 100 seconds + // Total deposit = 10 * 100 = 1000 tokens + let rate = 10_i128; + let booked_duration = 100_u64; + let booking_id = client.create_booking(&user, &expert, &rate, &booked_duration); + + // Verify user's balance decreased + assert_eq!(token.balance(&user), 9_000); + assert_eq!(token.balance(&client.address), 1_000); + + // Oracle finalizes with 50% of booked time (50 seconds) + let actual_duration = 50_u64; + client.finalize_session(&booking_id, &actual_duration); + + // Expected: expert_pay = 10 * 50 = 500, refund = 1000 - 500 = 500 + assert_eq!(token.balance(&expert), 500); + assert_eq!(token.balance(&user), 9_500); // 9000 + 500 refund + assert_eq!(token.balance(&client.address), 0); +} + +#[test] +fn test_full_duration_no_refund() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + // Create booking + let rate = 10_i128; + let booked_duration = 100_u64; + let booking_id = client.create_booking(&user, &expert, &rate, &booked_duration); + + // Oracle finalizes with full duration (100 seconds) + let actual_duration = 100_u64; + client.finalize_session(&booking_id, &actual_duration); + + // Expected: expert_pay = 10 * 100 = 1000, refund = 0 + assert_eq!(token.balance(&expert), 1_000); + assert_eq!(token.balance(&user), 9_000); // No refund + assert_eq!(token.balance(&client.address), 0); +} + +#[test] +fn test_double_finalization_protection() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + let rate = 10_i128; + let booked_duration = 100_u64; + let booking_id = client.create_booking(&user, &expert, &rate, &booked_duration); + + // First finalization succeeds + let actual_duration = 50_u64; + let result = client.try_finalize_session(&booking_id, &actual_duration); + assert!(result.is_ok()); + + // Second finalization should fail (booking no longer Pending) + let result_duplicate = client.try_finalize_session(&booking_id, &actual_duration); + assert!(result_duplicate.is_err()); +} + +#[test] +fn test_oracle_authorization_enforcement() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + let rate = 10_i128; + let booked_duration = 100_u64; + let booking_id = client.create_booking(&user, &expert, &rate, &booked_duration); + + // Clear all mocked auths to test Oracle authorization + env.set_auths(&[]); + + // Try to finalize without any auth (should fail with auth error) + let result = client.try_finalize_session(&booking_id, &50); + assert!(result.is_err()); + + // Finalize with Oracle auth (should succeed) + env.mock_all_auths(); + client.finalize_session(&booking_id, &50); + + // Verify finalization succeeded + assert_eq!(token.balance(&expert), 500); +} + +#[test] +fn test_zero_duration_finalization() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + let rate = 10_i128; + let booked_duration = 100_u64; + let booking_id = client.create_booking(&user, &expert, &rate, &booked_duration); + + // Oracle finalizes with 0 duration (session cancelled) + let actual_duration = 0_u64; + client.finalize_session(&booking_id, &actual_duration); + + // Expected: expert_pay = 0, full refund to user + assert_eq!(token.balance(&expert), 0); + assert_eq!(token.balance(&user), 10_000); // Full refund + assert_eq!(token.balance(&client.address), 0); +} + +#[test] +fn test_booking_not_found() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let token = Address::generate(&env); + + let client = create_client(&env); + client.init(&admin, &token, &oracle); + // Try to finalize non-existent booking + let result = client.try_finalize_session(&999, &50); + assert!(result.is_err()); } \ No newline at end of file