diff --git a/contracts/payment-vault-contract/src/contract.rs b/contracts/payment-vault-contract/src/contract.rs index 919db0f..e43e6c1 100644 --- a/contracts/payment-vault-contract/src/contract.rs +++ b/contracts/payment-vault-contract/src/contract.rs @@ -63,6 +63,7 @@ pub fn book_session( max_duration, total_deposit, status: BookingStatus::Pending, + created_at: env.ledger().timestamp(), }; // Save booking @@ -127,5 +128,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 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 diff --git a/contracts/payment-vault-contract/src/events.rs b/contracts/payment-vault-contract/src/events.rs index f4a27e4..db2a857 100644 --- a/contracts/payment-vault-contract/src/events.rs +++ b/contracts/payment-vault-contract/src/events.rs @@ -11,3 +11,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); +} diff --git a/contracts/payment-vault-contract/src/lib.rs b/contracts/payment-vault-contract/src/lib.rs index 86fab6a..67220ec 100644 --- a/contracts/payment-vault-contract/src/lib.rs +++ b/contracts/payment-vault-contract/src/lib.rs @@ -49,6 +49,16 @@ impl PaymentVaultContract { 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) + } + /// Get all booking IDs for a specific user pub fn get_user_bookings(env: Env, user: Address) -> Vec { storage::get_user_bookings(&env, &user) @@ -63,4 +73,4 @@ impl PaymentVaultContract { pub fn get_booking(env: Env, booking_id: u64) -> Option { storage::get_booking(&env, booking_id) } -} \ 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 5c6caa9..55e8ce3 100644 --- a/contracts/payment-vault-contract/src/storage.rs +++ b/contracts/payment-vault-contract/src/storage.rs @@ -22,6 +22,7 @@ pub fn set_admin(env: &Env, admin: &Address) { env.storage().instance().set(&DataKey::Admin, admin); } +#[allow(dead_code)] pub fn get_admin(env: &Env) -> Option
{ env.storage().instance().get(&DataKey::Admin) } @@ -117,4 +118,4 @@ pub fn get_expert_bookings(env: &Env, expert: &Address) -> soroban_sdk::Vec .persistent() .get(&DataKey::ExpertBookings(expert.clone())) .unwrap_or(soroban_sdk::Vec::new(env)) -} \ No newline at end of file +} diff --git a/contracts/payment-vault-contract/src/test.rs b/contracts/payment-vault-contract/src/test.rs index 2fc4503..054366d 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, }; @@ -327,4 +327,137 @@ fn test_get_user_and_expert_bookings() { // Test get_booking for non-existent booking let non_existent = client.get_booking(&999); assert!(non_existent.is_none()); -} \ No newline at end of file +} + +#[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_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_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_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_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_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_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_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_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()); +} diff --git a/contracts/payment-vault-contract/src/types.rs b/contracts/payment-vault-contract/src/types.rs index 1816565..6d985ad 100644 --- a/contracts/payment-vault-contract/src/types.rs +++ b/contracts/payment-vault-contract/src/types.rs @@ -7,6 +7,7 @@ use soroban_sdk::{contracttype, Address}; pub enum BookingStatus { Pending = 0, Complete = 1, + Reclaimed = 2, } /// Record of a consultation booking with deposit locked @@ -20,4 +21,5 @@ pub struct BookingRecord { 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 + pub created_at: u64, // Ledger timestamp when booking was created }