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
115 changes: 115 additions & 0 deletions contracts/payment-vault-contract/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ pub fn book_session(
status: BookingStatus::Pending,
created_at: env.ledger().timestamp(),
started_at: None,
dispute_user_refund: None,
dispute_expert_pay: None,
dispute_remainder_recovered: false,
};

// Save booking
Expand Down Expand Up @@ -411,3 +414,115 @@ pub fn reject_session(env: &Env, expert: &Address, booking_id: u64) -> Result<()

Ok(())
}

/// Admin dispute resolution: forcefully split escrowed funds for a Pending booking.
/// Used when the Oracle crashes or an unresolvable dispute occurs between user and expert.
pub fn resolve_dispute(
env: &Env,
booking_id: u64,
user_refund: i128,
expert_pay: i128,
) -> Result<(), VaultError> {
if storage::is_paused(env) {
return Err(VaultError::ContractPaused);
}

// 1. Require admin authorization
let admin = storage::get_admin(env).ok_or(VaultError::NotInitialized)?;
admin.require_auth();

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

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

// 4. Validate split amounts
if user_refund < 0 || expert_pay < 0 {
return Err(VaultError::InvalidAmount);
}

let total_split = user_refund
.checked_add(expert_pay)
.ok_or(VaultError::Overflow)?;

if total_split > booking.total_deposit {
return Err(VaultError::InvalidAmount);
}

// 5. Get token contract
let token_address = storage::get_token(env);
let token_client = token::Client::new(env, &token_address);
let contract_address = env.current_contract_address();

// 6. Execute transfers
if user_refund > 0 {
token_client.transfer(&contract_address, &booking.user, &user_refund);
}

if expert_pay > 0 {
token_client.transfer(&contract_address, &booking.expert, &expert_pay);
}

// 7. Persist dispute split and transition booking to DisputedAndResolved
booking.status = BookingStatus::DisputedAndResolved;
booking.dispute_user_refund = Some(user_refund);
booking.dispute_expert_pay = Some(expert_pay);
booking.dispute_remainder_recovered = false;
storage::update_booking(env, &booking);

// 8. Emit event
events::dispute_resolved(env, booking_id, user_refund, expert_pay);

Ok(())
}

/// Admin-only recovery path for disputed remainder left in vault after resolve_dispute.
/// Recovers `total_deposit - dispute_user_refund - dispute_expert_pay` exactly once.
pub fn recover_disputed_remainder(env: &Env, booking_id: u64) -> Result<i128, VaultError> {
if storage::is_paused(env) {
return Err(VaultError::ContractPaused);
}

let admin = storage::get_admin(env).ok_or(VaultError::NotInitialized)?;
admin.require_auth();

let mut booking = storage::get_booking(env, booking_id).ok_or(VaultError::BookingNotFound)?;

if booking.status != BookingStatus::DisputedAndResolved {
return Err(VaultError::BookingNotDisputed);
}

if booking.dispute_remainder_recovered {
return Err(VaultError::RemainderAlreadyRecovered);
}

let user_refund = booking.dispute_user_refund.unwrap_or(0);
let expert_pay = booking.dispute_expert_pay.unwrap_or(0);

let remainder = booking
.total_deposit
.checked_sub(user_refund)
.and_then(|v| v.checked_sub(expert_pay))
.ok_or(VaultError::Overflow)?;

if remainder < 0 {
return Err(VaultError::InvalidAmount);
}

let token_address = storage::get_token(env);
let token_client = token::Client::new(env, &token_address);
let contract_address = env.current_contract_address();

if remainder > 0 {
token_client.transfer(&contract_address, &admin, &remainder);
}

booking.dispute_remainder_recovered = true;
storage::update_booking(env, &booking);
events::disputed_remainder_recovered(env, booking_id, remainder);

Ok(remainder)
}
2 changes: 2 additions & 0 deletions contracts/payment-vault-contract/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ pub enum VaultError {
ExpertNotVerified = 10,
SessionAlreadyStarted = 11,
Overflow = 12,
BookingNotDisputed = 13,
RemainderAlreadyRecovered = 14,
}
15 changes: 14 additions & 1 deletion contracts/payment-vault-contract/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,18 @@ pub fn oracle_updated(env: &Env, old_oracle: &Address, new_oracle: &Address) {
/// 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));
env.events()
.publish(topics, (additional_duration, extra_cost));
}

/// Emitted when an admin resolves a dispute by splitting escrowed funds
pub fn dispute_resolved(env: &Env, booking_id: u64, user_refund: i128, expert_pay: i128) {
let topics = (symbol_short!("dispute"), booking_id);
env.events().publish(topics, (user_refund, expert_pay));
}

/// Emitted when admin recovers remaining disputed funds left in vault
pub fn disputed_remainder_recovered(env: &Env, booking_id: u64, amount: i128) {
let topics = (symbol_short!("dsp_rcvr"), booking_id);
env.events().publish(topics, amount);
}
18 changes: 18 additions & 0 deletions contracts/payment-vault-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,24 @@ impl PaymentVaultContract {
contract::cancel_booking(&env, &user, booking_id)
}

/// Resolve a dispute by forcefully splitting escrowed funds (Admin-only).
/// Used when the Oracle crashes or a severe, unresolvable dispute occurs.
/// `user_refund + expert_pay` must not exceed the booking's `total_deposit`.
pub fn resolve_dispute(
env: Env,
booking_id: u64,
user_refund: i128,
expert_pay: i128,
) -> Result<(), VaultError> {
contract::resolve_dispute(&env, booking_id, user_refund, expert_pay)
}

/// Recover any disputed remainder still locked in vault after dispute split (Admin-only).
/// Can be executed once per booking after status reaches DisputedAndResolved.
pub fn recover_disputed_remainder(env: Env, booking_id: u64) -> Result<i128, VaultError> {
contract::recover_disputed_remainder(&env, booking_id)
}

/// Get a paginated list of booking IDs for a specific user.
/// `start_index` is 0-based. Returns at most `limit` booking IDs.
pub fn get_user_bookings(env: Env, user: Address, start_index: u32, limit: u32) -> Vec<u64> {
Expand Down
4 changes: 4 additions & 0 deletions contracts/payment-vault-contract/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ pub fn update_booking_status(env: &Env, booking_id: u64, status: BookingStatus)
}
}

pub fn update_booking(env: &Env, booking: &BookingRecord) {
save_booking(env, booking);
}

pub fn update_booking_started_at(env: &Env, booking_id: u64, started_at: u64) {
if let Some(mut booking) = get_booking(env, booking_id) {
booking.started_at = Some(started_at);
Expand Down
Loading
Loading