Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions contracts/payment-vault-contract/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub fn book_session(
max_duration,
total_deposit,
status: BookingStatus::Pending,
created_at: env.ledger().timestamp(),
};

// Save booking
Expand Down Expand Up @@ -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(())
}
1 change: 1 addition & 0 deletions contracts/payment-vault-contract/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ pub enum VaultError {
BookingNotFound = 4,
BookingNotPending = 5,
InvalidAmount = 6,
ReclaimTooEarly = 7,
}
5 changes: 5 additions & 0 deletions contracts/payment-vault-contract/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
12 changes: 11 additions & 1 deletion contracts/payment-vault-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64> {
storage::get_user_bookings(&env, &user)
Expand All @@ -63,4 +73,4 @@ impl PaymentVaultContract {
pub fn get_booking(env: Env, booking_id: u64) -> Option<BookingRecord> {
storage::get_booking(&env, booking_id)
}
}
}
3 changes: 2 additions & 1 deletion contracts/payment-vault-contract/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address> {
env.storage().instance().get(&DataKey::Admin)
}
Expand Down Expand Up @@ -117,4 +118,4 @@ pub fn get_expert_bookings(env: &Env, expert: &Address) -> soroban_sdk::Vec<u64>
.persistent()
.get(&DataKey::ExpertBookings(expert.clone()))
.unwrap_or(soroban_sdk::Vec::new(env))
}
}
137 changes: 135 additions & 2 deletions contracts/payment-vault-contract/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![cfg(test)]
use crate::{PaymentVaultContract, PaymentVaultContractClient};
use soroban_sdk::{
testutils::Address as _,
testutils::{Address as _, Ledger},
token, Address, Env,
};

Expand Down Expand Up @@ -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());
}
}

#[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());
}
2 changes: 2 additions & 0 deletions contracts/payment-vault-contract/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}