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
63 changes: 63 additions & 0 deletions contracts/payment-vault-contract/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,69 @@ pub fn book_session(
Ok(booking_id)
}

pub fn top_up_session(
env: &Env,
user: &Address,
booking_id: u64,
additional_duration: u64,
) -> Result<(), VaultError> {
if storage::is_paused(env) {
return Err(VaultError::ContractPaused);
}

// Require authorization from the user
user.require_auth();

// Get booking and verify it exists
let mut booking = storage::get_booking(env, booking_id).ok_or(VaultError::BookingNotFound)?;

// Verify the caller is the booking owner
if booking.user != *user {
return Err(VaultError::NotAuthorized);
}

// Verify booking is in Pending status
if booking.status != BookingStatus::Pending {
return Err(VaultError::BookingNotPending);
}

// Calculate extra cost
let extra_cost = booking
.rate_per_second
.checked_mul(additional_duration as i128)
.ok_or(VaultError::Overflow)?;

if extra_cost <= 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 extra tokens from user to this contract
let contract_address = env.current_contract_address();
token_client.transfer(user, &contract_address, &extra_cost);

// Update booking
booking.total_deposit = booking
.total_deposit
.checked_add(extra_cost)
.ok_or(VaultError::Overflow)?;
booking.max_duration = booking
.max_duration
.checked_add(additional_duration)
.ok_or(VaultError::Overflow)?;

// Save booking
storage::save_booking(env, &booking);

// Emit event
events::session_topped_up(env, booking_id, additional_duration, extra_cost);

Ok(())
}

pub fn finalize_session(
env: &Env,
booking_id: u64,
Expand Down
6 changes: 6 additions & 0 deletions contracts/payment-vault-contract/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,9 @@ pub fn oracle_updated(env: &Env, old_oracle: &Address, new_oracle: &Address) {
env.events()
.publish(topics, (old_oracle.clone(), new_oracle.clone()));
}

/// Emitted when a user tops up a pending session
pub fn session_topped_up(env: &Env, booking_id: u64, additional_duration: u64, extra_cost: i128) {
let topics = (symbol_short!("top_up"), booking_id);
env.events().publish(topics, (additional_duration, extra_cost));
}
11 changes: 11 additions & 0 deletions contracts/payment-vault-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ impl PaymentVaultContract {
contract::book_session(&env, &user, &expert, max_duration)
}

/// Add more time to a live (or pending) session without disconnecting.
/// Deducts corresponding tokens from the user based on the expert's rate.
pub fn top_up_session(
env: Env,
user: Address,
booking_id: u64,
additional_duration: u64,
) -> Result<(), VaultError> {
contract::top_up_session(&env, &user, booking_id, additional_duration)
}

/// Finalize a session (Oracle-only).
/// Calculates payments based on actual duration and processes refunds.
pub fn finalize_session(
Expand Down
77 changes: 77 additions & 0 deletions contracts/payment-vault-contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1403,3 +1403,80 @@ fn test_expert_pagination_50_bookings() {
let tail = client.get_expert_bookings(&expert, &40, &20);
assert_eq!(tail.len(), 10); // only 10 left from index 40 to 49
}

#[test]
fn test_top_up_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 registry = create_mock_registry(&env);

let token_admin = Address::generate(&env);
let token = create_token_contract(&env, &token_admin);
token.mint(&user, &100_000);

let client = create_client(&env);
client.init(&admin, &token.address, &oracle, &registry);

let rate_per_second = 10_i128;
let initial_duration = 1800_u64; // 30 mins
let additional_duration = 900_u64; // 15 mins

let booking_id = {
client.set_my_rate(&expert, &rate_per_second);
client.book_session(&user, &expert, &initial_duration)
};

let initial_deposit = rate_per_second * (initial_duration as i128); // 18,000
assert_eq!(token.balance(&client.address), initial_deposit);

// Top up 15 mins
let extra_cost = rate_per_second * (additional_duration as i128); // 9,000
let result = client.try_top_up_session(&user, &booking_id, &additional_duration);
assert!(result.is_ok());

// Verify balances
let expected_total_deposit = initial_deposit + extra_cost;
assert_eq!(token.balance(&client.address), expected_total_deposit);

// Verify booking record
let booking = client.get_booking(&booking_id).unwrap();
assert_eq!(booking.max_duration, initial_duration + additional_duration);
assert_eq!(booking.total_deposit, expected_total_deposit);
}

#[test]
fn test_top_up_wrong_user_fails() {
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 registry = create_mock_registry(&env);

let token_admin = Address::generate(&env);
let token = create_token_contract(&env, &token_admin);
token.mint(&user, &40_000);
token.mint(&other_user, &40_000);

let client = create_client(&env);
client.init(&admin, &token.address, &oracle, &registry);

let rate_per_second = 10_i128;
let initial_duration = 1800_u64;

let booking_id = {
client.set_my_rate(&expert, &rate_per_second);
client.book_session(&user, &expert, &initial_duration)
};

let result = client.try_top_up_session(&other_user, &booking_id, &900);
assert!(result.is_err());
}
Loading