From 377c0c56400fd7be28a6d842bee5bb4be5c7f468 Mon Sep 17 00:00:00 2001 From: Josue19-08 Date: Mon, 26 Jan 2026 11:24:42 -0600 Subject: [PATCH 1/6] feat(payment-vault): add created_at field and Reclaimed status Add created_at timestamp to Booking struct and Reclaimed variant to BookingStatus enum for stale booking reclaim functionality. Co-Authored-By: Claude Opus 4.5 --- contracts/payment-vault-contract/src/storage.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/payment-vault-contract/src/storage.rs b/contracts/payment-vault-contract/src/storage.rs index 8ba49a3..04909d1 100644 --- a/contracts/payment-vault-contract/src/storage.rs +++ b/contracts/payment-vault-contract/src/storage.rs @@ -15,6 +15,7 @@ pub enum DataKey { pub enum BookingStatus { Pending, Complete, + Reclaimed, } #[contracttype] @@ -27,6 +28,7 @@ pub struct Booking { pub total_deposit: i128, // Total amount deposited by user pub booked_duration: u64, // Booked duration in seconds pub status: BookingStatus, + pub created_at: u64, // Ledger timestamp when booking was created } // --- Admin --- From 35d889338f4f7bd75ed5e26647e0b67b37ded296 Mon Sep 17 00:00:00 2001 From: Josue19-08 Date: Mon, 26 Jan 2026 11:24:47 -0600 Subject: [PATCH 2/6] feat(payment-vault): add ReclaimTooEarly error Add error variant for when users try to reclaim funds before the 24-hour timeout has elapsed. Co-Authored-By: Claude Opus 4.5 --- contracts/payment-vault-contract/src/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/payment-vault-contract/src/error.rs b/contracts/payment-vault-contract/src/error.rs index 9803897..28760ff 100644 --- a/contracts/payment-vault-contract/src/error.rs +++ b/contracts/payment-vault-contract/src/error.rs @@ -10,4 +10,5 @@ pub enum VaultError { BookingNotFound = 4, BookingNotPending = 5, InvalidAmount = 6, + ReclaimTooEarly = 7, } \ No newline at end of file From d97be2586f30cf02f932d002791061d4d1a995f5 Mon Sep 17 00:00:00 2001 From: Josue19-08 Date: Mon, 26 Jan 2026 11:24:50 -0600 Subject: [PATCH 3/6] feat(payment-vault): add session_reclaimed event Add event emission for when users reclaim funds from stale bookings. Co-Authored-By: Claude Opus 4.5 --- contracts/payment-vault-contract/src/events.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/payment-vault-contract/src/events.rs b/contracts/payment-vault-contract/src/events.rs index ac8407c..8070c20 100644 --- a/contracts/payment-vault-contract/src/events.rs +++ b/contracts/payment-vault-contract/src/events.rs @@ -4,3 +4,8 @@ pub fn session_finalized(env: &Env, booking_id: u64, actual_duration: u64, total let topics = (symbol_short!("finalized"), booking_id); env.events().publish(topics, (actual_duration, total_cost)); } + +pub fn session_reclaimed(env: &Env, booking_id: u64, amount: i128) { + let topics = (symbol_short!("reclaim"), booking_id); + env.events().publish(topics, amount); +} From 84ff1aeebe438c93f563a4f99c8a98161ab4b6c8 Mon Sep 17 00:00:00 2001 From: Josue19-08 Date: Mon, 26 Jan 2026 11:24:55 -0600 Subject: [PATCH 4/6] feat(payment-vault): implement reclaim_stale_session function Add reclaim_stale_session function allowing users to reclaim their deposit after 24 hours if booking is still pending. Includes: - User authorization check - Booking ownership verification - 24-hour timeout enforcement - Token transfer back to user - Status update to Reclaimed Co-Authored-By: Claude Opus 4.5 --- .../payment-vault-contract/src/contract.rs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/contracts/payment-vault-contract/src/contract.rs b/contracts/payment-vault-contract/src/contract.rs index 7783eed..16e70e3 100644 --- a/contracts/payment-vault-contract/src/contract.rs +++ b/contracts/payment-vault-contract/src/contract.rs @@ -62,6 +62,7 @@ pub fn create_booking( total_deposit, booked_duration, status: BookingStatus::Pending, + created_at: env.ledger().timestamp(), }; // Save booking @@ -119,5 +120,51 @@ pub fn finalize_session( // 8. Emit SessionFinalized event events::session_finalized(env, booking_id, actual_duration, expert_pay); + Ok(()) +} + +/// 24 hours in seconds +const RECLAIM_TIMEOUT: u64 = 86400; + +pub fn reclaim_stale_session( + env: &Env, + user: &Address, + booking_id: u64, +) -> Result<(), VaultError> { + // 1. Require user authorization + user.require_auth(); + + // 2. Get booking and verify it exists + let booking = storage::get_booking(env, booking_id) + .ok_or(VaultError::BookingNotFound)?; + + // 3. Verify the caller is the booking owner + if booking.user != *user { + return Err(VaultError::NotAuthorized); + } + + // 4. Verify booking is in Pending status + if booking.status != BookingStatus::Pending { + return Err(VaultError::BookingNotPending); + } + + // 5. Check if 24 hours have passed since booking creation + let current_time = env.ledger().timestamp(); + if current_time <= booking.created_at + RECLAIM_TIMEOUT { + return Err(VaultError::ReclaimTooEarly); + } + + // 6. Transfer total_deposit back to user + let token_address = storage::get_token(env); + let token_client = token::Client::new(env, &token_address); + let contract_address = env.current_contract_address(); + token_client.transfer(&contract_address, &booking.user, &booking.total_deposit); + + // 7. Update booking status to Reclaimed + storage::update_booking_status(env, booking_id, BookingStatus::Reclaimed); + + // 8. Emit event + events::session_reclaimed(env, booking_id, booking.total_deposit); + Ok(()) } \ No newline at end of file From d9fb3b2a9472ece9874dbade8cceeb923fd88fd2 Mon Sep 17 00:00:00 2001 From: Josue19-08 Date: Mon, 26 Jan 2026 11:24:58 -0600 Subject: [PATCH 5/6] feat(payment-vault): expose reclaim_stale_session as public function Expose reclaim_stale_session in contract interface allowing users to reclaim stuck funds from stale bookings. Co-Authored-By: Claude Opus 4.5 --- contracts/payment-vault-contract/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/payment-vault-contract/src/lib.rs b/contracts/payment-vault-contract/src/lib.rs index 0402cef..57cc393 100644 --- a/contracts/payment-vault-contract/src/lib.rs +++ b/contracts/payment-vault-contract/src/lib.rs @@ -46,4 +46,14 @@ impl PaymentVaultContract { ) -> Result<(), VaultError> { contract::finalize_session(&env, booking_id, actual_duration) } + + /// Reclaim funds from a stale booking (User-only) + /// Users can reclaim their deposit if the booking has been pending for more than 24 hours + pub fn reclaim_stale_session( + env: Env, + user: Address, + booking_id: u64, + ) -> Result<(), VaultError> { + contract::reclaim_stale_session(&env, &user, booking_id) + } } \ No newline at end of file From 3d9973f088c4459ef65d1f94aefc6f48d7491e5c Mon Sep 17 00:00:00 2001 From: Josue19-08 Date: Mon, 26 Jan 2026 11:25:03 -0600 Subject: [PATCH 6/6] test(payment-vault): add stale booking reclaim tests Add tests for reclaim_stale_session functionality: - test_reclaim_stale_session_too_early: fails before 24h - test_reclaim_stale_session_success: succeeds after 25h - test_reclaim_stale_session_wrong_user: fails for non-owner - test_reclaim_already_finalized: fails for completed bookings Co-Authored-By: Claude Opus 4.5 --- contracts/payment-vault-contract/src/test.rs | 135 ++++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) diff --git a/contracts/payment-vault-contract/src/test.rs b/contracts/payment-vault-contract/src/test.rs index c4e0851..fc1103d 100644 --- a/contracts/payment-vault-contract/src/test.rs +++ b/contracts/payment-vault-contract/src/test.rs @@ -1,7 +1,7 @@ #![cfg(test)] use crate::{PaymentVaultContract, PaymentVaultContractClient}; use soroban_sdk::{ - testutils::Address as _, + testutils::{Address as _, Ledger}, token, Address, Env, }; @@ -219,4 +219,137 @@ 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_reclaim_stale_session_too_early() { + 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); + + // User tries to reclaim immediately (should fail - too early) + let result = client.try_reclaim_stale_session(&user, &booking_id); + assert!(result.is_err()); + + // Verify funds are still in contract + assert_eq!(token.balance(&client.address), 1_000); + assert_eq!(token.balance(&user), 9_000); +} + +#[test] +fn test_reclaim_stale_session_success() { + 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); + + // Advance ledger timestamp by 25 hours (90000 seconds) + env.ledger().set_timestamp(env.ledger().timestamp() + 90_000); + + // User tries to reclaim after 25 hours (should succeed) + let result = client.try_reclaim_stale_session(&user, &booking_id); + assert!(result.is_ok()); + + // Verify funds returned to user + assert_eq!(token.balance(&client.address), 0); + assert_eq!(token.balance(&user), 10_000); + assert_eq!(token.balance(&expert), 0); +} + +#[test] +fn test_reclaim_stale_session_wrong_user() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let other_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); + + // Advance ledger timestamp by 25 hours + env.ledger().set_timestamp(env.ledger().timestamp() + 90_000); + + // Other user tries to reclaim (should fail - not authorized) + let result = client.try_reclaim_stale_session(&other_user, &booking_id); + assert!(result.is_err()); + + // Verify funds still in contract + assert_eq!(token.balance(&client.address), 1_000); +} + +#[test] +fn test_reclaim_already_finalized() { + 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 the session + client.finalize_session(&booking_id, &50); + + // Advance ledger timestamp by 25 hours + env.ledger().set_timestamp(env.ledger().timestamp() + 90_000); + + // User tries to reclaim after finalization (should fail - not pending) + let result = client.try_reclaim_stale_session(&user, &booking_id); + assert!(result.is_err()); } \ No newline at end of file