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
26 changes: 15 additions & 11 deletions contracts/payment-vault-contract/src/contract.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<u64, VaultError> {
// 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);
Expand All @@ -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)
}

Expand All @@ -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
Expand Down
9 changes: 8 additions & 1 deletion contracts/payment-vault-contract/src/events.rs
Original file line number Diff line number Diff line change
@@ -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));
Expand Down
13 changes: 7 additions & 6 deletions contracts/payment-vault-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod contract;
mod error;
mod events;
mod storage;
mod types;
#[cfg(test)]
mod test;

Expand All @@ -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<u64, VaultError> {
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)
Expand Down
26 changes: 4 additions & 22 deletions contracts/payment-vault-contract/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,16 @@
use soroban_sdk::{contracttype, Address, Env};
use crate::types::{BookingRecord, BookingStatus};

#[contracttype]
#[derive(Clone)]
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)
Expand Down Expand Up @@ -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<Booking> {
pub fn get_booking(env: &Env, booking_id: u64) -> Option<BookingRecord> {
env.storage()
.persistent()
.get(&DataKey::Booking(booking_id))
Expand Down
88 changes: 71 additions & 17 deletions contracts/payment-vault-contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(&[]);
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
23 changes: 23 additions & 0 deletions contracts/payment-vault-contract/src/types.rs
Original file line number Diff line number Diff line change
@@ -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
}