From 5aefadd18034c3188e7f25be4a431218f1998379 Mon Sep 17 00:00:00 2001 From: majormaxx <125857575+Majormaxx@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:49:42 +0100 Subject: [PATCH 1/5] feat(vault): Added BookingRecord and BookingStatus types - Created types.rs to centralize data structures - Defined BookingRecord struct and BookingStatus enum - Aligned field names with issue specification (rate_per_second, max_duration) Refs: #6 --- contracts/payment-vault-contract/src/types.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 contracts/payment-vault-contract/src/types.rs diff --git a/contracts/payment-vault-contract/src/types.rs b/contracts/payment-vault-contract/src/types.rs new file mode 100644 index 0000000..1816565 --- /dev/null +++ b/contracts/payment-vault-contract/src/types.rs @@ -0,0 +1,23 @@ +use soroban_sdk::{contracttype, Address}; + +/// Status of a booking in the payment vault +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum BookingStatus { + Pending = 0, + Complete = 1, +} + +/// Record of a consultation booking with deposit locked +#[contracttype] +#[derive(Clone, Debug)] +pub struct BookingRecord { + pub id: u64, // Storage key identifier + pub user: Address, // User who created the booking + pub expert: Address, // Expert providing consultation + pub rate_per_second: i128, // Payment rate per second + pub max_duration: u64, // Maximum booked duration in seconds + pub total_deposit: i128, // Total deposit (rate_per_second * max_duration) + pub status: BookingStatus, // Current booking status +} From 03fc0cb70b9246e1e45b7c1224b8dd6e0260ca9e Mon Sep 17 00:00:00 2001 From: majormaxx <125857575+Majormaxx@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:49:42 +0100 Subject: [PATCH 2/5] refactor(vault): Updated storage module to use BookingRecord types - Migrated storage logic to use refined types from types module - Updated DataKey and persistence methods - Enhanced storage organization for better maintainability Refs: #6 --- .../payment-vault-contract/src/storage.rs | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/contracts/payment-vault-contract/src/storage.rs b/contracts/payment-vault-contract/src/storage.rs index 8ba49a3..b12d145 100644 --- a/contracts/payment-vault-contract/src/storage.rs +++ b/contracts/payment-vault-contract/src/storage.rs @@ -1,4 +1,5 @@ use soroban_sdk::{contracttype, Address, Env}; +use crate::types::{BookingRecord, BookingStatus}; #[contracttype] #[derive(Clone)] @@ -6,29 +7,10 @@ pub enum DataKey { Admin, Token, Oracle, - Booking(u64), // Booking ID -> Booking + Booking(u64), // Booking ID -> BookingRecord 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 --- pub fn has_admin(env: &Env) -> bool { env.storage().instance().has(&DataKey::Admin) @@ -73,13 +55,13 @@ pub fn get_next_booking_id(env: &Env) -> u64 { } // --- Bookings --- -pub fn save_booking(env: &Env, booking: &Booking) { +pub fn save_booking(env: &Env, booking: &BookingRecord) { env.storage() .persistent() .set(&DataKey::Booking(booking.id), booking); } -pub fn get_booking(env: &Env, booking_id: u64) -> Option { +pub fn get_booking(env: &Env, booking_id: u64) -> Option { env.storage() .persistent() .get(&DataKey::Booking(booking_id)) From 9fd187bb9a7515f4db9d67321bfc6b41f3d44a4e Mon Sep 17 00:00:00 2001 From: majormaxx <125857575+Majormaxx@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:49:42 +0100 Subject: [PATCH 3/5] feat(vault): Implemented book_session logic and event emission - Renamed create_booking to book_session for consistency - Implemented deposit locking logic with specifying rate and duration - Added booking_created event for frontend tracking Refs: #6 --- .../payment-vault-contract/src/contract.rs | 26 +++++++++++-------- .../payment-vault-contract/src/events.rs | 9 ++++++- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/contracts/payment-vault-contract/src/contract.rs b/contracts/payment-vault-contract/src/contract.rs index 7783eed..7c2cc2a 100644 --- a/contracts/payment-vault-contract/src/contract.rs +++ b/contracts/payment-vault-contract/src/contract.rs @@ -1,5 +1,6 @@ use soroban_sdk::{Address, Env, token}; -use crate::storage::{self, Booking, BookingStatus}; +use crate::storage; +use crate::types::{BookingRecord, BookingStatus}; use crate::error::VaultError; use crate::events; @@ -22,23 +23,23 @@ pub fn initialize_vault( Ok(()) } -pub fn create_booking( +pub fn book_session( env: &Env, user: &Address, expert: &Address, - rate: i128, - booked_duration: u64, + rate_per_second: i128, + max_duration: u64, ) -> Result { // Require authorization from the user creating the booking user.require_auth(); // Validate rate - if rate <= 0 { + if rate_per_second <= 0 { return Err(VaultError::InvalidAmount); } // Calculate total deposit - let total_deposit = rate * (booked_duration as i128); + let total_deposit = rate_per_second * (max_duration as i128); if total_deposit <= 0 { return Err(VaultError::InvalidAmount); @@ -54,19 +55,22 @@ pub fn create_booking( // Generate booking ID and create booking let booking_id = storage::get_next_booking_id(env); - let booking = Booking { + let booking = BookingRecord { id: booking_id, - expert: expert.clone(), user: user.clone(), - rate, + expert: expert.clone(), + rate_per_second, + max_duration, total_deposit, - booked_duration, status: BookingStatus::Pending, }; // Save booking storage::save_booking(env, &booking); + // Emit event for booking creation + events::booking_created(env, booking_id, user, expert, total_deposit); + Ok(booking_id) } @@ -89,7 +93,7 @@ pub fn finalize_session( } // 4. Calculate payments - let expert_pay = booking.rate * (actual_duration as i128); + let expert_pay = booking.rate_per_second * (actual_duration as i128); let refund = booking.total_deposit - expert_pay; // Ensure calculations are valid diff --git a/contracts/payment-vault-contract/src/events.rs b/contracts/payment-vault-contract/src/events.rs index ac8407c..f4a27e4 100644 --- a/contracts/payment-vault-contract/src/events.rs +++ b/contracts/payment-vault-contract/src/events.rs @@ -1,5 +1,12 @@ -use soroban_sdk::{Env, symbol_short}; +use soroban_sdk::{Address, Env, symbol_short}; +/// Emitted when a new booking is created +pub fn booking_created(env: &Env, booking_id: u64, user: &Address, expert: &Address, deposit: i128) { + let topics = (symbol_short!("booked"), booking_id); + env.events().publish(topics, (user.clone(), expert.clone(), deposit)); +} + +/// Emitted when a session is finalized 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 ef3d4df213a77dd6e62b1fc3c948a9633c63f222 Mon Sep 17 00:00:00 2001 From: majormaxx <125857575+Majormaxx@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:49:42 +0100 Subject: [PATCH 4/5] refactor(vault): Aligned public interface with naming conventions - Exposed book_session instead of create_booking - Registered types module as a public module - Updated interface to reflect refined parameter naming Refs: #6 --- contracts/payment-vault-contract/src/lib.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/contracts/payment-vault-contract/src/lib.rs b/contracts/payment-vault-contract/src/lib.rs index 0402cef..def4262 100644 --- a/contracts/payment-vault-contract/src/lib.rs +++ b/contracts/payment-vault-contract/src/lib.rs @@ -4,6 +4,7 @@ mod contract; mod error; mod events; mod storage; +mod types; #[cfg(test)] mod test; @@ -25,16 +26,16 @@ impl PaymentVaultContract { 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( + /// Book a session with an expert + /// User deposits tokens upfront based on rate_per_second * max_duration + pub fn book_session( env: Env, user: Address, expert: Address, - rate: i128, - booked_duration: u64, + rate_per_second: i128, + max_duration: u64, ) -> Result { - contract::create_booking(&env, &user, &expert, rate, booked_duration) + contract::book_session(&env, &user, &expert, rate_per_second, max_duration) } /// Finalize a session (Oracle-only) From 31443fef08a43c77c44472d6000a7e582eb4e6d7 Mon Sep 17 00:00:00 2001 From: majormaxx <125857575+Majormaxx@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:49:42 +0100 Subject: [PATCH 5/5] test(vault): Updated test suite and added balance verification - Re-aligned existing tests with new naming and structure - Added test_book_session_balance_transfer to verify contract locking logic - Ensured 100% pass rate for current and new tests Refs: #6 --- contracts/payment-vault-contract/src/test.rs | 88 ++++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/contracts/payment-vault-contract/src/test.rs b/contracts/payment-vault-contract/src/test.rs index c4e0851..a50f661 100644 --- a/contracts/payment-vault-contract/src/test.rs +++ b/contracts/payment-vault-contract/src/test.rs @@ -54,11 +54,11 @@ fn test_partial_duration_scenario() { let client = create_client(&env); client.init(&admin, &token.address, &oracle); - // Create booking: rate = 10 tokens/second, duration = 100 seconds + // Book session: rate = 10 tokens/second, max_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); + let rate_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); // Verify user's balance decreased assert_eq!(token.balance(&user), 9_000); @@ -91,10 +91,10 @@ fn test_full_duration_no_refund() { 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); + // Book session + let rate_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); // Oracle finalizes with full duration (100 seconds) let actual_duration = 100_u64; @@ -123,9 +123,9 @@ fn test_double_finalization_protection() { 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); + let rate_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); // First finalization succeeds let actual_duration = 50_u64; @@ -154,9 +154,9 @@ fn test_oracle_authorization_enforcement() { 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); + let rate_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); // Clear all mocked auths to test Oracle authorization env.set_auths(&[]); @@ -190,9 +190,9 @@ fn test_zero_duration_finalization() { 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); + let rate_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); // Oracle finalizes with 0 duration (session cancelled) let actual_duration = 0_u64; @@ -219,4 +219,58 @@ fn test_booking_not_found() { // Try to finalize non-existent booking let result = client.try_finalize_session(&999, &50); assert!(result.is_err()); +} + +#[test] +fn test_book_session_balance_transfer() { + // This test specifically verifies the acceptance criteria from Issue #6: + // - User's balance decreases + // - Contract's balance increases + // - Booking ID is unique and retrievable + 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); + + // Initial balance + let initial_balance = 5_000_i128; + token.mint(&user, &initial_balance); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + // Book session with specific deposit + let rate_per_second = 5_i128; + let max_duration = 200_u64; + let expected_deposit = rate_per_second * (max_duration as i128); // 1000 tokens + + // Verify initial state + assert_eq!(token.balance(&user), initial_balance); + assert_eq!(token.balance(&client.address), 0); + + // Book session + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); + + // Acceptance Criteria #1: User's balance decreases + assert_eq!(token.balance(&user), initial_balance - expected_deposit); + + // Acceptance Criteria #2: Contract's balance increases + assert_eq!(token.balance(&client.address), expected_deposit); + + // Acceptance Criteria #3: Booking ID is unique (first booking should be ID 1) + assert_eq!(booking_id, 1); + + // Create another booking to verify uniqueness + token.mint(&user, &expected_deposit); // Mint more tokens for second booking + let booking_id_2 = client.book_session(&user, &expert, &rate_per_second, &max_duration); + + // Second booking should have different ID + assert_eq!(booking_id_2, 2); + assert_ne!(booking_id, booking_id_2); } \ No newline at end of file